fix: hold cbc to a wall clock so a stuck solve cannot take the replica - #120
Draft
andig wants to merge 2 commits into
Draft
fix: hold cbc to a wall clock so a stuck solve cannot take the replica#120andig wants to merge 2 commits into
andig wants to merge 2 commits into
Conversation
CBC only tests -sec between branch and bound nodes, so a solve that grinds in presolve or the root LP runs past the 10 s limit unbounded. With one worker per replica that worker is the replica: gunicorn kills it at --timeout, the request and everything queued behind it on that replica dies, and the solver is orphaned. Production saw it eight times in the ten hours after the 26 Jul 11:00 UTC rollout, every one of them leaving a solver for the master to reap. The solve now runs under its own wall at twice the limit plus five seconds. PuLP raises when its solver dies, so the request fails in 25 s with the worker intact instead of wedging it for the full gunicorn timeout. The /proc scan the master already used to find orphans is the same scan a worker needs for its own solver, so it moves to optimizer.solvers and takes the parent pid as an argument. Its tests move with it. --timeout drops 60 -> 40 as the backstop for a request stuck outside the solve. It stays above the worst elapsed time seen in production, 30 s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…me knob The reason CBC needs a wall was written out in three files. It stays in optimizer.solvers, next to the code that acts on it; the hook module and the call site keep only what is theirs. solvers_of took a process name nobody ever passed, and the wall's guard read 'time_limit and 2 * time_limit + 5', which returns None by way of a falsy number. Both are now what they look like. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andig
marked this pull request as draft
July 27, 2026 06:38
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.
What happens today
OPTIMIZER_TIME_LIMITis 10 s, but CBC only tests-secbetween branch and bound nodes. Presolve and the root LP are not interrupted, so a solve that grinds there runs unbounded.With
--workers 1, that worker is the replica. gunicorn kills it at--timeout 60, the in-flight request dies, everything queued behind it on that replica dies with it, and the solver is orphaned onto the master.Eight times in the ten hours after the 26 Jul 11:00 UTC rollout of
optimizer--0000029, on four different replicas:Every one of them left a solver behind, which is what puts the stall inside the solve rather than in Python.
The core leak this used to cause is already gone — #117's reaper fires in the same second, 7 of the 8 events show the
reaping orphaned solverline. What is left is the replica: up to 60 s of a replica serving nothing, plus its queue.What this changes
2 × time_limit + 5— 25 s in production. PuLP raises when its solver dies (cbc.wait() != 0), andapp.pyalready turns that into a 500, so the request fails in 25 s with the worker intact./procscan the master used to find orphans is the same scan a worker needs for its own solver, so it moves tooptimizer.solversand takes the parent pid as an argument.solvers_of(1)is the master's reaper,solvers_of(os.getpid())is the worker's wall. Tests move with it.--timeout60 → 40, the backstop for a request stuck outside the solve. Still above the worst elapsed time seen in production, 30 s.Not changed:
-timeMode elapsedwas the first thing I reached for, but PuLP 3.3 already passes it whenevertimeLimitis set.-secis wall clock here — it is simply not checked often enough.Verification
27 tests pass, ruff clean,
az bicep buildclean. The new tests drivesolver_wallagainst a fake/proc: a solve past the wall loses its own solver and not somebody else's, a solve inside the wall keeps it, and no configured limit means no wall. The scan itself is a no-op off procfs, so the wall does nothing on macOS — the same scan has been running in production for the reaper since #117.Untested outside CI: that a real cbc dies at the wall under load. Worth watching
WORKER TIMEOUTafter the rollout — the count should go to zero and be replaced by 500s at ~25 s.🤖 Generated with Claude Code