sema acquire/release inside an async request handler: how to prove a missing release is what blocks drain() at shutdown
0 reputation · 04 Feb 2024, 03:22 UTC
0 reputation · 04 Feb 2024, 03:22 UTC
I'm using the sema JavaScript semaphore library (async acquire/release primitives) to cap concurrency inside a Node.js request handler, constructed with a fixed maximum. Each request calls acquire() before doing work and release() afterward, and at shutdown I call drain() to quiesce in-flight work before closing the server.
The boundary I'm unsure about is between the semaphore and the async/await error path: if a handler throws between acquire() and release() without a try/finally, my understanding is that the slot is never returned, nrWaiting() keeps growing, and drain() never resolves. I want to confirm that this is the documented behavior rather than something the library guards against internally, and to distinguish a genuinely leaked acquisition from a simply long-running one during diagnosis.
Assume a recent version of the package; exact signatures of tryAcquire() and drain() may differ across versions, so answers should note version assumptions.
Specifically:
drain() a reliable signal that at least one acquisition was never released, or are there other states that produce the same symptom?sema expose any supported way to inspect or time out individual waiters, or is nrWaiting() the only diagnostic surface?release() more times than acquire() defined behavior, or can it silently over-increment capacity?32775 reputation · 04 Feb 2024, 15:14 UTC
In the current sema releases (≥ v1.4), drain() resolves only when the semaphore’s internal queue of pending acquire promises is empty. If a handler acquires a slot and never releases it (because the code throws before release() or the try/finally is omitted), that slot stays “checked out” forever. The queue never empties, so drain() stays pending. A long‑running request that *does* eventually call release() will also block drain() until the release occurs, but the library treats both situations identically.
drain() guarantees that at least one acquire promise is still queued or an active acquisition hasn’t finished.release() in a try/finally block. Another possibility is that the request is legitimately taking a long time before it releases.drain() pending.sema expose?nrWaiting(), which reports the number of queued acquire promises. It does not expose per‑waiter timeouts or cancellation hooks.acquire() in a try/finally and log nrWaiting() before and after the critical section.release() is called more times than acquire()?release() more times than acquire() is defined behaviour but has no effect other than potentially logging a warning in debug mode.async function handler(req, res) {
const sem = await semaphore.acquire();
try {
// … work …
} finally {
semaphore.release();
}
}
drain() at shutdown, log semaphore.nrWaiting() and the number of active acquisitions (if you expose it).drain() still hangs, check whether any request is still in flight – e.g. by maintaining a counter of active handlers or by inspecting your application logs.To narrow down whether the hang is due to a leak or a long‑running request, could you confirm whether any requests are still actively executing when drain() is invoked?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.