Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search asynchronously with context #440

Merged
merged 10 commits into from Jun 30, 2023

Conversation

t2y
Copy link
Contributor

@t2y t2y commented Jun 1, 2023

After I discussed with @cpuschma and @johnweldon, we decided to provide a search asynchronous function.

SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response

type Response interface {
	Entry() *Entry
	Referral() string
	Controls() []Control
	Err() error
	Next() bool
}

This PR is inspired by #319, I appreciate @stefanmcshane.

#319 is a draft, and it seems there is no activity now. So, I recreated the functionality with the addition of my idea.

This PR is different from #319. I describe them below.

  • manage channel and goroutine in ldap.Conn since a developer can use it easily
  • can cancel with context by the caller's side
  • add SearchSingleResult for a single entry and handle an error
func (l *Conn) SearchWithChannel(ctx context.Context, searchRequest *SearchRequest) chan *SearchSingleResult
// SearchSingleResult holds the server's single response to a search request
type SearchSingleResult struct {
	// Entry is the returned entry
	Entry *Entry
	// Referral is the returned referral
	Referral string
	// Controls are the returned controls
	Controls []Control
	// Error is set when the search request was failed
	Error error
}

I'm new to LDAP protocol, so any comments are welcome.

References

Copy link
Member

@cpuschma cpuschma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for refreshing the PR.

I would like to question the design for asynchronous search queries in general: What do you think about a design similar to go/databases/sql/driver.go, like:

result, err := conn.SearchAsync(...)
if err != nil {
    // ...
}

for result.Next() {
    entry := result.Entry()
}

Thus one would not have to release a channel directly and would be freer in the implementation which goes to future changes.

@stefanmcshane @vetinari @johnweldon I would be glad about your opinions

