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()
LRUDict is not thread-safe on free threading.