Repository navigation
Propagate deadlines in the background, without waiting for locks - #24
Merged
Merged
Conversation
Adding a precondition moved the deadline of it and of all its preconditions, recursively, in the same transaction. That updated goals along existing dependencies, from dependent to precondition, while a worker finishing a goal holds it and updates its dependents' counters, the other way. Both directions on the same pair of goals deadlocked: - a worker returning RetryMeLater([B]), where B depends on C, while C was being achieved; Postgres aborted one of them, so its work was rolled back (or C's handler ran again), - schedule(precondition_goals=[B]) from outside a worker, the same way. Even without a deadlock, schedule() waited for any goal being pursued down the dependency graph, for as long as its handler ran. Now _add_precondition_goals only marks its new dependencies on preconditions with a later deadline (deadline_propagated=False). handle_deadline_propagation, run by busy and threaded workers, moves the deadline over marked dependencies, one level per call. It locks goals with SKIP LOCKED, so it never waits for one being pursued: a dependency on a locked goal stays marked for a later call. A precondition whose deadline moved gets marked itself, and the next call marks its late dependencies. Marking them at once could deadlock with another call holding them. Each piece of work is recorded once and found by a partial index, so no list of preconditions is scanned more than once per deadline change. - remove update_goals_deadline - existing goals and dependencies start as propagated Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017FZQDBWAKp5JbsKYHbMfox
…ists Drop the pypi.yml chore, fixed by "Run the publish workflow only on main and tags". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017FZQDBWAKp5JbsKYHbMfox
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.
Problem
Adding a precondition moved deadlines of it and all its preconditions, recursively, in the same transaction. That locked goals along existing dependencies from dependent to precondition, while a worker finishing a goal locks it and updates its dependents' counters, the other way. Reproduced deadlocks:
RetryMeLater(precondition_goals=[B]),Bdepends onC,Cis being achieved. Postgres aborts one of them: either the handler's work is rolled back, orC's finished handler is rolled back and runs again.schedule(precondition_goals=[B])from outside a worker (e.g. a web request), the same way.Even without a deadlock,
schedule()waited for any goal being pursued down the dependency graph, for as long as its handler ran.Change
_add_precondition_goalsonly marks its new dependencies whose precondition has a later deadline (GoalDependency.deadline_propagated=False).handle_deadline_propagation(), run by busy and threaded workers, moves deadlines over marked dependencies, one level per call. It uses row-level locks withSKIP LOCKEDonly, so it never waits for a goal being pursued: a dependency on a locked goal stays marked for a later call.Goal.deadline_propagated=False, and the next call marks its late dependencies. Marking them in the same transaction could deadlock with another call holding them; the goal row is already locked, so the flag costs no waiting. An edge-only variant deadlocked 59 times in a 40 s stress run, this one 0.EXPLAINon 200k goals: adding a precondition to a goal with 50k preconditions touches one row.update_goals_deadlineis removed._add_precondition_goals(still there, separate change) and the duplicated handler lists; drop thepypi.ymlchore fixed on main.Behavior change: deadlines reach preconditions in the background, one level per worker turn, not inside
schedule().Testing
pytest(94),mypy,flake8on Django 6.1 and 5.2;makemigrations --check.lock_timeoutset: on main it fails withLockNotAvailable, here it passes.schedule()calls and deadline drops. main: 29 deadlocks; this branch: 0, and after settling no unachieved precondition has a later deadline than its dependent.🤖 Generated with Claude Code
https://claude.ai/code/session_017FZQDBWAKp5JbsKYHbMfox
Generated by Claude Code