Repository navigation
Conversation
ryuwd
force-pushed
the
roneil-throttle-recovery
branch
from
February 26, 2026 12:40
06a5f45 to
6c44a2e
Compare
aldbr
requested review from
atsareg and
chaen
and removed request for
atsareg
February 26, 2026 12:54
aldbr
marked this pull request as ready for review
March 10, 2026 11:49
Contributor
|
Was this PR hotfixed somewhere, or at least tested in the certification setup? |
Contributor
Author
|
It's not been hotfixed or tested in certification yet |
Contributor
|
hi @ryuwd is this PR still relevant? |
Contributor
Author
|
I don't believe any of the bugs that I aimed to fix here have been addressed, so I think it's still relevant, but it needs the input of e.g. @chaen |
… growth The previous throttle mechanism accepted one connection every 0.25s even while the service was overloaded. When threads were stuck (e.g. blocked on DB queries or deadlocked), each accepted connection added to the already-full queue, making recovery impossible. Now all incoming connections are rejected while wantsThrottle is True, with a brief sleep to avoid busy-spinning. This prevents the self-reinforcing stuck state where the queue grows faster than it can drain.
Track when throttling starts and log state transitions with queue/thread diagnostics. Periodic warnings are emitted every 30s while throttling persists, making it easier to diagnose stuck services from logs without needing to attach a debugger. Adds Service.throttleDiagnostics() to expose queue size, max queue, active threads, and max threads for logging purposes.
…ation When a service is stuck in throttle mode (all threads blocked, queue full), it cannot recover without external intervention. Add a configurable MaxThrottleDuration CS option (default: 0 = disabled) that triggers a process exit after the specified number of seconds of continuous throttling. The process supervisor (e.g. runsv) then restarts the service cleanly, clearing all stuck state. When enabled, a FATAL log message is emitted before exit with full queue/thread diagnostics for post-mortem analysis.
…very The 0.25s sleep between throttle rejections was a leftover from the old "accept 1 every 0.25s" mechanism. With the new reject-all approach, sel.select(timeout=10) already rate-limits when no connections arrive. Removing the sleep lets the service reject incoming connections immediately (faster client failover) and re-check wantsThrottle sooner (faster recovery when queue drains).
Throttle state (start time, last log time) was local to __acceptIncomingConnection, which resets on every re-entry from serve() (e.g. after a 10s select timeout). This meant MaxThrottleDuration could never accumulate past 10s if connections arrived slowly. Move tracking into Service instance state alongside wantsThrottle, which now handles entry/exit logging and periodic diagnostics. Add throttleDuration property for the reactor to check against MaxThrottleDuration. The ServiceReactor throttle block is simplified to just rejection + max duration check.
ryuwd
force-pushed
the
roneil-throttle-recovery
branch
from
October 15, 2026 10:31
1a435ec to
2f8205f
Compare
- Exit with os._exit after flushing logs when MaxThrottleDuration is exceeded. A normal exit hangs joining the stuck ThreadPoolExecutor workers, so the supervisor never restarted the service. - In clone processes, signal the parent so the whole service restarts instead of silently losing the clone. - Only clear throttling once the queue drains to half of MaxWaitingPetitions, to avoid flapping and log spam. - Rename the side-effecting wantsThrottle property to updateThrottleState(). - Read MaxThrottleDuration once at service start-up instead of on every rejected connection. - Don't mutate the process list while iterating in stopAllProcess. - Document MaxThrottleDuration and add unit tests for the throttle state.
ryuwd
force-pushed
the
roneil-throttle-recovery
branch
from
October 15, 2026 10:32
2f8205f to
579e16b
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Production DISET services can enter throttle mode and never recover, requiring manual restarts. This PR stops the queue growing while a service is overloaded, adds diagnostics, and adds an opt-in auto-restart for services that stay stuck.
MaxWaitingPetitions, connections were rejected for a 0.25s window and then one was accepted, so roughly 4 req/s still got through. When threads were stuck (blocked DB queries, deadlocks, etc.) the queue kept growing, and by the time queued requests were processed their clients had long since timed out. Now all connections are rejected while throttled, which bounds the queue. This does not unstick stuck threads; that is what the auto-restart is for.MaxWaitingPetitionsand only clears once it has drained to half of that. This avoids flapping in and out of throttle mode (and the log spam that comes with it) when rejected clients immediately reconnect.Serviceinstance, so it persists across__acceptIncomingConnectionre-entries. Logs WARN on entry with queue/thread stats, WARN every 30s while it persists, and INFO on clear with the total duration.MaxThrottleDurationCS option (default: 0 = disabled). When exceeded, the service logs a FATAL message, flushes its log handlers and exits withos._exit(1)so the process supervisor (runsv) restarts it. A normal interpreter exit cannot be used: it joins theThreadPoolExecutorworkers, which hangs forever when they are stuck. In a clone process (CloneProcesses > 1), the clone signals the parent so the whole service restarts rather than silently losing a clone.Operational Impact
Behavioural change
Previously, a throttled service would still accept ~4 connections/s. Now it rejects all connections until the queue has drained to half of
MaxWaitingPetitions. This means:New logging
queue=X/Y, threads=X/YdiagnosticsAuto-restart
MaxThrottleDuration = 0)Systems/<System>/Services/<Service>/MaxThrottleDuration = 600Recommended deployment
MaxThrottleDurationfor services known to get stuck (suggested starting value: 600s)Test plan
Test_ServiceThrottle.py)MaxThrottleDurationset, check the service is restarted by runsv (single process and withCloneProcesses > 1)BEGINRELEASENOTES
*Core
FIX: prevent DISET services from getting permanently stuck in throttle mode by rejecting all connections while overloaded instead of accepting one every 0.25s
NEW: add throttle duration tracking with diagnostic logging (queue/thread stats on entry, periodic warnings, clear notification)
NEW: add configurable MaxThrottleDuration CS option to auto-restart services stuck in throttle mode (default: disabled)
ENDRELEASENOTES