-
Notifications
You must be signed in to change notification settings - Fork 363
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
Conversation
There was a problem hiding this 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
// 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) |
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
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) {
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
I agree with |
I found |
How do we get a consensus asynchronous search design to move this PR forward? |
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 |
@johnweldon Thank you for reviewing. I also added SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response
type Response interface {
Entry() *Entry
Referral() string
Controls() []Control
Err() error
Next() bool
} |
As stated in my review, I would go for the design similar to the type Response interface {
Entry() *Entry
Referral() string
Controls() []Control
Err() error
Next() bool
} |
We have three options now.
At the current moment, we're considering No.2 (only SearchAsync()) as better, right? |
Agreed; I vote no.2 as the preferred choice. |
@cpuschma @stefanmcshane @vetinari What do you think above three options? I think No. 2 is better, too. |
Sorry I'm currently sick. I would also go for Option 2 with the SQL like syntax |
Thanks for the comments. I'm going to update the implementation to provide No.2 only. |
Got race condition with go 1.16.x.
https://github.com/go-ldap/ldap/actions/runs/5340457447/jobs/9680294821 |
@cpuschma @johnweldon @stefanmcshane @vetinari I cleaned up this PR to provide SearchAsync() only. Could you review it? |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Thanks
I'll prepare a pre-release for people to test the new feature |
…is trying to lock, and another goroutine waits since full of channel buffers go-ldap#440
…is trying to lock, and another goroutine waits since full of channel buffers go-ldap#440
After I discussed with @cpuschma and @johnweldon, we decided to provide a search asynchronous function.
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.
I'm new to LDAP protocol, so any comments are welcome.
References