-
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
Changes from 1 commit
81ab75d
1131fd5
60bc080
03e1d76
51a1a4f
698ccfe
7279710
a9daeeb
2f623f0
9de41ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package ldap | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"reflect" | ||
|
@@ -375,6 +376,28 @@ func (s *SearchResult) appendTo(r *SearchResult) { | |
r.Controls = append(r.Controls, s.Controls...) | ||
} | ||
|
||
// 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 | ||
} | ||
|
||
// Print outputs a human-readable description | ||
func (s *SearchSingleResult) Print() { | ||
s.Entry.Print() | ||
} | ||
|
||
// PrettyPrint outputs a human-readable description with indenting | ||
func (s *SearchSingleResult) PrettyPrint(indent int) { | ||
s.Entry.PrettyPrint(indent) | ||
} | ||
|
||
// SearchRequest represents a search request to send to the server | ||
type SearchRequest struct { | ||
BaseDN string | ||
|
@@ -559,6 +582,111 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { | |
} | ||
} | ||
|
||
// SearchWithChannel performs a search request and returns all search results | ||
// via the returned channel as soon as they are received. This means you get | ||
// all results until an error happens (or the search successfully finished), | ||
// 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) | ||
go func() { | ||
defer close(ch) | ||
if l.IsClosing() { | ||
return | ||
} | ||
|
||
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") | ||
packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) | ||
// encode search request | ||
err := searchRequest.appendTo(packet) | ||
if err != nil { | ||
ch <- &SearchSingleResult{Error: err} | ||
return | ||
} | ||
l.Debug.PrintPacket(packet) | ||
|
||
msgCtx, err := l.sendMessage(packet) | ||
if err != nil { | ||
ch <- &SearchSingleResult{Error: err} | ||
return | ||
} | ||
defer l.finishMessage(msgCtx) | ||
|
||
foundSearchSingleResultDone := false | ||
for !foundSearchSingleResultDone { | ||
select { | ||
case <-ctx.Done(): | ||
l.Debug.Printf("%d: %s", msgCtx.id, ctx.Err().Error()) | ||
return | ||
default: | ||
l.Debug.Printf("%d: waiting for response", msgCtx.id) | ||
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 commentThe 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 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 commentThe 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In any case, I learned |
||
return | ||
} | ||
packet, err = packetResponse.ReadPacket() | ||
l.Debug.Printf("%d: got response %p", msgCtx.id, packet) | ||
if err != nil { | ||
ch <- &SearchSingleResult{Error: err} | ||
return | ||
} | ||
|
||
if l.Debug { | ||
if err := addLDAPDescriptions(packet); err != nil { | ||
ch <- &SearchSingleResult{Error: err} | ||
return | ||
} | ||
ber.PrintPacket(packet) | ||
} | ||
|
||
switch packet.Children[1].Tag { | ||
case ApplicationSearchResultEntry: | ||
entry := new(Entry) | ||
entry.DN = packet.Children[1].Children[0].Value.(string) | ||
for _, child := range packet.Children[1].Children[1].Children { | ||
cpuschma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
attr := new(EntryAttribute) | ||
attr.Name = child.Children[0].Value.(string) | ||
for _, value := range child.Children[1].Children { | ||
attr.Values = append(attr.Values, value.Value.(string)) | ||
attr.ByteValues = append(attr.ByteValues, value.ByteValue) | ||
} | ||
entry.Attributes = append(entry.Attributes, attr) | ||
} | ||
ch <- &SearchSingleResult{Entry: entry} | ||
|
||
case ApplicationSearchResultDone: | ||
if err := GetLDAPError(packet); err != nil { | ||
ch <- &SearchSingleResult{Error: err} | ||
return | ||
} | ||
if len(packet.Children) == 3 { | ||
result := &SearchSingleResult{} | ||
for _, child := range packet.Children[2].Children { | ||
decodedChild, err := DecodeControl(child) | ||
if err != nil { | ||
werr := fmt.Errorf("failed to decode child control: %w", err) | ||
ch <- &SearchSingleResult{Error: werr} | ||
return | ||
} | ||
result.Controls = append(result.Controls, decodedChild) | ||
} | ||
ch <- result | ||
} | ||
foundSearchSingleResultDone = true | ||
|
||
case ApplicationSearchResultReference: | ||
ref := packet.Children[1].Children[0].Value.(string) | ||
ch <- &SearchSingleResult{Referral: ref} | ||
} | ||
} | ||
} | ||
l.Debug.Printf("%d: returning", msgCtx.id) | ||
}() | ||
return ch | ||
} | ||
|
||
// unpackAttributes will extract all given LDAP attributes and it's values | ||
// from the ber.Packet | ||
func unpackAttributes(children []*ber.Packet) []*EntryAttribute { | ||
|
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
. IfchannelSize
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.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.
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
search
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.