Skip to content

Commit efb76f7

Browse files
committed
test: Implement test cases for reconnection handling
test_106_reconnect_restore() handles a SERVER_DOWN exception manually and tries to re-use the connection afterwards again. This established the connection again but did not bind(), so it now raises ldap.INSUFFICIENT_ACCESS. test_107_reconnect_restore() restarts the LDAP server during searches, which causes a UNAVAILABLE exception.
1 parent 2229d83 commit efb76f7

File tree

2 files changed

+84
-3
lines changed

2 files changed

+84
-3
lines changed

Lib/slapdtest/_slapdtest.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,8 +467,16 @@ def restart(self):
467467
"""
468468
Restarts the slapd server with same data
469469
"""
470-
self._proc.terminate()
470+
self.terminate()
471471
self.wait()
472+
self.resume()
473+
474+
def terminate(self):
475+
"""Terminate slapd server"""
476+
self._proc.terminate()
477+
478+
def resume(self):
479+
"""Start slapd server"""
472480
self._start_slapd()
473481

474482
def wait(self):

Tests/t_ldapobject.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
import os
1010
import re
1111
import socket
12+
import threading
13+
import time
14+
import traceback
1215
import unittest
1316
import pickle
1417

18+
1519
# Switch off processing .ldaprc or ldap.conf before importing _ldap
1620
os.environ['LDAPNOINIT'] = '1'
1721

@@ -631,7 +635,7 @@ def test105_reconnect_restore(self):
631635
bind_dn = 'cn=user1,'+self.server.suffix
632636
l1.simple_bind_s(bind_dn, 'user1_pw')
633637
self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn)
634-
self.server._proc.terminate()
638+
self.server.terminate()
635639
self.server.wait()
636640
try:
637641
l1.whoami_s()
@@ -640,9 +644,78 @@ def test105_reconnect_restore(self):
640644
else:
641645
self.assertEqual(True, False)
642646
finally:
643-
self.server._start_slapd()
647+
self.server.resume()
644648
self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn)
645649

650+
def test106_reconnect_restore(self):
651+
"""
652+
The idea of this test is to stop the LDAP server, make a search and ignore the `SERVER_DOWN` exception which happens after the reconnect timeout
653+
and then re-use the same connection when the LDAP server is available again.
654+
After starting the server the LDAP connection can be re-used again as it will reconnect on the next operation.
655+
Prior to fixing PR !267 the connection was reestablished but no `bind()` was done resulting in a anonymous search which caused `INSUFFICIENT_ACCESS` when anonymous seach is disallowed.
656+
"""
657+
lo = self.ldap_object_class(self.server.ldap_uri, retry_max=2, retry_delay=1)
658+
bind_dn = 'cn=user1,' + self.server.suffix
659+
lo.simple_bind_s(bind_dn, 'user1_pw')
660+
661+
dn = lo.whoami_s()[3:]
662+
663+
self.server.terminate()
664+
self.server.wait()
665+
666+
# do a search, wait for the timeout, ignore SERVER_DOWN
667+
with self.assertRaises(ldap.SERVER_DOWN):
668+
lo.search_s(dn, ldap.SCOPE_BASE, '(objectClass=*)')
669+
670+
self.server.resume()
671+
672+
# try to use the connection again
673+
lo.search_s(dn, ldap.SCOPE_BASE, '(objectClass=*)')
674+
675+
def test107_reconnect_restore(self):
676+
"""
677+
The idea of this test is to restart the LDAP-Server while there are ongoing searches.
678+
This causes :class:`ldap.UNAVAILABLE` to be raised (with |OpenLDAP|) for a short time.
679+
To increase the chance of triggering this bug we are starting multiple threads
680+
with a large number of retry attempts in a short amount of time.
681+
"""
682+
excs = []
683+
thread_count = 10
684+
run_time = 10.0
685+
start_barrier = threading.Barrier(thread_count + 1) # +1 for the main thread
686+
687+
def _reconnect_search_thread():
688+
lo = self.ldap_object_class(self.server.ldap_uri)
689+
bind_dn = 'cn=user1,' + self.server.suffix
690+
lo.simple_bind_s(bind_dn, 'user1_pw')
691+
lo._retry_max = 10E4
692+
lo._retry_delay = 0.001
693+
lo.search_ext_s(self.server.suffix, ldap.SCOPE_SUBTREE, "cn=user1", attrlist=["cn"])
694+
start_barrier.wait()
695+
end_time = time.time() + run_time
696+
while time.time() < end_time:
697+
lo.search_ext_s(self.server.suffix, ldap.SCOPE_SUBTREE, filterstr="cn=user1", attrlist=["cn"])
698+
699+
def reconnect_search_thread():
700+
try:
701+
_reconnect_search_thread()
702+
except Exception as exc:
703+
excs.append((str(exc), traceback.format_exc()))
704+
705+
threads = [threading.Thread(target=reconnect_search_thread) for _ in range(thread_count)]
706+
for t in threads:
707+
t.start()
708+
709+
start_barrier.wait() # wait until all threads are ready to start
710+
self.server.restart() # restart after all threads have started their search loop
711+
712+
for t in threads:
713+
t.join()
714+
715+
for exc, tb in excs[:5]:
716+
print('Exception occurred', exc, tb)
717+
self.assertEqual(excs, [])
718+
646719

647720
@requires_init_fd()
648721
class Test03_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject):

0 commit comments

Comments
 (0)