Add search asynchronously with context#440
Conversation
There was a problem hiding this comment.
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.
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.
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.
I confirmed a deadlock issue using a channel with zero buffer size sometimes. Asynchronous is difficult.
There was a problem hiding this comment.
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.
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.
To unlock the mutex, defer l.finishMessage(msgCtx) must be called. Got it.
There was a problem hiding this comment.
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.
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.
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.
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 |
|
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