|
1 | | -# example-countdown-py |
| 1 | +# Example: Countdown with Durable Sleep (Python) |
| 2 | + |
| 3 | +A countdown timer that demonstrates **durable sleep** - long pauses without consuming resources, surviving crashes and resuming exactly where it left off. |
| 4 | + |
| 5 | +## What This Demonstrates |
| 6 | + |
| 7 | +### Problem: Long-Running Timers |
| 8 | +Traditional countdown timers keep a process running continuously, wasting resources during sleep periods. If the process crashes, you lose the countdown state. |
| 9 | + |
| 10 | +### Solution: Durable Sleep |
| 11 | +Resonate's durable sleep allows workflows to: |
| 12 | +- Pause for extended periods (hours, days, weeks) |
| 13 | +- Consume zero resources while sleeping |
| 14 | +- Survive crashes and resume at the right time |
| 15 | +- Send periodic notifications reliably |
| 16 | + |
| 17 | +## Use Cases |
| 18 | + |
| 19 | +This pattern applies to: |
| 20 | +- **Scheduled reminders** - Meeting notifications, task deadlines |
| 21 | +- **SLA monitors** - Alert if response not received in time |
| 22 | +- **Rate limiting** - Enforce delays between operations |
| 23 | +- **Periodic reports** - Daily/weekly/monthly automation |
| 24 | +- **Countdown timers** - Track time until events |
| 25 | + |
| 26 | +## How It Works |
| 27 | + |
| 28 | +```python |
| 29 | +def countdown(ctx: Context, count: int, interval, url: str): |
| 30 | + for i in range(count, 0, -1): |
| 31 | + # Send notification (durable) |
| 32 | + yield ctx.run(notify, message=f"Countdown: {i}", url=url) |
| 33 | + |
| 34 | + # Sleep for interval (process can exit here) |
| 35 | + yield ctx.sleep(interval) |
| 36 | + |
| 37 | + # Final notification |
| 38 | + yield ctx.run(notify, message="Countdown complete", url=url) |
| 39 | +``` |
| 40 | + |
| 41 | +### What Happens on Each Iteration |
| 42 | + |
| 43 | +1. **Send notification** - `ctx.run()` executes and checkpoints the result |
| 44 | +2. **Sleep** - `ctx.sleep()` suspends the workflow, process can exit |
| 45 | +3. **Resume** - Resonate wakes the workflow after the interval |
| 46 | +4. **Repeat** - Continue from checkpoint without re-sending notifications |
| 47 | + |
| 48 | +### Crash Recovery |
| 49 | + |
| 50 | +If the process crashes during sleep: |
| 51 | +- Workflow state is preserved in Resonate |
| 52 | +- Resonate automatically resumes at the scheduled wake time |
| 53 | +- No notifications are duplicated |
| 54 | +- Countdown continues as if nothing happened |
| 55 | + |
| 56 | +## Running the Example |
| 57 | + |
| 58 | +### Prerequisites |
| 59 | + |
| 60 | +- Python 3.13+ |
| 61 | +- uv (Python package manager) |
| 62 | +- ntfy.sh account or webhook URL |
| 63 | + |
| 64 | +### Installation |
| 65 | + |
| 66 | +```bash |
| 67 | +# Install dependencies |
| 68 | +uv sync |
| 69 | +``` |
| 70 | + |
| 71 | +### Usage |
| 72 | + |
| 73 | +```bash |
| 74 | +# Start the countdown worker |
| 75 | +uv run python countdown.py |
| 76 | +``` |
| 77 | + |
| 78 | +Then trigger a countdown via the Resonate API or another process: |
| 79 | + |
| 80 | +```bash |
| 81 | +# Start 10-minute countdown with 1-minute intervals |
| 82 | +curl -X POST http://localhost:8001/promises \ |
| 83 | + -H "Content-Type: application/json" \ |
| 84 | + -d '{ |
| 85 | + "id": "countdown/demo", |
| 86 | + "timeout": 36000000, |
| 87 | + "data": { |
| 88 | + "func": "countdown", |
| 89 | + "args": [10, 60000, "https://ntfy.sh/your-topic"] |
| 90 | + } |
| 91 | + }' |
| 92 | +``` |
| 93 | + |
| 94 | +**Parameters:** |
| 95 | +- `count`: Number of countdown steps (10 = 10 notifications) |
| 96 | +- `interval`: Milliseconds between steps (60000 = 1 minute) |
| 97 | +- `url`: Webhook URL for notifications (ntfy.sh, Slack, etc.) |
| 98 | + |
| 99 | +### Notification Format |
| 100 | + |
| 101 | +Each notification sends JSON: |
| 102 | +```json |
| 103 | +{ |
| 104 | + "message": "Countdown: 7" |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +## Configuration |
| 109 | + |
| 110 | +### Environment Variables |
| 111 | + |
| 112 | +Create a `.env` file: |
| 113 | +```bash |
| 114 | +RESONATE_URL=http://localhost:8001 |
| 115 | +RESONATE_TOKEN=your-token-here |
| 116 | +NTFY_URL=https://ntfy.sh/your-topic |
| 117 | +``` |
| 118 | + |
| 119 | +### Notification Backends |
| 120 | + |
| 121 | +The example uses ntfy.sh by default, but works with any webhook: |
| 122 | + |
| 123 | +**Slack:** |
| 124 | +```python |
| 125 | +url = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" |
| 126 | +``` |
| 127 | + |
| 128 | +**Discord:** |
| 129 | +```python |
| 130 | +url = "https://discord.com/api/webhooks/YOUR/WEBHOOK" |
| 131 | +``` |
| 132 | + |
| 133 | +**Custom API:** |
| 134 | +```python |
| 135 | +url = "https://api.example.com/notifications" |
| 136 | +``` |
| 137 | + |
| 138 | +## Testing Failure Scenarios |
| 139 | + |
| 140 | +### Test 1: Crash During Sleep |
| 141 | + |
| 142 | +1. Start countdown with long interval: |
| 143 | +```bash |
| 144 | +# 5 steps, 30 seconds each |
| 145 | +resonate.run("test/crash", countdown, 5, 30000, url) |
| 146 | +``` |
| 147 | + |
| 148 | +2. Wait for first notification |
| 149 | + |
| 150 | +3. Kill the process (Ctrl+C) |
| 151 | + |
| 152 | +4. Restart the process: |
| 153 | +```bash |
| 154 | +uv run python countdown.py |
| 155 | +``` |
| 156 | + |
| 157 | +5. Observe: Countdown resumes at the correct step, no duplicates |
| 158 | + |
| 159 | +### Test 2: Long-Running Countdown |
| 160 | + |
| 161 | +```bash |
| 162 | +# 24-hour countdown with hourly notifications |
| 163 | +resonate.run("test/longrun", countdown, 24, 3600000, url) |
| 164 | +``` |
| 165 | + |
| 166 | +Process can stop/start freely. Notifications arrive on schedule. |
| 167 | + |
| 168 | +## Code Structure |
| 169 | + |
| 170 | +``` |
| 171 | +countdown.py # Countdown workflow definition |
| 172 | +main.py # Entry point (if using GCP Functions) |
| 173 | +pyproject.toml # Dependencies |
| 174 | +``` |
| 175 | + |
| 176 | +## Serverless Deployment |
| 177 | + |
| 178 | +This example works on serverless platforms with execution time limits: |
| 179 | + |
| 180 | +### Google Cloud Functions |
| 181 | + |
| 182 | +The `main.py` file shows GCP Functions integration: |
| 183 | +```python |
| 184 | +from resonategcp import Resonate |
| 185 | + |
| 186 | +resonate = Resonate.remote() |
| 187 | +resonate.register(countdown) |
| 188 | +handler = resonate.handler_http() |
| 189 | +``` |
| 190 | + |
| 191 | +Deploy with: |
| 192 | +```bash |
| 193 | +gcloud functions deploy countdown \ |
| 194 | + --runtime python313 \ |
| 195 | + --trigger-http \ |
| 196 | + --entry-point handler \ |
| 197 | + --set-env-vars RESONATE_URL=your-server |
| 198 | +``` |
| 199 | + |
| 200 | +The function can timeout after each sleep - Resonate handles resumption. |
| 201 | + |
| 202 | +### Other Platforms |
| 203 | + |
| 204 | +Similar patterns work on: |
| 205 | +- AWS Lambda (with custom runtime or layers) |
| 206 | +- Azure Functions |
| 207 | +- Cloudflare Workers (via WebAssembly) |
| 208 | + |
| 209 | +## Key Concepts |
| 210 | + |
| 211 | +### Durable Sleep |
| 212 | +- `ctx.sleep(milliseconds)` suspends workflow |
| 213 | +- Process can exit, workflow state persists |
| 214 | +- Resonate resumes at the right time |
| 215 | +- No polling, no cron jobs needed |
| 216 | + |
| 217 | +### Idempotency |
| 218 | +- Each notification uses a deterministic ID |
| 219 | +- Replays don't duplicate notifications |
| 220 | +- `ctx.run()` checkpoints prevent re-execution |
| 221 | + |
| 222 | +### Resource Efficiency |
| 223 | +- Zero CPU/memory during sleep |
| 224 | +- Scale to zero between notifications |
| 225 | +- Pay only for active execution time |
| 226 | + |
| 227 | +## Production Considerations |
| 228 | + |
| 229 | +1. **Monitoring**: Track notification delivery success |
| 230 | +2. **Error handling**: Add retry logic for webhook failures |
| 231 | +3. **Timeouts**: Set workflow timeout longer than total countdown |
| 232 | +4. **Backpressure**: Limit concurrent countdown workflows |
| 233 | +5. **Observability**: Log each notification for audit trail |
| 234 | + |
| 235 | +## Learn More |
| 236 | + |
| 237 | +- [Python SDK Docs](https://docs.resonatehq.io/sdk/python) |
| 238 | +- [Durable Sleep Explained](https://docs.resonatehq.io/concepts/sleep) |
| 239 | +- [Serverless Patterns](https://docs.resonatehq.io/patterns/serverless) |
| 240 | + |
| 241 | +## Related Examples |
| 242 | + |
| 243 | +- [example-countdown-ts](../example-countdown-ts) - TypeScript version |
| 244 | +- [example-countdown-gcp-ts](../example-countdown-gcp-ts) - GCP Functions (TypeScript) |
| 245 | +- [example-countdown-supabase-ts](../example-countdown-supabase-ts) - Supabase Edge Functions |
| 246 | +- [example-durable-sleep-py](../example-durable-sleep-py) - Basic sleep patterns |
0 commit comments