Skip to content

Add FailureHandler and CompletionHandler base classes for dependency hooks#299

Merged
chrisguidry merged 10 commits intomainfrom
dependency-hooks
Jan 25, 2026
Merged

Add FailureHandler and CompletionHandler base classes for dependency hooks#299
chrisguidry merged 10 commits intomainfrom
dependency-hooks

Conversation

@chrisguidry
Copy link
Owner

@chrisguidry chrisguidry commented Jan 25, 2026

Summary

Introduces two new abstract base classes that let dependencies control post-execution flow:

  • FailureHandler - controls what happens when a task fails. Retry now inherits from this and implements handle_failure() to schedule retries.

  • CompletionHandler - controls what happens after task completion. Perpetual now inherits from this and implements on_complete() to schedule the next execution.

This follows the pattern established by Runtime for Timeout: the Worker delegates control to specialized dependencies rather than knowing the details itself. The Worker's _retry_if_requested() and _perpetuate_if_requested() methods are removed (~50 lines), with that logic now living in the dependencies.

The dependency hierarchy now looks like:

Dependency (base - can observe via __aexit__)
├── Runtime (controls HOW task executes - Timeout)
├── FailureHandler (controls WHAT HAPPENS on failure - Retry)
└── CompletionHandler (controls WHAT HAPPENS after completion - Perpetual)

All three have single = True because only one thing can control each aspect.

Also adds after(timedelta) and at(datetime) methods to both Perpetual and Retry, giving developers flexibility to schedule the next execution using whatever they have handy. For Retry, the old in_() method is kept as a backwards-compatible alias.

Closes #297

🤖 Generated with Claude Code

…hooks

Introduces two new abstract base classes that let dependencies control
post-execution flow:

- `FailureHandler` - controls what happens when a task fails. `Retry` now
  inherits from this and implements `handle_failure()` to schedule retries.

- `CompletionHandler` - controls what happens after task completion.
  `Perpetual` now inherits from this and implements `on_complete()` to
  schedule the next execution.

This follows the pattern established by `Runtime` for `Timeout`: the Worker
delegates control to specialized dependencies rather than knowing the details
itself. The Worker's `_retry_if_requested()` and `_perpetuate_if_requested()`
methods are removed (~50 lines), with that logic now living in the dependencies.

Also consolidates the duplicate `ms()`/`_ms()` duration formatting functions
into a single `format_duration()` in `_base.py`, which fixed the remaining
coverage gap.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@github-actions
Copy link

github-actions bot commented Jan 25, 2026

📚 Documentation has been built for this PR!

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

@codecov-commenter
Copy link

codecov-commenter commented Jan 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.64%. Comparing base (c8b09e7) to head (3a2db00).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             main     #299    +/-   ##
========================================
  Coverage   99.63%   99.64%            
========================================
  Files          96       98     +2     
  Lines        9608     9766   +158     
  Branches      463      468     +5     
========================================
+ Hits         9573     9731   +158     
  Misses         20       20            
  Partials       15       15            
Flag Coverage Δ
python-3.10 99.64% <100.00%> (-0.36%) ⬇️
python-3.11 98.35% <100.00%> (+0.11%) ⬆️
python-3.12 99.64% <100.00%> (+<0.01%) ⬆️
python-3.13 99.64% <100.00%> (+<0.01%) ⬆️
python-3.14 99.63% <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/dependencies/__init__.py 100.00% <100.00%> (ø)
src/docket/dependencies/_base.py 100.00% <100.00%> (ø)
src/docket/dependencies/_perpetual.py 100.00% <100.00%> (ø)
src/docket/dependencies/_retry.py 100.00% <100.00%> (ø)
src/docket/execution.py 100.00% <100.00%> (ø)
src/docket/worker.py 100.00% <100.00%> (ø)
tests/fundamentals/test_perpetual.py 100.00% <100.00%> (ø)
tests/test_cancellation.py 100.00% <100.00%> (ø)
tests/test_dependencies_core.py 100.00% <100.00%> (ø)
tests/test_dependency_uniqueness.py 100.00% <100.00%> (ø)
... and 2 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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: 3faac76684

ℹ️ 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".

chrisguidry and others added 9 commits January 25, 2026 13:27
Refactors FailureHandler.handle_failure() and CompletionHandler.on_complete()
to accept (execution, outcome) instead of multiple parameters. TaskOutcome
bundles duration, result, and exception into a clean dataclass.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Gives developers flexibility to schedule the next execution using whatever
they have handy - a timedelta ("run again in 5 minutes") or an absolute
datetime ("run at midnight").

For Retry, keeps in_() as a backwards-compatible alias for after().

Closes #297

Co-Authored-By: Claude Opus 4.5 <[email protected]>
When a FailureHandler returns True (e.g., Retry schedules a retry),
we should not call mark_as_failed() since that would publish a FAILED
state that callers using get_result() could observe.

Also fixes stricken tasks (e.g., from run_at_most) to be properly
marked as cancelled so their keys get TTL.

Adds tests verifying:
- Retrying tasks have SCHEDULED state, not FAILED
- Exhausted retries properly mark tasks as FAILED

Thanks to Codex for catching this semantic issue.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Now that stricken tasks are properly marked as CANCELLED, get_result()
needs to handle this state. It now raises asyncio.CancelledError when
the task was cancelled.

Adds test for get_result() on cancelled tasks.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Verifies that a Perpetual task that fails still gets rescheduled for
its next execution. After running 3 times (all failures), the state
correctly shows FAILED from the last execution.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Verifies that a task can have both Retry and Perpetual dependencies.
On failure, Retry handles it first (if attempts remain). When retries
are exhausted, Perpetual reschedules the next run with attempt reset.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Verifies that when a task calls perpetual.after() to set a custom
delay and then fails, the next perpetual run still uses that delay.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add ExecutionCancelled exception for get_result() on cancelled tasks,
  avoiding conflation with asyncio.CancelledError semantics
- Split test_dependencies_core.py into focused test files:
  - test_dependency_uniqueness.py (single=True constraint tests)
  - test_handler_semantics.py (FailureHandler/CompletionHandler behavior)
  - test_dependencies_core.py (core dependency injection tests)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@chrisguidry chrisguidry merged commit 0062982 into main Jan 25, 2026
40 checks passed
@chrisguidry chrisguidry deleted the dependency-hooks branch January 25, 2026 20:51
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.

Perpetual should really have an easy shortcut for rescheduling the next run

2 participants