You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b2ab4de
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: README.md
+11-11Lines changed: 11 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -24,7 +24,7 @@ Schedule messages to be delivered to subscribers at a future point in time, with
24
24
25
25
> **Already using Postgres?** Prefer [`faststream-outbox`](https://github.com/modern-python/faststream-outbox), which also supports
26
26
> [timers](https://faststream-outbox.modern-python.org/usage/timers/) and lets you schedule a timer in the **same SQLAlchemy
27
-
> transaction** as the domain write — no dual-write between your DB and Redis. Reach for `faststream-redis-timers` when your stack
27
+
> transaction** as the domain write, which avoids a dual-write between your DB and Redis. Reach for `faststream-redis-timers` when your stack
28
28
> doesn't include Postgres, or when you want timer scheduling decoupled from your relational store.
29
29
30
30
```python
@@ -72,10 +72,10 @@ Timers are stored in Redis as two keys **per topic**:
72
72
- A **sorted set** (`timers_timeline:{topic}`) with the activation timestamp as score
73
73
- A **hash** (`timers_payloads:{topic}`) with the serialized message body
74
74
75
-
So a timer on the `invoices` topic lives under `timers_timeline:invoices` and `timers_payloads:invoices` — handy when poking around in `redis-cli`. A `TimersRouter(prefix="my-service:")` extends the suffix to `my-service:invoices`.
75
+
So a timer on the `invoices` topic lives under `timers_timeline:invoices` and `timers_payloads:invoices`; those are the keys to look up in `redis-cli`. A `TimersRouter(prefix="my-service:")` extends the suffix to `my-service:invoices`.
76
76
77
77
A polling loop checks for due timers and atomically claims each one via a Lua
78
-
script that pushes its score forward by `lease_ttl` seconds — granting the
78
+
script that pushes its score forward by `lease_ttl` seconds, which grants the
79
79
worker a lease. The timer is removed from Redis only **after** the handler
80
80
completes successfully. If the worker crashes mid-handler or the handler
81
81
raises, the lease eventually expires and another worker re-claims the timer.
The envelope is FastStream's `BinaryMessageFormatV1` — a binary-safe wire format that carries the message body alongside `correlation_id`, `content-type`, `reply_to`, and any user-supplied headers. The same format is used by FastStream's built-in Redis broker.
19
+
The envelope is FastStream's `BinaryMessageFormatV1`, a binary-safe wire format that carries the message body alongside `correlation_id`, `content-type`, `reply_to`, and any user-supplied headers. The same format is used by FastStream's built-in Redis broker.
20
20
21
21
## Polling loop
22
22
23
23
Each subscriber runs a background polling loop that:
24
24
25
25
1. Calls `ZRANGEBYSCORE timers_timeline:{topic} -inf <now> LIMIT 0 <free>` (where `<free>` is the unused capacity from the per-subscriber concurrency limiter) to find due timers
26
-
2. For each due timer, spawns a task that — bounded by the `max_concurrent` capacity limiter — runs an atomic Lua **claim** script that:
26
+
2. For each due timer, spawns a task (bounded by the `max_concurrent` capacity limiter) that runs an atomic Lua **claim** script that:
27
27
- Verifies the timer is still due (score ≤ now)
28
-
- Pushes its score forward by `lease_ttl` seconds — granting the worker a lease
28
+
- Pushes its score forward by `lease_ttl` seconds, which grants the worker a lease
29
29
- Returns the payload
30
30
3. Delivers the payload to the user handler
31
31
4. On handler success, runs an atomic Lua **commit** script (`ZREM` + `HDEL`) to remove the timer from Redis
32
-
5. On handler exception, leaves the timer in place — the lease eventually expires and another worker re-claims it
32
+
5. On handler exception, leaves the timer in place; the lease eventually expires and another worker re-claims it
33
33
34
-
This is the standard SQS-style **visibility-timeout** pattern. The timer's own score in the sorted set acts as the lease deadline — there is no separate lock primitive.
34
+
This is the standard SQS-style **visibility-timeout** pattern. The timer's own score in the sorted set is the lease deadline; there is no separate lock primitive.
35
35
36
36
## At-least-once delivery
37
37
38
-
The lease ensures each due timer is processed by exactly one worker at a time. Because the timer is removed from Redis only after the handler completes successfully, **no timer is lost on crash** — if the worker dies mid-handler, the lease expires and the timer is re-delivered.
38
+
The lease ensures each due timer is processed by exactly one worker at a time. Because the timer is removed from Redis only after the handler completes successfully, **no timer is lost on crash**: if the worker dies mid-handler, the lease expires and the timer is re-delivered.
39
39
40
40
The trade-off: handlers that take longer than `lease_ttl`, or workers that crash after the handler ran but before the commit landed, may see the timer delivered more than once. Handlers must therefore be **idempotent**.
41
41
@@ -44,8 +44,8 @@ The trade-off: handlers that take longer than `lease_ttl`, or workers that crash
44
44
| Action | Effect |
45
45
|--------|--------|
46
46
|`ack`| Atomically removes the timer from `timers_timeline` and `timers_payloads`|
47
-
|`nack`| No-op — the lease expires and another worker re-claims the timer |
48
-
|`reject`| Same as `ack` — permanently removes the timer (use for poison-pill messages) |
47
+
|`nack`| No-op; the lease expires and another worker re-claims the timer |
48
+
|`reject`| Same as `ack`: permanently removes the timer (use for poison-pill messages) |
49
49
50
50
The default ack policy is `NACK_ON_ERROR`: the timer is acknowledged on success, and left for retry on any unhandled exception.
51
51
@@ -64,7 +64,7 @@ The default ack policy is `NACK_ON_ERROR`: the timer is acknowledged on success,
64
64
| Parameter | Default | Description |
65
65
|-----------|---------|-------------|
66
66
|`polling_interval`|`0.05` s | Base poll interval used when the topic has due timers or just transitioned from idle |
67
-
|`max_polling_interval`|`5.0` s | Ceiling for the adaptive idle backoff —`polling_interval` doubles up to this value on consecutive empty polls. Jitter is applied before the cap, so worst-case delivery latency on a previously-idle topic is `max_polling_interval`|
67
+
|`max_polling_interval`|`5.0` s | Ceiling for the adaptive idle backoff:`polling_interval` doubles up to this value on consecutive empty polls. Jitter is applied before the cap, so worst-case delivery latency on a previously-idle topic is `max_polling_interval`|
68
68
|`max_concurrent`|`5`| Max handlers running concurrently per subscriber; also bounds fetch batch size |
69
69
|`lease_ttl`|`30` s | How long a worker holds the lease before another worker may re-claim. Set to ~3–5× the P99 handler runtime: lower values speed up recovery from worker death, higher values tolerate handler GC pauses and clock skew |
0 commit comments