fix: Add global semaphore to concurrency sync context managers to prevent DB timeouts (closes #16299) - #22972
Conversation
…vent DB timeouts (closes PrefectHQ#16299)
| import threading | ||
|
|
||
| # Global semaphore to limit concurrent concurrency slot operations, preventing DB timeouts under high load | ||
| _slot_operation_semaphore = threading.Semaphore(50) |
There was a problem hiding this comment.
🟡 Concurrency cap never enforced
The _slot_operation_semaphore is created but never acquired or released; concurrency and rate_limit call the internal functions without it. The intended cap on concurrent slot operations has no effect, so the DB timeouts under high load are not mitigated.
Prompt for agents
The module-level _slot_operation_semaphore in src/prefect/concurrency/sync.py is defined but never used. The PR's goal is to cap concurrent concurrency slot operations, but the semaphore is never acquired or released. Wrap the calls to the internal _concurrency_internal (in the concurrency context manager) and _acquire_concurrency_slots (in rate_limit) so the semaphore is acquired before invoking them and released afterward (e.g. using `with _slot_operation_semaphore:`), ensuring release even on exception. Note that for the concurrency context manager, holding the semaphore only for the acquire phase versus the entire lifetime of the with-block has very different semantics — decide intentionally and consider that holding it for the whole block could deadlock nested concurrency usages given a fixed limit of 50.
Was this helpful? React with 👍 or 👎 to provide feedback.
| import threading | ||
|
|
||
| # Global semaphore to limit concurrent concurrency slot operations, preventing DB timeouts under high load | ||
| _slot_operation_semaphore = threading.Semaphore(50) |
There was a problem hiding this comment.
🔍 Fix would miss the async path
The concurrency AGENTS.md requires sync.py and asyncio.py to stay in lockstep. A semaphore added only in the sync wrapper leaves async callers unprotected, and the module docs suggest the real fix belongs in the internal _sync/_asyncio layer or connection pool sizing.
Was this helpful? React with 👍 or 👎 to provide feedback.
Merging this PR will not alter performance
Comparing Footnotes
|
What
Under high load, the concurrency slot acquisition and lease renewal operations can overwhelm the database connections, leading to timeouts and OOMs. This is observed as services overrunning their loop intervals.
Fix
Introduce a global semaphore (default limit 50) in
src/prefect/concurrency/sync.pyto cap the number of concurrent concurrency slot operations. The semaphore should be acquired before calling the internal_syncfunctions and released after. Since the file provided only contains wrappers, the semaphore is added here as a minimal safety measure. For a more robust solution, consider moving this semaphore into the_syncmodule or configuring connection pool sizes appropriately.Closes #16299