307 lines
6.5 KiB
Markdown
307 lines
6.5 KiB
Markdown
# Alarms API - Scheduled Tasks
|
|
|
|
Complete guide to scheduling future tasks with alarms.
|
|
|
|
---
|
|
|
|
## What are Alarms?
|
|
|
|
Alarms allow Durable Objects to **schedule themselves** to wake up at a specific time in the future.
|
|
|
|
**Use Cases:**
|
|
- Batching (accumulate items, process in bulk)
|
|
- Cleanup (delete old data periodically)
|
|
- Reminders (notifications, alerts)
|
|
- Delayed operations (rate limiting reset)
|
|
- Periodic tasks (health checks, sync)
|
|
|
|
---
|
|
|
|
## Set Alarm
|
|
|
|
### `storage.setAlarm(time)`
|
|
|
|
```typescript
|
|
// Fire in 10 seconds
|
|
await this.ctx.storage.setAlarm(Date.now() + 10000);
|
|
|
|
// Fire at specific date/time
|
|
await this.ctx.storage.setAlarm(new Date('2025-12-31T23:59:59Z'));
|
|
|
|
// Fire in 1 hour
|
|
await this.ctx.storage.setAlarm(Date.now() + 3600000);
|
|
```
|
|
|
|
**Parameters:**
|
|
- `time` (number | Date): Unix timestamp (ms) or Date object
|
|
|
|
**Behavior:**
|
|
- ✅ **Only ONE alarm per DO** - setting new alarm overwrites previous
|
|
- ✅ **Persists across hibernation** - survives DO eviction
|
|
- ✅ **Guaranteed at-least-once execution**
|
|
|
|
---
|
|
|
|
## Alarm Handler
|
|
|
|
### `alarm(alarmInfo)`
|
|
|
|
Called when alarm fires (or retries).
|
|
|
|
```typescript
|
|
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
|
|
console.log(`Alarm fired (retry: ${alarmInfo.isRetry}, count: ${alarmInfo.retryCount})`);
|
|
|
|
// Do work
|
|
await this.processBatch();
|
|
|
|
// Alarm is automatically deleted after successful execution
|
|
}
|
|
```
|
|
|
|
**Parameters:**
|
|
- `alarmInfo.retryCount` (number): Number of retries (0 on first attempt)
|
|
- `alarmInfo.isRetry` (boolean): True if this is a retry
|
|
|
|
**CRITICAL:**
|
|
- ✅ **Implement idempotent operations** (safe to retry)
|
|
- ✅ **Limit retry attempts** (avoid infinite retries)
|
|
- ❌ **Don't throw errors lightly** (triggers automatic retry)
|
|
|
|
---
|
|
|
|
## Get Alarm
|
|
|
|
### `storage.getAlarm()`
|
|
|
|
Get current alarm time (null if not set).
|
|
|
|
```typescript
|
|
const alarmTime = await this.ctx.storage.getAlarm();
|
|
|
|
if (alarmTime === null) {
|
|
// No alarm set
|
|
await this.ctx.storage.setAlarm(Date.now() + 60000);
|
|
} else {
|
|
console.log(`Alarm scheduled for ${new Date(alarmTime).toISOString()}`);
|
|
}
|
|
```
|
|
|
|
**Returns:** Promise<number | null> (Unix timestamp in ms)
|
|
|
|
---
|
|
|
|
## Delete Alarm
|
|
|
|
### `storage.deleteAlarm()`
|
|
|
|
Cancel scheduled alarm.
|
|
|
|
```typescript
|
|
await this.ctx.storage.deleteAlarm();
|
|
```
|
|
|
|
**When to use:**
|
|
- Cancel scheduled task
|
|
- Before deleting DO (if using `deleteAll()`)
|
|
|
|
---
|
|
|
|
## Retry Behavior
|
|
|
|
**Automatic Retries:**
|
|
- Up to **6 retries** on failure
|
|
- Exponential backoff: **2s, 4s, 8s, 16s, 32s, 64s**
|
|
- Retries if `alarm()` throws uncaught exception
|
|
|
|
**Example with retry limit:**
|
|
|
|
```typescript
|
|
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
|
|
if (alarmInfo.retryCount > 3) {
|
|
console.error('Alarm failed after 3 retries, giving up');
|
|
// Clean up to avoid infinite retries
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.sendNotification();
|
|
} catch (error) {
|
|
console.error('Alarm failed:', error);
|
|
throw error; // Will trigger retry
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Pattern 1: Batching
|
|
|
|
Accumulate items, process in bulk.
|
|
|
|
```typescript
|
|
async addItem(item: string): Promise<void> {
|
|
this.buffer.push(item);
|
|
await this.ctx.storage.put('buffer', this.buffer);
|
|
|
|
// Schedule alarm if not already set
|
|
const alarm = await this.ctx.storage.getAlarm();
|
|
if (alarm === null) {
|
|
await this.ctx.storage.setAlarm(Date.now() + 10000); // 10s
|
|
}
|
|
}
|
|
|
|
async alarm(): Promise<void> {
|
|
this.buffer = await this.ctx.storage.get('buffer') || [];
|
|
|
|
if (this.buffer.length > 0) {
|
|
await this.processBatch(this.buffer);
|
|
this.buffer = [];
|
|
await this.ctx.storage.put('buffer', []);
|
|
}
|
|
|
|
// Alarm automatically deleted after success
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Periodic Cleanup
|
|
|
|
Run cleanup every hour.
|
|
|
|
```typescript
|
|
constructor(ctx: DurableObjectState, env: Env) {
|
|
super(ctx, env);
|
|
|
|
// Schedule first cleanup
|
|
ctx.blockConcurrencyWhile(async () => {
|
|
const alarm = await ctx.storage.getAlarm();
|
|
if (alarm === null) {
|
|
await ctx.storage.setAlarm(Date.now() + 3600000); // 1 hour
|
|
}
|
|
});
|
|
}
|
|
|
|
async alarm(): Promise<void> {
|
|
// Cleanup old data
|
|
await this.cleanup();
|
|
|
|
// Schedule next cleanup
|
|
await this.ctx.storage.setAlarm(Date.now() + 3600000);
|
|
}
|
|
```
|
|
|
|
### Pattern 3: Delayed Operation
|
|
|
|
Execute task after delay.
|
|
|
|
```typescript
|
|
async scheduleTask(task: string, delayMs: number): Promise<void> {
|
|
await this.ctx.storage.put('pendingTask', task);
|
|
await this.ctx.storage.setAlarm(Date.now() + delayMs);
|
|
}
|
|
|
|
async alarm(): Promise<void> {
|
|
const task = await this.ctx.storage.get('pendingTask');
|
|
|
|
if (task) {
|
|
await this.executeTask(task);
|
|
await this.ctx.storage.delete('pendingTask');
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 4: Reminder/Notification
|
|
|
|
One-time reminder.
|
|
|
|
```typescript
|
|
async setReminder(message: string, fireAt: Date): Promise<void> {
|
|
await this.ctx.storage.put('reminder', { message, fireAt: fireAt.getTime() });
|
|
await this.ctx.storage.setAlarm(fireAt);
|
|
}
|
|
|
|
async alarm(): Promise<void> {
|
|
const reminder = await this.ctx.storage.get('reminder');
|
|
|
|
if (reminder) {
|
|
await this.sendNotification(reminder.message);
|
|
await this.ctx.storage.delete('reminder');
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Limitations
|
|
|
|
⚠️ **One alarm per DO**
|
|
- Setting new alarm overwrites previous
|
|
- Use storage to track multiple pending tasks
|
|
|
|
⚠️ **No cron syntax**
|
|
- Alarm is one-time (but can reschedule in handler)
|
|
- For periodic tasks, reschedule in `alarm()` handler
|
|
|
|
⚠️ **Minimum precision: ~1 second**
|
|
- Don't expect millisecond precision
|
|
- Designed for longer delays (seconds to hours)
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### Idempotent Operations
|
|
|
|
```typescript
|
|
// ✅ GOOD: Idempotent (safe to retry)
|
|
async alarm(): Promise<void> {
|
|
const messageId = await this.ctx.storage.get('messageId');
|
|
|
|
// Check if already sent (idempotent)
|
|
const sent = await this.checkIfSent(messageId);
|
|
if (sent) {
|
|
return;
|
|
}
|
|
|
|
await this.sendMessage(messageId);
|
|
await this.markAsSent(messageId);
|
|
}
|
|
|
|
// ❌ BAD: Not idempotent (duplicate sends on retry)
|
|
async alarm(): Promise<void> {
|
|
await this.sendMessage(); // Will send duplicate if retried
|
|
}
|
|
```
|
|
|
|
### Limit Retries
|
|
|
|
```typescript
|
|
async alarm(info: { retryCount: number }): Promise<void> {
|
|
if (info.retryCount > 3) {
|
|
console.error('Giving up after 3 retries');
|
|
return;
|
|
}
|
|
|
|
// Try operation
|
|
await this.doWork();
|
|
}
|
|
```
|
|
|
|
### Clean Up Before `deleteAll()`
|
|
|
|
```typescript
|
|
async destroy(): Promise<void> {
|
|
// Delete alarm first
|
|
await this.ctx.storage.deleteAlarm();
|
|
|
|
// Then delete all storage
|
|
await this.ctx.storage.deleteAll();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
**Official Docs**: https://developers.cloudflare.com/durable-objects/api/alarms/
|