Skip to content

LRUDict is not thread-safe on free threading #48

Description

@ddorian

LRUDict is not thread-safe on free threading.

❯ uv run --python 3.14.6t repro_device_detector_freethreading.py
caught_errors={'RuntimeError': 1005154}
  e.g. RuntimeError: OrderedDict mutated during iteration
#!/usr/bin/env python3
# /// script
# requires-python = "==3.14.6"
# dependencies = [
#     "device_detector @ git+https://github.com/thinkwelltwd/device_detector@244d935c380b5317d198c08078670cfb305ff916",
# ]
# ///
import sys
import threading
import time
from collections import Counter

from device_detector.settings import LRUDict

MAXKEYS = 10_000_000  # huge -> purge() never pops, so the *catchable* exception wins
# MAXKEYS = 1024       # device_detector's real default -> popitem() races -> SIGSEGV

d: LRUDict = LRUDict(maxkeys=MAXKEYS)
errors: Counter[str] = Counter()
sample: dict[str, str] = {}
lock = threading.Lock()
stop = threading.Event()


def record(exc: BaseException) -> None:
    with lock:
        errors[type(exc).__name__] += 1
        sample.setdefault(type(exc).__name__, f"{type(exc).__name__}: {exc}")


def writer(tid: int) -> None:
    i = 0
    while not stop.is_set():
        try:
            d[(tid, i)] = i  # __setitem__ -> insert (+ purge): changes the dict
        except Exception as exc:
            record(exc)
        i += 1


def reader() -> None:
    while not stop.is_set():
        try:
            for _k in d:  # iterate while other threads mutate it
                pass
        except Exception as exc:
            record(exc)


def main() -> None:
    assert not sys._is_gil_enabled(), "run on a free-threaded build (GIL must be off)"
    threads = [threading.Thread(target=writer, args=(t,), daemon=True) for t in range(8)]
    threads += [threading.Thread(target=reader, daemon=True) for _ in range(8)]
    for t in threads:
        t.start()
    time.sleep(8)
    stop.set()
    print(f"caught_errors={dict(errors)}")
    for msg in sample.values():
        print(f"  e.g. {msg}")


if __name__ == "__main__":
    main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions