Skip to content

Debounce and Cooldown admission control dependencies#355

Merged
chrisguidry merged 7 commits intomainfrom
debounce-cooldown
Feb 27, 2026
Merged

Debounce and Cooldown admission control dependencies#355
chrisguidry merged 7 commits intomainfrom
debounce-cooldown

Conversation

@chrisguidry
Copy link
Owner

@chrisguidry chrisguidry commented Feb 27, 2026

Summary

Reworked the Debounce and Cooldown semantics from #322 after realizing what we originally called "Debounce" was really a cooldown, and we were missing true debounce (wait for things to settle, then fire).

Cooldown (formerly Debounce) — execute first, drop duplicates within a window. Sets a Redis key on entry with TTL (SET NX PX). Blocked tasks are silently dropped.

Debounce (new) — wait for submissions to settle, then fire once. Uses a Lua script with two Redis keys (winner + last_seen). The first submission becomes the "winner" and gets rescheduled for the settle window. Subsequent submissions reset the timer and are dropped. Once the settle window passes with no new activity, the winner proceeds.

Also adds a retry_delay field to AdmissionBlocked so Debounce can tell the worker exactly how long to wait before rescheduling (the remaining settle time), rather than using the fixed default delay.

The old success-anchored Cooldown (check on entry, set on successful exit) is removed — you can get the same effect with Cooldown + Retry.

Closes #322, closes #161.

🤖 Generated with Claude Code

Two new time-based admission controls for tasks:

- **Debounce** (leading edge): "don't start this if one was recently started."
  Sets a Redis key with TTL on entry via `SET NX PX`. Good for deduplicating
  rapid-fire events like webhooks.

- **Cooldown** (trailing edge): "don't start this if one recently succeeded."
  Checks for a key on entry, but only sets it on successful exit. Failed tasks
  don't trigger cooldown, so they can be retried immediately.

Both work as default parameters (per-task) or via `Annotated` (per-parameter
value), same pattern as `ConcurrencyLimit`.

Also adds a `reschedule` flag to `AdmissionBlocked` so the worker knows whether
to requeue blocked tasks (like ConcurrencyLimit does) or silently drop them
(appropriate for debounce/cooldown where retrying would just hit the same
window).

Closes #322, closes #161.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@read-the-docs-community
Copy link

read-the-docs-community bot commented Feb 27, 2026

Documentation build overview

📚 docket | 🛠️ Build #31594668 | 📁 Comparing 15ae145 against latest (7180b79)


🔍 Preview build

Show files changed (3 files in total): 📝 3 modified | ➕ 0 added | ➖ 0 deleted
File Status
api-reference/index.html 📝 modified
getting-started/index.html 📝 modified
task-behaviors/index.html 📝 modified

… style

Adds docs for the new Debounce and Cooldown admission controls, and rewrites
the ConcurrencyLimit section to use the Annotated style as the primary approach
(with a backward-compat note for the old string-name style).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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: 1b08852150

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

