Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/source/NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ Fixes
server accessed over TLS (or any other framing mismatch) fails fast
rather than hanging the client. Also removes a stray ``print`` from
production code (#170, #240, #243).
- ``SearchByTreeWalkingMixin.search`` now waits for the underlying
subtree iterator's Deferred before firing its own. Previously the
iterator's Deferred was silently discarded, so back ends whose
``children`` / ``subtree`` methods actually behaved asynchronously
returned an empty result set (#62).


21.2.0 (2021-02-28)
Expand Down
8 changes: 4 additions & 4 deletions ldaptor/entryhelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@ def _tryMatch(entry):
if entry.match(filterObject):
matchCallback(entry)

iterator(callback=_tryMatch)

d = defer.maybeDeferred(iterator, callback=_tryMatch)
if callback is None:
return defer.succeed(results)
d.addCallback(lambda _: results)
else:
return defer.succeed(None)
d.addCallback(lambda _: None)
return d
44 changes: 44 additions & 0 deletions ldaptor/test/test_inmemory.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,3 +827,47 @@ def testDeleteSubtree(self):
],
)
return d


class AsyncChildrenEntry(inmemory.ReadOnlyInMemoryLDAPEntry):
"""
An entry whose ``children`` returns a Deferred that fires later, so the
surrounding subtree walk is genuinely asynchronous (issue #62).
"""

def children(self, callback=None):
from twisted.internet import defer as _defer

self.pending = _defer.Deferred()
base = inmemory.ReadOnlyInMemoryLDAPEntry
self.pending.addCallback(lambda _: base.children(self, callback=callback))
return self.pending


class TestSearchAsync(unittest.TestCase):
def test_search_waitsForAsyncIterator(self):
"""
SearchByTreeWalkingMixin.search must return a Deferred that fires
only after the underlying subtree iterator's Deferred has fired.
Previously the iterator's Deferred was discarded, so asynchronous
back ends silently returned an empty result set.
"""
root = AsyncChildrenEntry(
dn=distinguishedname.DistinguishedName("dc=example,dc=com"),
attributes={"objectClass": ["a"], "dc": ["example"]},
)
root.addChild(
rdn="cn=foo",
attributes={"objectClass": ["a"], "cn": ["foo"]},
)

d = root.search()
fired = []
d.addCallback(fired.append)

self.assertEqual(fired, [], "search Deferred fired before children")
root.pending.callback(None)
self.assertEqual(len(fired), 1)
dns = [str(e.dn) for e in fired[0]]
self.assertIn("dc=example,dc=com", dns)
self.assertIn("cn=foo,dc=example,dc=com", dns)
Loading