Debounce and Cooldown admission control dependencies#355
Conversation
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>
Documentation build overview
Show files changed (3 files in total): 📝 3 modified | ➕ 0 added | ➖ 0 deleted
|
… 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>
There was a problem hiding this comment.
💡 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".
src/docket/dependencies/_debounce.py
Outdated
| scope = self.scope or docket.name | ||
| if self._argument_name is not None: | ||
| debounce_key = ( | ||
| f"{scope}:debounce:{self._argument_name}:{self._argument_value}" |
There was a problem hiding this comment.
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 👍 / 👎.
src/docket/dependencies/_cooldown.py
Outdated
| 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}" |
There was a problem hiding this comment.
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 Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #355 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 102 106 +4
Lines 3046 3106 +60
Branches 26 26
=========================================
+ Hits 3046 3106 +60
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
I'm realizing these aren't the right names for these behaviors, introducing an actual debounce here shortly... |
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>
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_delayfield toAdmissionBlockedso 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