Skip to content

concurrency: fix meter, executor, work-queue and transport races - #23

Merged
adnanhd merged 1 commit into
mainfrom
fix/concurrency
Aug 1, 2026
Merged

concurrency: fix meter, executor, work-queue and transport races#23
adnanhd merged 1 commit into
mainfrom
fix/concurrency

Conversation

@adnanhd

@adnanhd adnanhd commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Fixes the thread-safety defects found in a concurrency audit. The library ships concurrent execution modes (ConcurrentDispatcher, Executor THREAD/PROCESS, per-message consumer threads), so these are hit under real use.

  • Meter aggregation lost updates (observers.py). update/reset/value/stats read-modify-wrote sum/count/max/min/last with no lock -> concurrent task completions dropped updates silently. Added _state_lock; dispatch stays outside it.
  • MemoryTransport silently dropped messages when full (transports/memory.py). put_nowait + bare except: pass meant messages vanished from receive() under load with no signal. Now raises a new TransportFullError (mirrors QueueFullError) after notifying live subscribers; no silent drop.
  • Executor.start() double-construction race (executor.py). Unlocked check-then-act let two concurrent submit()s each build a pool and leak one. The running-flag check + pool construction is now one atomic block under the lock.
  • WorkQueue.remove_consumer dropped the wrong handler (work_queue.py). Consumer groups were a bare handler list, so pop() removed the last-registered handler, not the one for the given consumer_id. Groups now carry (consumer_id, handler).
  • Reaper-requeue starved push-only consumers (work_queue.py). A visibility-timeout requeue only notified the pull-side condition; now it also calls _try_dispatch so push consumers get the redelivery.
  • GIL-reliant globals (_CLASS_SUBSCRIBERS, RPCServer._methods) now have explicit locks.

New tests/test_concurrency.py (6 tests). The straight-line races (Meter, Executor) barely interleave under CPython 3.14's specializing interpreter, so those tests inject a small delay to widen the check-then-act window; each fails on the pre-fix code with the exact symptom and passes after. Full suite 188 passed, mypy strict + ruff clean.

Not included: a callback-group / mutually-exclusive-subscriber abstraction (so a shared-state handler can declare it must not run concurrently) -- that's a Dispatcher public-contract change and belongs as a separate designed feature.

- Meter.update/reset guard sum/count/max/min/last with a lock, fixing
  lost updates under concurrent task execution
- MemoryTransport.send raises TransportFullError instead of silently
  dropping messages when the bounded queue is full
- Executor.start()/submit() make the running-flag check and pool
  construction atomic, preventing double pool construction
- WorkQueue.remove_consumer keys handlers by consumer_id instead of
  popping the last-registered one
- reaper-driven requeue now calls _try_dispatch so push-only consumers
  aren't starved after a visibility timeout
- add locks around the _CLASS_SUBSCRIBERS registry and RPCServer._methods

Adds tests/test_concurrency.py: producer/consumer exactly-once and
failure-handling stress tests, remove_consumer correctness, reaper
starvation regression, meter race, and executor pool race.
@adnanhd
adnanhd merged commit 72c879a into main Aug 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant