Skip to content

Conversation

@chrisguidry
Copy link
Owner

When a Perpetual task finishes, on_complete() calls docket.replace() to
schedule the next run, which writes the successor's generation/state to
the runs hash. Then _mark_as_terminal() unconditionally overwrites that
with state=completed, or (with execution_ttl=0) deletes the entire hash.
The successor becomes invisible.

The fix uses a Lua script in _mark_as_terminal that atomically checks
the generation counter before writing. If replace() already bumped the
generation, the in-hand execution is superseded and the write is skipped.
Progress cleanup and pub/sub notifications still fire unconditionally.

For non-Perpetual tasks nothing changes — no replace() means the
generation still matches and the write proceeds normally.

Also parametrizes test_perpetual_race.py over execution_ttl to catch
both the TTL=0 (hash deletion) and TTL>0 (state overwrite) variants,
and adds a pub/sub event test for Perpetual completion.

Relates to #331

🤖 Generated with Claude Code

chrisguidry and others added 2 commits February 11, 2026 15:20
When a Perpetual task finishes, on_complete() calls docket.replace() to
schedule the next run, which writes the successor's generation/state to
the runs hash. Then _mark_as_terminal() unconditionally overwrites that
with state=completed, or (with execution_ttl=0) deletes the entire hash.
The successor becomes invisible.

The fix uses a Lua script in _mark_as_terminal that atomically checks
the generation counter before writing. If replace() already bumped the
generation, the in-hand execution is superseded and the write is skipped.
Progress cleanup and pub/sub notifications still fire unconditionally.

For non-Perpetual tasks nothing changes — no replace() means the
generation still matches and the write proceeds normally.

Also parametrizes test_perpetual_race.py over execution_ttl to catch
both the TTL=0 (hash deletion) and TTL>0 (state overwrite) variants,
and adds a pub/sub event test for Perpetual completion.

Relates to #331

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Feb 11, 2026

📚 Documentation has been built for this PR!

You can download the documentation directly here:
https://github.com/chrisguidry/docket/actions/runs/21922192926/artifacts/5472353126

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 11f775586e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +678 to +679
if current and tonumber(current) > generation then
return 'SUPERSEDED'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve terminal metadata for superseded generations

Returning SUPERSEDED here skips writing terminal state/result fields to runs:{key}, but Execution.get_result() still relies on a terminal pub/sub event followed by sync() against that hash; for Perpetual (or any running task replaced mid-flight), sync() now sees the successor's non-terminal state and get_result() falls through as if unfinished, returning None (and dropping failures/results) for an execution that actually completed. This is a behavior regression introduced by the supersession early-return path.

Useful? React with 👍 / 👎.

@codecov-commenter
Copy link

codecov-commenter commented Feb 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.67%. Comparing base (0547095) to head (15217ec).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #335   +/-   ##
=======================================
  Coverage   98.67%   98.67%           
=======================================
  Files         103      103           
  Lines       10323    10375   +52     
  Branches      497      496    -1     
=======================================
+ Hits        10186    10238   +52     
  Misses        121      121           
  Partials       16       16           
Flag Coverage Δ
python-3.10 98.67% <100.00%> (+<0.01%) ⬆️
python-3.11 97.34% <98.36%> (+<0.01%) ⬆️
python-3.12 98.67% <100.00%> (+<0.01%) ⬆️
python-3.13 98.67% <100.00%> (+<0.01%) ⬆️
python-3.14 98.67% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/docket/execution.py 100.00% <100.00%> (ø)
tests/test_perpetual_race.py 100.00% <100.00%> (ø)
tests/test_perpetual_state.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

When execution_ttl=0, _mark_as_terminal DELs the runs hash after
completion. If a stale message (from a replaced task) tries to claim()
concurrently, the generation evidence is gone and the stale task runs.

With real Redis the network latency means both messages usually claim
before either completes, but the in-memory backend processes things
instantly so one task can fully complete (and DEL the hash) before the
other even starts.

The fix: in the claim Lua script, if a generation-tracked message
(gen > 0) finds no runs hash at all, it's stale — return SUPERSEDED.
A generation-tracked message should always have a corresponding runs
hash; if it's gone, a newer generation already completed and cleaned up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chrisguidry chrisguidry merged commit 0812c86 into main Feb 11, 2026
72 of 73 checks passed
@chrisguidry chrisguidry deleted the perpetual-race-ttl-zero branch February 11, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants