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
15 changes: 10 additions & 5 deletions Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,9 +1377,10 @@ def getLogger(self, name):
raise TypeError('A logger name must be a string')
# Fast path: an already-registered, non-placeholder logger can be
# returned without taking the lock. dict.get() is atomic under both
# the GIL and free threading, and a Logger is fully initialised before
# being inserted into loggerDict under the lock, so this never sees a
# partially-constructed object.
# the GIL and free threading. A Logger is inserted into loggerDict only
# after it is fully wired up (parent/child references fixed) under the
# lock, so the fast path never observes a logger whose parent is not yet
# set.
rv = self.loggerDict.get(name)
if rv is not None and not isinstance(rv, PlaceHolder):
return rv
Expand All @@ -1390,14 +1391,18 @@ def getLogger(self, name):
ph = rv
rv = (self.loggerClass or _loggerClass)(name)
rv.manager = self
self.loggerDict[name] = rv
self._fixupChildren(ph, rv)
self._fixupParents(rv)
# Publish only after rv is fully wired: the fast path reads
# loggerDict without the lock.
self.loggerDict[name] = rv
else:
rv = (self.loggerClass or _loggerClass)(name)
rv.manager = self
self.loggerDict[name] = rv
self._fixupParents(rv)
# Publish only after rv is fully wired: the fast path reads
# loggerDict without the lock.
self.loggerDict[name] = rv
return rv

def setLoggerClass(self, klass):
Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4269,6 +4269,43 @@ def test_set_log_record_factory(self):
man.setLogRecordFactory(expected)
self.assertEqual(man.logRecordFactory, expected)

@threading_helper.requires_working_threading()
def test_getLogger_fast_path_never_returns_unwired_logger(self):
# getLogger()'s lock-free fast path returns a logger straight out of
# loggerDict, so a logger must be published there only after
# _fixupParents() has set its parent; otherwise a concurrent caller
# observes it detached from the hierarchy (gh-150818 follow-up).
manager = logging.Manager(logging.RootLogger(logging.WARNING))
name = 'a.b.c'

paused = threading.Event()
seen = []
real_fixup = manager._fixupParents

# Pause the creating thread between publishing rv and wiring its
# parent, then read loggerDict the way the fast path does and snapshot
# the parent at that instant (rv is wired in place soon after).
def fixup(alogger):
paused.set()
reader.join()
real_fixup(alogger)

def read():
paused.wait()
rv = manager.loggerDict.get(name)
if rv is not None and not isinstance(rv, logging.PlaceHolder):
seen.append(rv.parent)

reader = threading.Thread(target=read)
manager._fixupParents = fixup
try:
reader.start()
manager.getLogger(name)
finally:
manager._fixupParents = real_fixup

self.assertNotIn(None, seen)

class ChildLoggerTest(BaseTest):
def test_child_loggers(self):
r = logging.getLogger()
Expand Down
Loading