scope = self.scope or docket.name
if self._argument_name is not None:
debounce_key = (
f"{scope}:debounce:{self._argument_name}:{self._argument_value}"

Choose a reason for hiding this comment

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

P2 Badge Use bound argument values for per-parameter debounce keys

When Debounce is used via Annotated, the key is built from _argument_value, but that value is captured before positional args are rebound to parameter names; in resolved_dependencies this comes from execution.kwargs.get(...), so calls like await docket.add(task)(1) and await docket.add(task)(2) both produce a ...:customer_id:None key and one task is incorrectly dropped. This breaks per-value debouncing for positional invocation and causes false positives in normal task usage.

Useful? React with 👍 / 👎.

def _cooldown_key(self, function_name: str) -> str:
scope = self.scope or current_docket.get().name
if self._argument_name is not None:
return f"{scope}:cooldown:{self._argument_name}:{self._argument_value}"

Choose a reason for hiding this comment

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

P2 Badge Use bound argument values for per-parameter cooldown keys

The per-parameter Cooldown key also depends on _argument_value, which is captured from pre-bound values and can be None for positional calls, so different positional inputs collapse onto the same Redis key (for example, task(1) and task(2) both map to ...:customer_id:None). That makes cooldown block unrelated argument values and silently drops valid executions.

Useful? React with 👍 / 👎.

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

codecov-commenter commented Feb 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (7180b79) to head (15ae145).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##              main      #355   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          102       106    +4     
  Lines         3046      3106   +60     
  Branches        26        26           
=========================================
+ Hits          3046      3106   +60     
Flag Coverage Δ
cli-python-3.10 100.00% <ø> (ø)
cli-python-3.11 100.00% <ø> (ø)
cli-python-3.12 100.00% <ø> (ø)
cli-python-3.13 100.00% <ø> (ø)
cli-python-3.14 100.00% <ø> (ø)
python-3.10 100.00% <100.00%> (ø)
python-3.11 98.00% <100.00%> (+<0.01%) ⬆️
python-3.12 100.00% <100.00%> (ø)
python-3.13 100.00% <100.00%> (ø)
python-3.14 100.00% <100.00%> (ø)
windows-python-3.10 100.00% <100.00%> (ø)
windows-python-3.11 97.91% <100.00%> (+0.04%) ⬆️
windows-python-3.12 100.00% <100.00%> (ø)
windows-python-3.13 100.00% <100.00%> (ø)
windows-python-3.14 100.00% <100.00%> (ø)

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% <ø> (ø)
src/docket/dependencies/_base.py 100.00% <ø> (ø)
src/docket/dependencies/_cooldown.py 100.00% <100.00%> (ø)
src/docket/dependencies/_debounce.py 100.00% <100.00%> (ø)
src/docket/dependencies/_resolution.py 100.00% <ø> (ø)
src/docket/worker.py 100.00% <100.00%> (ø)
tests/test_cooldown.py 100.00% <100.00%> (ø)
tests/test_debounce.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chrisguidry
Copy link
Owner Author

I'm realizing these aren't the right names for these behaviors, introducing an actual debounce here shortly...

@chrisguidry chrisguidry changed the title Add Debounce and Cooldown admission control dependencies Debounce and Cooldown admission control dependencies Feb 27, 2026
chrisguidry and others added 4 commits February 27, 2026 16:10
After reviewing what we shipped in 1b08852, we realized the names were backwards:
what we called "Debounce" (execute first, drop duplicates) is really a cooldown,
and we were missing true debounce (wait for things to settle, then fire once).

- **Cooldown** (formerly Debounce): same SET NX PX logic, just renamed with
  `cooldown:` key prefix
- **Debounce** (new): Lua script with winner + last_seen keys. First submission
  becomes the winner and bounces via reschedule until the settle window passes
  without new activity, then proceeds. Non-winners are immediately dropped.
- Removed the old success-anchored Cooldown (check on entry, set on exit)
- Added `retry_delay` to `AdmissionBlocked` so Debounce can tell the worker
  exactly how long to wait rather than using the fixed default

Closes #322, closes #161.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…iction

Drop CooldownBlocked and DebounceBlocked in favor of raising AdmissionBlocked
directly with reschedule/retry_delay as constructor arguments. Less ceremony,
same behavior.

Cooldown no longer enforces single=True — multiple per-parameter cooldowns on
the same task are independent and work fine. Debounce keeps single=True because
its reschedule mechanism means multiple debounces would loop forever.

Also trims implementation details (Redis key patterns, Lua scripts) from the
user-facing docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…trol

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The debounce Lua script uses two keys (winner + last_seen) that need to
land on the same cluster slot. Added a hash tag so Redis routes them
together. Also fixed the multiple-cooldowns test to not depend on
execution order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chrisguidry chrisguidry merged commit 58a9bf5 into main Feb 27, 2026
50 checks passed
@chrisguidry chrisguidry deleted the debounce-cooldown branch February 27, 2026 21:50
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.

Debounce dependency Rate-limiting tasks

2 participants