Summary
recost/_aggregator.py:127-132's flush() iterates self._buckets.values() while user threads concurrently call ingest() (via interceptor wrappers, which run on whatever thread issued the HTTP request). The flush is driven by a background threading.Timer. Concurrent dict mutation during iteration raises RuntimeError: dictionary changed size during iteration.
This is a real race under any non-trivial load. There is no lock anywhere in Aggregator.
Fix
Wrap ingest, flush, and would_overflow with a threading.RLock. Add a regression test that pumps N worker threads into ingest while the timer thread calls flush.
Files
recost/_aggregator.pytests/test_aggregator.py
Priority
P0 — corrupts telemetry / crashes the timer thread under load.
Summary
recost/_aggregator.py:127-132'sflush()iteratesself._buckets.values()while user threads concurrently callingest()(via interceptor wrappers, which run on whatever thread issued the HTTP request). The flush is driven by a backgroundthreading.Timer. Concurrent dict mutation during iteration raisesRuntimeError: dictionary changed size during iteration.This is a real race under any non-trivial load. There is no lock anywhere in
Aggregator.Fix
Wrap
ingest,flush, andwould_overflowwith athreading.RLock. Add a regression test that pumps N worker threads intoingestwhile the timer thread callsflush.Files
recost/_aggregator.pytests/test_aggregator.pyPriority
P0 — corrupts telemetry / crashes the timer thread under load.