|
| 1 | +# Redis Timers |
| 2 | + |
| 3 | +[](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration) |
| 4 | +[](https://pypi.python.org/pypi/redis-timers) |
| 5 | +[](https://pypistats.org/packages/redis-timers) |
| 6 | +[](https://github.com/modern-python/redis-timers/stargazers) |
| 7 | + |
| 8 | +Redis Timers is a Python library that provides a robust framework for managing timed events using Redis as the backend. |
| 9 | + |
| 10 | +It allows you to schedule timers that trigger handlers at specific times, with payloads automatically validated using Pydantic schemas. |
| 11 | + |
| 12 | +## Features |
| 13 | + |
| 14 | +- Schedule timers with custom payloads |
| 15 | +- Automatic payload validation using Pydantic |
| 16 | +- Distributed lock mechanism to prevent duplicate processing |
| 17 | +- Topic-based routing for timer handlers |
| 18 | +- Automatic cleanup of processed timers |
| 19 | +- Built with asyncio for high performance |
| 20 | + |
| 21 | +## 📦 [PyPi](https://pypi.org/project/redis-timers) |
| 22 | + |
| 23 | +## 📝 [License](LICENSE) |
| 24 | + |
| 25 | + |
| 26 | +## Installation |
| 27 | + |
| 28 | +```bash |
| 29 | +pip install redis-timers |
| 30 | +``` |
| 31 | + |
| 32 | +## Quick Start |
| 33 | + |
| 34 | +```python |
| 35 | +import asyncio |
| 36 | +import datetime |
| 37 | +from redis import asyncio as aioredis |
| 38 | +from pydantic import BaseModel |
| 39 | + |
| 40 | +from redis_timers import Timers, Router |
| 41 | + |
| 42 | +# Define your payload schema |
| 43 | +class MyPayload(BaseModel): |
| 44 | + message: str |
| 45 | + count: int |
| 46 | + |
| 47 | +# Create a router and register handlers |
| 48 | +router = Router() |
| 49 | + |
| 50 | +@router.handler(topic="my_topic", schema=MyPayload) |
| 51 | +async def my_timer_handler(data: MyPayload, context: dict): |
| 52 | + print(f"Timer triggered: {data.message} (count: {data.count})") |
| 53 | + |
| 54 | +# Initialize Redis client |
| 55 | +redis_client = aioredis.Redis.from_url("redis://localhost:6379", decode_responses=True) |
| 56 | + |
| 57 | +# Initialize timers |
| 58 | +timers = Timers(redis_client=redis_client, context={"app_name": "my_app"}) |
| 59 | +timers.include_router(router) |
| 60 | + |
| 61 | +# Schedule a timer |
| 62 | +async def schedule_timer(): |
| 63 | + payload = MyPayload(message="Hello, World!", count=42) |
| 64 | + await timers.set_timer( |
| 65 | + topic="my_topic", |
| 66 | + timer_id="unique_timer_id", |
| 67 | + payload=payload, |
| 68 | + activation_period=datetime.timedelta(minutes=5) |
| 69 | + ) |
| 70 | + |
| 71 | +# Run the timer processing loop |
| 72 | +async def main(): |
| 73 | + # Schedule a timer |
| 74 | + await schedule_timer() |
| 75 | + |
| 76 | + # Process timers continuously |
| 77 | + await timers.run_forever() |
| 78 | + |
| 79 | +if __name__ == "__main__": |
| 80 | + asyncio.run(main()) |
| 81 | +``` |
| 82 | + |
| 83 | +## Core Concepts |
| 84 | + |
| 85 | +### Timers |
| 86 | + |
| 87 | +The `Timers` class is the main interface for managing timed events. It connects to Redis and handles the scheduling, execution, and cleanup of timers. |
| 88 | + |
| 89 | +### Router |
| 90 | + |
| 91 | +The `Router` class is used to register timer handlers. Handlers are functions that get executed when a timer expires. |
| 92 | + |
| 93 | +### Handlers |
| 94 | + |
| 95 | +Handlers are async functions decorated with `@router.handler()` that process timer events. Each handler is associated with a topic and a Pydantic schema for payload validation. |
| 96 | + |
| 97 | +### Payloads |
| 98 | + |
| 99 | +Payloads are Pydantic models that contain the data associated with a timer. They are automatically validated when timers are processed. |
| 100 | + |
| 101 | +## API Reference |
| 102 | + |
| 103 | +### Timers |
| 104 | + |
| 105 | +#### `Timers(redis_client, context={})` |
| 106 | + |
| 107 | +Initialize a Timers instance. |
| 108 | + |
| 109 | +- `redis_client`: An async Redis client instance |
| 110 | +- `context`: A dictionary of context data passed to handlers |
| 111 | + |
| 112 | +#### `timers.include_router(router)` |
| 113 | + |
| 114 | +Include a router with its handlers. |
| 115 | + |
| 116 | +#### `timers.include_routers(*routers)` |
| 117 | + |
| 118 | +Include multiple routers. |
| 119 | + |
| 120 | +#### `timers.set_timer(topic, timer_id, payload, activation_period)` |
| 121 | + |
| 122 | +Schedule a timer. |
| 123 | + |
| 124 | +- `topic`: The topic associated with the timer handler |
| 125 | +- `timer_id`: A unique identifier for the timer |
| 126 | +- `payload`: A Pydantic model instance containing the timer data |
| 127 | +- `activation_period`: A timedelta specifying when the timer should trigger |
| 128 | + |
| 129 | +#### `timers.remove_timer(topic, timer_id)` |
| 130 | + |
| 131 | +Remove a scheduled timer. |
| 132 | + |
| 133 | +#### `timers.handle_ready_timers()` |
| 134 | + |
| 135 | +Process all timers that are ready to be triggered. |
| 136 | + |
| 137 | +#### `timers.run_forever()` |
| 138 | + |
| 139 | +Continuously process timers in a loop. |
| 140 | + |
| 141 | +### Router |
| 142 | + |
| 143 | +#### `Router()` |
| 144 | + |
| 145 | +Create a new router instance. |
| 146 | + |
| 147 | +#### `router.handler(topic, schema)` |
| 148 | + |
| 149 | +Decorator for registering timer handlers. |
| 150 | + |
| 151 | +- `topic`: The topic to associate with this handler |
| 152 | +- `schema`: The Pydantic model class for payload validation |
| 153 | + |
| 154 | +## Configuration |
| 155 | + |
| 156 | +Redis Timers can be configured using environment variables: |
| 157 | + |
| 158 | +- `TIMERS_TIMELINE_KEY`: Redis key for the sorted set storing timer timestamps (default: "timers_timeline") |
| 159 | +- `TIMERS_PAYLOADS_KEY`: Redis key for the hash storing timer payloads (default: "timers_payloads") |
| 160 | +- `TIMERS_HANDLING_SLEEP`: Base sleep time between timer processing cycles (default: 0.05 seconds) |
| 161 | +- `TIMERS_HANDLING_JITTER_MIN_VALUE`: Minimum jitter multiplier (default: 0.5) |
| 162 | +- `TIMERS_HANDLING_JITTER_MAX_VALUE`: Maximum jitter multiplier (default: 2.0) |
| 163 | +- `TIMERS_SEPARATOR`: Separator used between topic and timer ID (default: "--") |
| 164 | + |
| 165 | +## How It Works |
| 166 | + |
| 167 | +1. **Timer Scheduling**: When you call `set_timer()`, Redis Timers stores: |
| 168 | + - The timer's activation timestamp in a Redis sorted set (`timers_timeline`) |
| 169 | + - The timer's payload as JSON in a Redis hash (`timers_payloads`) |
| 170 | + - The timer key is constructed as `{topic}{separator}{timer_id}` |
| 171 | + |
| 172 | +2. **Timer Processing**: The `run_forever()` method continuously: |
| 173 | + - Checks for timers that should be triggered (current timestamp >= activation timestamp) |
| 174 | + - Acquires a distributed lock to prevent duplicate processing |
| 175 | + - Validates the payload using the registered Pydantic schema |
| 176 | + - Calls the appropriate handler function |
| 177 | + - Removes the processed timer from Redis |
| 178 | + |
| 179 | +3. **Distributed Locking**: Redis Timers uses Redis locks to ensure that: |
| 180 | + - The same timer isn't processed multiple times in distributed environments |
| 181 | + - Concurrent modifications to the same timer are prevented |
| 182 | + |
| 183 | +## Requirements |
| 184 | + |
| 185 | +- Python 3.13+ |
| 186 | +- Redis server |
| 187 | +- Dependencies: `pydantic`, `redis` |
0 commit comments