Skip to content

logging deadlock when dictConfig or fileConfig used concurrently with custom handlers #96727

@shalabhc

Description

@shalabhc

Bug report

The attached code below fairly reliably reproduces the deadlock.

There is a module level lock in logging/__init__.py. Each handler has its own lock also, implemented as an instance attribute.

Calling dictConfig (or fileConfig) locks them in this order:

  1. acquire module lock
  2. acquire handler locks in _clearExistingHandlers, which calls shutdown

Calling some logging functions from within a custom emit() can lock in this order:

  1. acquire handler lock (acquired before emit is called)
  2. acquire module lock (if emit calls into a library that uses logging.getLogger(), it will acquire the module lock)

The same issue exists with fileConfig. Note calling fileConfig during program execution is ok as per the docs: This function can be called several times from an application, allowing an end user to select from various pre-canned configurations (if the developer provides a mechanism to present the choices and load the chosen configuration).

While emit may not directly include code to logging.getLogger() it may call into another library that happens to do that. Even logger.isEnabledFor and logger.setLevel etc acquire the module lock and have the same issue.

Your environment

  • CPython versions tested on: 3.8.13, 3.10.5
  • Operating system and architecture: mac-os

Code to reproduce deadlock

import logging
import logging.config
import threading
import time
import os


def tick(msg):
    print(time.strftime("%H:%M:%S"), msg)


class CustomHandler(logging.Handler):
    def emit(self, record):
        logging.getLogger("other-logger")  # comment out this line to avoid deadlock
        pass


def log_loop(i):
    while True:
        logger = logging.getLogger()
        logger.setLevel(logging.INFO)
        logger.info("some log")

        tick(f"tick from log_loop-{i}")


def config_loop():
    while True:
        config_once()


def config_once():
    logging.config.dictConfig(
        {
            "version": 1,
            "handlers": {"custom": {"class": "logging_deadlock.CustomHandler"}},
            "root": {"handlers": ["custom"]},
        }
    )
    tick(f"tick from config_once. pid: {os.getpid()}")


if __name__ == "__main__":
    for i in range(10):
        t = threading.Thread(target=log_loop, args=[i], name=f"log_loop-{i}")
        t.start()

    t2 = threading.Thread(target=config_loop, name="config_loop")
    t2.start()

Call stacks that deadlocked

Thread 0x305833000 (idle): "log_loop-3"
    _acquireLock (logging/__init__.py:226)  # asking for the module lock
    getLogger (logging/__init__.py:1329)
    getLogger (logging/__init__.py:2079)
    emit (logging_deadlock.py:16)
    handle (logging/__init__.py:968)   # this acquired the handler lock
    callHandlers (logging/__init__.py:1696)
    handle (logging/__init__.py:1634)
    _log (logging/__init__.py:1624)
    info (logging/__init__.py:1477)
    log_loop (logging_deadlock.py:24)
    run (threading.py:953)
    _bootstrap_inner (threading.py:1016)
    _bootstrap (threading.py:973)

Thread 0x30C848000 (idle): "config_loop"
    acquire (logging/__init__.py:917)  # asking for the handler lock
    shutdown (logging/__init__.py:2181)
    _clearExistingHandlers (logging/config.py:275)
    configure (logging/config.py:544)  # acquired the module lock
    dictConfig (logging/config.py:810)
    config_once (logging_deadlock.py:35)
    config_loop (logging_deadlock.py:31)
    run (threading.py:953)
    _bootstrap_inner (threading.py:1016)
    _bootstrap (threading.py:973)

Metadata

Metadata

Assignees

Labels

pendingThe issue will be closed if no feedback is providedstdlibStandard Library Python modules in the Lib/ directory

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions