Skip to content

Commit 3b4b28e

Browse files
authored
[3.7] bpo-36533: Reinit logging.Handler locks on fork(). (GH-12704) (GH-13170)
Instead of attempting to acquire and release them all across fork which was leading to deadlocks in some applications that had chained their own handlers while holding multiple locks. (cherry picked from commit 64aa6d2) Co-authored-by: Gregory P. Smith <[email protected]> [Google LLC]
1 parent 299f69c commit 3b4b28e

File tree

3 files changed

+58
-40
lines changed

3 files changed

+58
-40
lines changed

Lib/logging/__init__.py

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -229,49 +229,38 @@ def _releaseLock():
229229
# Prevent a held logging lock from blocking a child from logging.
230230

231231
if not hasattr(os, 'register_at_fork'): # Windows and friends.
232-
def _register_at_fork_acquire_release(instance):
232+
def _register_at_fork_reinit_lock(instance):
233233
pass # no-op when os.register_at_fork does not exist.
234-
else: # The os.register_at_fork API exists
235-
os.register_at_fork(before=_acquireLock,
236-
after_in_child=_releaseLock,
237-
after_in_parent=_releaseLock)
238-
239-
# A collection of instances with acquire and release methods (logging.Handler)
240-
# to be called before and after fork. The weakref avoids us keeping discarded
241-
# Handler instances alive forever in case an odd program creates and destroys
242-
# many over its lifetime.
243-
_at_fork_acquire_release_weakset = weakref.WeakSet()
244-
245-
246-
def _register_at_fork_acquire_release(instance):
247-
# We put the instance itself in a single WeakSet as we MUST have only
248-
# one atomic weak ref. used by both before and after atfork calls to
249-
# guarantee matched pairs of acquire and release calls.
250-
_at_fork_acquire_release_weakset.add(instance)
251-
234+
else:
235+
# A collection of instances with a createLock method (logging.Handler)
236+
# to be called in the child after forking. The weakref avoids us keeping
237+
# discarded Handler instances alive. A set is used to avoid accumulating
238+
# duplicate registrations as createLock() is responsible for registering
239+
# a new Handler instance with this set in the first place.
240+
_at_fork_reinit_lock_weakset = weakref.WeakSet()
241+
242+
def _register_at_fork_reinit_lock(instance):
243+
_acquireLock()
244+
try:
245+
_at_fork_reinit_lock_weakset.add(instance)
246+
finally:
247+
_releaseLock()
252248

253-
def _at_fork_weak_calls(method_name):
254-
for instance in _at_fork_acquire_release_weakset:
255-
method = getattr(instance, method_name)
249+
def _after_at_fork_child_reinit_locks():
250+
# _acquireLock() was called in the parent before forking.
251+
for handler in _at_fork_reinit_lock_weakset:
256252
try:
257-
method()
253+
handler.createLock()
258254
except Exception as err:
259255
# Similar to what PyErr_WriteUnraisable does.
260256
print("Ignoring exception from logging atfork", instance,
261-
method_name, "method:", err, file=sys.stderr)
262-
263-
264-
def _before_at_fork_weak_calls():
265-
_at_fork_weak_calls('acquire')
257+
"._reinit_lock() method:", err, file=sys.stderr)
258+
_releaseLock() # Acquired by os.register_at_fork(before=.
266259

267260

268-
def _after_at_fork_weak_calls():
269-
_at_fork_weak_calls('release')
270-
271-
272-
os.register_at_fork(before=_before_at_fork_weak_calls,
273-
after_in_child=_after_at_fork_weak_calls,
274-
after_in_parent=_after_at_fork_weak_calls)
261+
os.register_at_fork(before=_acquireLock,
262+
after_in_child=_after_at_fork_child_reinit_locks,
263+
after_in_parent=_releaseLock)
275264

276265

277266
#---------------------------------------------------------------------------
@@ -844,7 +833,7 @@ def createLock(self):
844833
Acquire a thread lock for serializing access to the underlying I/O.
845834
"""
846835
self.lock = threading.RLock()
847-
_register_at_fork_acquire_release(self)
836+
_register_at_fork_reinit_lock(self)
848837

849838
def acquire(self):
850839
"""

Lib/test/test_logging.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -673,10 +673,28 @@ def remove_loop(fname, tries):
673673
# register_at_fork mechanism is also present and used.
674674
@unittest.skipIf(not hasattr(os, 'fork'), 'Test requires os.fork().')
675675
def test_post_fork_child_no_deadlock(self):
676-
"""Ensure forked child logging locks are not held; bpo-6721."""
677-
refed_h = logging.Handler()
676+
"""Ensure child logging locks are not held; bpo-6721 & bpo-36533."""
677+
class _OurHandler(logging.Handler):
678+
def __init__(self):
679+
super().__init__()
680+
self.sub_handler = logging.StreamHandler(
681+
stream=open('/dev/null', 'wt'))
682+
683+
def emit(self, record):
684+
self.sub_handler.acquire()
685+
try:
686+
self.sub_handler.emit(record)
687+
finally:
688+
self.sub_handler.release()
689+
690+
self.assertEqual(len(logging._handlers), 0)
691+
refed_h = _OurHandler()
678692
refed_h.name = 'because we need at least one for this test'
679693
self.assertGreater(len(logging._handlers), 0)
694+
self.assertGreater(len(logging._at_fork_reinit_lock_weakset), 1)
695+
test_logger = logging.getLogger('test_post_fork_child_no_deadlock')
696+
test_logger.addHandler(refed_h)
697+
test_logger.setLevel(logging.DEBUG)
680698

681699
locks_held__ready_to_fork = threading.Event()
682700
fork_happened__release_locks_and_end_thread = threading.Event()
@@ -714,19 +732,24 @@ def lock_holder_thread_fn():
714732
locks_held__ready_to_fork.wait()
715733
pid = os.fork()
716734
if pid == 0: # Child.
717-
logging.error(r'Child process did not deadlock. \o/')
718-
os._exit(0)
735+
try:
736+
test_logger.info(r'Child process did not deadlock. \o/')
737+
finally:
738+
os._exit(0)
719739
else: # Parent.
740+
test_logger.info(r'Parent process returned from fork. \o/')
720741
fork_happened__release_locks_and_end_thread.set()
721742
lock_holder_thread.join()
722743
start_time = time.monotonic()
723744
while True:
745+
test_logger.debug('Waiting for child process.')
724746
waited_pid, status = os.waitpid(pid, os.WNOHANG)
725747
if waited_pid == pid:
726748
break # child process exited.
727749
if time.monotonic() - start_time > 7:
728750
break # so long? implies child deadlock.
729751
time.sleep(0.05)
752+
test_logger.debug('Done waiting.')
730753
if waited_pid != pid:
731754
os.kill(pid, signal.SIGKILL)
732755
waited_pid, status = os.waitpid(pid, 0)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Reinitialize logging.Handler locks in forked child processes instead of
2+
attempting to acquire them all in the parent before forking only to be
3+
released in the child process. The acquire/release pattern was leading to
4+
deadlocks in code that has implemented any form of chained logging handlers
5+
that depend upon one another as the lock acquision order cannot be
6+
guaranteed.

0 commit comments

Comments
 (0)