|
| 1 | +--- |
| 2 | +# 0.5 - API |
| 3 | +# 2 - Release |
| 4 | +# 3 - Contributing |
| 5 | +# 5 - Template Page |
| 6 | +# 10 - Default |
| 7 | +search: |
| 8 | + boost: 10 |
| 9 | +--- |
| 10 | + |
| 11 | +# Redis Stream Message Claiming |
| 12 | + |
| 13 | +When working with Redis Stream Consumer Groups, there may be situations where messages remain in a pending state because a consumer failed to process them. FastStream provides a mechanism to automatically claim these pending messages using Redis's `XAUTOCLAIM` command through the `min_idle_time` parameter. |
| 14 | + |
| 15 | +## What is Message Claiming? |
| 16 | + |
| 17 | +In Redis Streams, when a consumer reads a message from a consumer group but fails to acknowledge it (due to a crash, network issue, or processing error), the message remains in the [**Pending Entries List (PEL)**](https://redis.io/docs/latest/develop/data-types/streams/#working-with-multiple-consumer-groups) of that consumer group. These unacknowledged messages are associated with the original consumer and have an "idle time" - the duration since they were last delivered. |
| 18 | + |
| 19 | +Message claiming allows another consumer to take ownership of these pending messages that have been idle for too long, ensuring that messages don't get stuck and workload can be redistributed among healthy consumers. |
| 20 | + |
| 21 | +## Using `min_idle_time` for Automatic Claiming |
| 22 | + |
| 23 | +FastStream's `StreamSub` provides a `min_idle_time` parameter that enables automatic claiming of pending messages via Redis's `XAUTOCLAIM` command. When set, the consumer will automatically scan for and claim messages that have been pending for at least the specified duration (in milliseconds). |
| 24 | + |
| 25 | +### Basic Example |
| 26 | + |
| 27 | +Here's a simple example that demonstrates automatic message claiming: |
| 28 | + |
| 29 | +```python linenums="1" |
| 30 | +{! docs_src/redis/stream/claiming_basic.py !} |
| 31 | +``` |
| 32 | + |
| 33 | +## How It Works |
| 34 | + |
| 35 | +When `min_idle_time` is set: |
| 36 | + |
| 37 | +1. **Circular Scanning**: Instead of using `XREADGROUP` to read new messages, the consumer uses `XAUTOCLAIM` to scan the Pending Entries List |
| 38 | +2. **Idle Time Check**: Only messages that have been pending for at least `min_idle_time` milliseconds are claimed |
| 39 | +3. **Ownership Transfer**: Claimed messages are automatically transferred from the failing consumer to the claiming consumer |
| 40 | +4. **Continuous Processing**: The scanning process is circular - after reaching the end of the [PEL](https://redis.io/docs/latest/develop/data-types/streams/#working-with-multiple-consumer-groups), it starts over from the beginning |
| 41 | + |
| 42 | +### Practical Use Case |
| 43 | + |
| 44 | +Consider a scenario where you have multiple workers processing orders: |
| 45 | + |
| 46 | +```python linenums="1" |
| 47 | +from faststream import FastStream |
| 48 | +from faststream.redis import RedisBroker, StreamSub |
| 49 | + |
| 50 | +broker = RedisBroker() |
| 51 | +app = FastStream(broker) |
| 52 | + |
| 53 | +# Worker that might fail |
| 54 | +@broker.subscriber( |
| 55 | + stream=StreamSub( |
| 56 | + "orders", |
| 57 | + group="order-processors", |
| 58 | + consumer="worker-1", |
| 59 | + ) |
| 60 | +) |
| 61 | +async def worker_that_might_fail(order_id: str): |
| 62 | + # Process order - might crash before acknowledging |
| 63 | + await process_complex_order(order_id) |
| 64 | + # If crash happens here, message stays pending |
| 65 | + |
| 66 | +# Backup worker with message claiming |
| 67 | +@broker.subscriber( |
| 68 | + stream=StreamSub( |
| 69 | + "orders", |
| 70 | + group="order-processors", |
| 71 | + consumer="worker-2", |
| 72 | + min_idle_time=10000, # 10 seconds |
| 73 | + ) |
| 74 | +) |
| 75 | +async def backup_worker(order_id: str): |
| 76 | + # This worker will automatically pick up messages |
| 77 | + # that worker-1 failed to process within 10 seconds |
| 78 | + print(f"Recovering and processing order: {order_id}") |
| 79 | + await process_complex_order(order_id) |
| 80 | +``` |
| 81 | + |
| 82 | +## Combining with Manual Acknowledgment |
| 83 | + |
| 84 | +You can combine `min_idle_time` with manual acknowledgment policies for fine-grained control: |
| 85 | + |
| 86 | +```python linenums="1" |
| 87 | +{! docs_src/redis/stream/claiming_manual_ack.py !} |
| 88 | +``` |
| 89 | + |
| 90 | +## Configuration Guidelines |
| 91 | + |
| 92 | +### Choosing `min_idle_time` |
| 93 | + |
| 94 | +The appropriate `min_idle_time` value depends on your use case: |
| 95 | + |
| 96 | +- **Short duration (1-5 seconds)**: For fast-processing tasks where quick failure recovery is needed |
| 97 | +- **Medium duration (10-60 seconds)**: For most general-purpose applications with moderate processing times |
| 98 | +- **Long duration (5-30 minutes)**: For long-running tasks where you want to ensure a consumer has truly failed |
| 99 | + |
| 100 | +!!! warning |
| 101 | + Setting `min_idle_time` too low may cause messages to be unnecessarily transferred between healthy consumers. Set it based on your typical message processing time plus a safety buffer. |
| 102 | + |
| 103 | +### Deployment Patterns |
| 104 | + |
| 105 | +#### Pattern 1: Dedicated Claiming Worker |
| 106 | +Deploy a separate worker specifically for claiming abandoned messages: |
| 107 | + |
| 108 | +```python |
| 109 | +# Main workers (fast path) |
| 110 | +@broker.subscriber( |
| 111 | + stream=StreamSub("tasks", group="workers", consumer="main-1") |
| 112 | +) |
| 113 | +async def main_worker(task): ... |
| 114 | + |
| 115 | +# Claiming worker (recovery path) |
| 116 | +@broker.subscriber( |
| 117 | + stream=StreamSub("tasks", group="workers", consumer="claimer", min_idle_time=15000) |
| 118 | +) |
| 119 | +async def claiming_worker(task): ... |
| 120 | +``` |
| 121 | + |
| 122 | +#### Pattern 2: All Workers Can Claim |
| 123 | +All workers can claim messages from each other: |
| 124 | + |
| 125 | +```python |
| 126 | +# Each worker can both process new messages and claim abandoned ones |
| 127 | +@broker.subscriber( |
| 128 | + stream=StreamSub( |
| 129 | + "tasks", |
| 130 | + group="workers", |
| 131 | + consumer=f"worker-{instance_id}", |
| 132 | + min_idle_time=10000, |
| 133 | + ) |
| 134 | +) |
| 135 | +async def worker(task): ... |
| 136 | +``` |
| 137 | + |
| 138 | +## Technical Details |
| 139 | + |
| 140 | +- **Start ID**: FastStream automatically manages the `start_id` parameter for `XAUTOCLAIM`, enabling circular scanning through the Pending Entries List |
| 141 | +- **Empty Results**: When no pending messages meet the idle time criteria, the consumer will continue polling |
| 142 | +- **ACK Handling**: Claimed messages must still be acknowledged using `msg.ack()` to be removed from the [PEL](https://redis.io/docs/latest/develop/data-types/streams/#working-with-multiple-consumer-groups) |
| 143 | + |
| 144 | +## References |
| 145 | + |
| 146 | +For more information about Redis Streams message claiming: |
| 147 | + |
| 148 | +- [Redis XAUTOCLAIM Documentation](https://redis.io/docs/latest/commands/xautoclaim/){.external-link target="_blank"} |
| 149 | +- [Redis Streams Claiming Guide](https://redis.io/docs/latest/develop/data-types/streams/#claiming-and-the-delivery-counter){.external-link target="_blank"} |
0 commit comments