search.go Outdated Show resolved Hide resolved
search.go Outdated
// e.g. for size / time limited requests all are recieved via the channel
// until the limit is reached.
func (l *Conn) SearchWithChannel(ctx context.Context, searchRequest *SearchRequest) chan *SearchSingleResult {
ch := make(chan *SearchSingleResult)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am conflicted about the channels, already with the first pull request. I would like to leave as many options open to the developer, such as determining the channel size. A suggestion and I would ask for general feedback here:

An additional argument for the function channelSize. If channelSize is 0, a normal channel without a certain size is created like now. If the value is greater than 0, a buffered channel with the defined size is created, e.g.

var chan *SearchSingleResult
if channelSize > 0 {
    chan = make(chan *SearchSingleResult, channelSize)
} else {
    chan = make(chan *SearchSingleResult)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense. Also, SearchWithPaging takes pagingSize, so it seems it's no wonder API SearchWithChannel takes channelSize.

func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirmed a deadlock issue using a channel with zero buffer size sometimes. Asynchronous is difficult.

Copy link
Contributor Author

@t2y t2y Jun 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it occurs running with the below order.

caller

  • 1: result <- ch
  • 4: cancel() // never cancel when the search is blocked to write the next result
  • 5: result <- ch // block

search

  • 2: ch <- result
  • 3: ch <- result // block

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess another deadlock issue with zero buffer size is here: https://github.com/go-ldap/ldap/actions/runs/5167123452/jobs/9307800176?pr=440

client

3 defer l.Close() //block

search

1: ch <- result
2: ch <- result // block

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To unlock the mutex, defer l.finishMessage(msgCtx) must be called. Got it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not a bug on go-ldap code. So I fixed on caller's code.

search.go Outdated
packetResponse, ok := <-msgCtx.responses
if !ok {
err := NewError(ErrorNetwork, errors.New("ldap: response channel closed"))
ch <- &SearchSingleResult{Error: err}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These writes do not account for a closed channel, which would cause a panic. Since this goroutines does not have a recover in the deferred function, the program would crash.

If we stay with this design, I suggest to add a warning to the function's description to not close the channel outside the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good review! I changed the returned channel to receive-only. By doing this, the compiler rejects when the caller closes it.

$ go run main.go 
# command-line-arguments
./main.go:62:10: invalid operation: cannot close receive-only channel ch (variable of type <-chan *ldap.SearchSingleResult)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, I learned recover is needed to avoid panic from https://github.com/go-ldap/ldap/actions/runs/5166940415/jobs/9307492985.

@t2y
Copy link
Contributor Author

t2y commented Jun 4, 2023

I agree with conn.SearchAsync(...) API design when we provide a more robust and extendable API. I will change the API signature when other reviewers want.

@t2y
Copy link
Contributor Author

t2y commented Jun 4, 2023

I found conn.SearchAsync(...) API also suggested on #341. So I can merge some requirements if they are needed.

@t2y
Copy link
Contributor Author

t2y commented Jun 5, 2023

@cpuschma I implemented asynchronous search for v3 only (a9daeeb) as a basis for discussion. I hope it's helpful to discuss the design we require.

@t2y
Copy link
Contributor Author

t2y commented Jun 10, 2023

How do we get a consensus asynchronous search design to move this PR forward?

@johnweldon
Copy link
Member

Thanks for the updates @t2y

I'm also skeptical of exposing an internally created channel on the public API of the library.

I'd like feedback from others too though @go-ldap/committers

@t2y
Copy link
Contributor Author

t2y commented Jun 13, 2023

@johnweldon Thank you for reviewing. I also added conn.SearchAsync(...) API as a reference implementation. Tell me if you have a good idea.

SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response

type Response interface {
	Entry() *Entry
	Referral() string
	Controls() []Control
	Err() error
	Next() bool
}

@cpuschma
Copy link
Member

As stated in my review, I would go for the design similar to the sql.Rows interface of the official sql library. It would allow us to keep the internals, which makes changes easier. I think an interface like t2ys one is sufficient

type Response interface {
	Entry() *Entry
	Referral() string
	Controls() []Control
	Err() error
	Next() bool
}

@t2y
Copy link
Contributor Author

t2y commented Jun 16, 2023

We have three options now.

  1. provides only SearchWithChannel() returns the channel
  2. provides only SearchAsync() returns the abstract Response interface
  3. provides both SearchWithChannel() and SearchAsync()

At the current moment, we're considering No.2 (only SearchAsync()) as better, right?

@johnweldon
Copy link
Member

Agreed; I vote no.2 as the preferred choice.

@t2y
Copy link
Contributor Author

t2y commented Jun 19, 2023

@cpuschma @stefanmcshane @vetinari What do you think above three options? I think No. 2 is better, too.

@cpuschma
Copy link
Member

Sorry I'm currently sick. I would also go for Option 2 with the SQL like syntax

@t2y
Copy link
Contributor Author

t2y commented Jun 22, 2023

Thanks for the comments. I'm going to update the implementation to provide No.2 only.

@t2y
Copy link
Contributor Author

t2y commented Jun 22, 2023

Got race condition with go 1.16.x.

  github.com/go-ldap/ldap.(*Conn).GetLastError()
      /home/runner/work/ldap/ldap/conn.go:330 +0x204
--- FAIL: TestSearchAsyncAndCancel (0.06s)
    ldap_test.go:411: TestSearchAsyncAndCancel: (&(objectclass=rfc822mailgroup)(cn=*Computer*)) -> num of entries = 11
    testing.go:1102: race detected during execution of test

https://github.com/go-ldap/ldap/actions/runs/5340457447/jobs/9680294821

@t2y
Copy link
Contributor Author

t2y commented Jun 22, 2023

@cpuschma @johnweldon @stefanmcshane @vetinari I cleaned up this PR to provide SearchAsync() only. Could you review it?

@t2y t2y changed the title Add search with channels with context Add search asynchronously with context Jun 22, 2023
@cpuschma
Copy link
Member

I did check the source during lunchtime. LGTM so far, I'll run a few tests later and give you feedback as soon as I'm finished

Copy link
Member

@johnweldon johnweldon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Thanks

@cpuschma cpuschma merged commit 7778a1c into go-ldap:master Jun 30, 2023
16 checks passed
@cpuschma
Copy link
Member

I'll prepare a pre-release for people to test the new feature

@t2y t2y deleted the search-with-channel branch July 3, 2023 13:46
t2y added a commit to t2y/ldap that referenced this pull request Jul 19, 2023
…is trying to lock, and another goroutine waits since full of channel buffers go-ldap#440
t2y added a commit to t2y/ldap that referenced this pull request Jul 20, 2023
…is trying to lock, and another goroutine waits since full of channel buffers go-ldap#440
cpuschma pushed a commit that referenced this pull request Jul 22, 2023
…is trying to lock, and another goroutine waits since full of channel buffers #440 (#446)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants