From f1923b2dcc82313f89bad8a694c05cf1d1e5dae9 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Mon, 4 May 2026 08:37:03 +0300 Subject: [PATCH] Document leased-timer visibility and cancel_all race in basic.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two facts about the inspection methods are only captured in `broker.py` docstrings: - Leased (in-flight) timers have their score pushed forward by `lease_ttl`, so they still appear in `has_pending` and the default `get_pending_timers(before=None)`. A user polling `has_pending` to detect "fired and finished" will get false positives during the lease window — recommend `before=datetime.now(tz=UTC)`. - `cancel_all(topic)` running mid-handler lets the handler finish; its final commit becomes a no-op because the keys are already gone. Side effects are not rolled back. Pull both into the "Inspecting pending timers" section of basic.md. --- docs/usage/basic.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/usage/basic.md b/docs/usage/basic.md index c5c8193..2f649d5 100644 --- a/docs/usage/basic.md +++ b/docs/usage/basic.md @@ -152,6 +152,20 @@ removed = await broker.cancel_all("invoices") These methods only inspect/cancel timers in the queue — handlers that have already started running are unaffected. +### Leased timers still appear as pending + +Timers that have been claimed by a worker and are currently being processed have their score pushed forward by `lease_ttl` seconds (the visibility-timeout pattern — see [How it works](../introduction/how-it-works.md)). They are *still* in the sorted set, so: + +- `has_pending(topic, timer_id)` returns `True` while the handler is running. +- `get_pending_timers(topic)` (no `before`) includes them. +- `get_pending_timers(topic, before=datetime.now(tz=UTC))` excludes them — they are no longer due "by now" because their score is in the future. + +If you are polling `has_pending` to detect *"the timer fired and the handler finished"*, you will get false positives during the lease window. Pass `before=datetime.now(tz=UTC)` (or treat the timer as gone only after `has_pending` is `False`) to avoid this. + +### `cancel_all` race with executing handlers + +If `cancel_all(topic)` runs while a worker is mid-handler for a leased timer on that topic, the handler runs to completion. When it finishes, its commit (the `ZREM` + `HDEL` that normally removes the timer) becomes a no-op because `cancel_all` has already deleted both keys for the topic. The work is *not* rolled back — only the bookkeeping is skipped — so handlers that have side effects (sent emails, written rows) will have already done them. Use `cancel_all` for queue resets, not for "stop everything in flight." + ## Debug logging Set `log_level=logging.DEBUG` on `TimersBroker` to emit per-timer DEBUG lines: timers fetched per poll cycle, claim contested by another worker, and timer delivered to handler. Useful for diagnosing "my timer didn't fire".