mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-20 06:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# Cron Triggers Gotchas
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Timezone Issues"
|
||||
|
||||
**Problem:** Cron runs at wrong time relative to local timezone
|
||||
**Cause:** All crons execute in UTC, no local timezone support
|
||||
**Solution:** Convert local time to UTC manually
|
||||
|
||||
**Conversion formula:** `utcHour = (localHour - utcOffset + 24) % 24`
|
||||
|
||||
**Examples:**
|
||||
- 9am PST (UTC-8) → `(9 - (-8) + 24) % 24 = 17` → `0 17 * * *`
|
||||
- 2am EST (UTC-5) → `(2 - (-5) + 24) % 24 = 7` → `0 7 * * *`
|
||||
- 6pm JST (UTC+9) → `(18 - 9 + 24) % 24 = 33 % 24 = 9` → `0 9 * * *`
|
||||
|
||||
**Daylight Saving Time:** Adjust manually when DST changes, or schedule at times unaffected by DST (e.g., 2am-4am local time usually safe)
|
||||
|
||||
### "Cron Not Executing"
|
||||
|
||||
**Cause:** Missing `scheduled()` export, invalid syntax, propagation delay (<15min), or plan limits
|
||||
**Solution:** Verify export exists, validate at crontab.guru, wait 15+ min after deploy, check plan limits
|
||||
|
||||
### "Duplicate Executions"
|
||||
|
||||
**Cause:** At-least-once delivery
|
||||
**Solution:** Track execution IDs in KV - see idempotency pattern below
|
||||
|
||||
### "Execution Failures"
|
||||
|
||||
**Cause:** CPU exceeded, unhandled exceptions, network timeouts, binding errors
|
||||
**Solution:** Use try-catch, AbortController timeouts, `ctx.waitUntil()` for long ops, or Workflows for heavy tasks
|
||||
|
||||
### "Local Testing Not Working"
|
||||
|
||||
**Problem:** `/__scheduled` endpoint returns 404 or doesn't trigger handler
|
||||
**Cause:** Missing `scheduled()` export, wrangler not running, or incorrect endpoint format
|
||||
**Solution:**
|
||||
|
||||
1. Verify `scheduled()` is exported:
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
console.log("Cron triggered");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
2. Start dev server:
|
||||
```bash
|
||||
npx wrangler dev
|
||||
```
|
||||
|
||||
3. Use correct endpoint format (URL-encode spaces as `+`):
|
||||
```bash
|
||||
# Correct
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
|
||||
|
||||
# Wrong (will fail)
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5 * * * *"
|
||||
```
|
||||
|
||||
4. Update Wrangler if outdated:
|
||||
```bash
|
||||
npm install -g wrangler@latest
|
||||
```
|
||||
|
||||
### "waitUntil() Tasks Not Completing"
|
||||
|
||||
**Problem:** Background tasks in `ctx.waitUntil()` fail silently or don't execute
|
||||
**Cause:** Promises rejected without error handling, or handler returns before promise settles
|
||||
**Solution:** Always await or handle errors in waitUntil promises:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
// BAD: Silent failures
|
||||
ctx.waitUntil(riskyOperation());
|
||||
|
||||
// GOOD: Explicit error handling
|
||||
ctx.waitUntil(
|
||||
riskyOperation().catch(err => {
|
||||
console.error("Background task failed:", err);
|
||||
return logError(err, env);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### "Idempotency Issues"
|
||||
|
||||
**Problem:** At-least-once delivery causes duplicate side effects (double charges, duplicate emails)
|
||||
**Cause:** No deduplication mechanism
|
||||
**Solution:** Use KV to track execution IDs:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const executionId = `${controller.cron}-${controller.scheduledTime}`;
|
||||
const existing = await env.EXECUTIONS.get(executionId);
|
||||
|
||||
if (existing) {
|
||||
console.log("Already executed, skipping");
|
||||
controller.noRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
await env.EXECUTIONS.put(executionId, "1", { expirationTtl: 86400 }); // 24h TTL
|
||||
await performIdempotentOperation(env);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### "Security Concerns"
|
||||
|
||||
**Problem:** `__scheduled` endpoint exposed in production allows unauthorized cron triggering
|
||||
**Cause:** Testing endpoint available in deployed Workers
|
||||
**Solution:** Block `__scheduled` in production:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Block __scheduled in production
|
||||
if (url.pathname === "/__scheduled" && env.ENVIRONMENT === "production") {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
return handleRequest(request, env, ctx);
|
||||
},
|
||||
|
||||
async scheduled(controller, env, ctx) {
|
||||
// Your cron logic
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Also:** Use `env.API_KEY` for secrets (never hardcode)
|
||||
|
||||
**Alternative:** Add middleware to verify request origin:
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === "/__scheduled") {
|
||||
// Check Cloudflare headers to verify internal request
|
||||
const cfRay = request.headers.get("cf-ray");
|
||||
if (!cfRay && env.ENVIRONMENT === "production") {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
return handleRequest(request, env, ctx);
|
||||
},
|
||||
|
||||
async scheduled(controller, env, ctx) {
|
||||
// Your cron logic
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Limits & Quotas
|
||||
|
||||
| Limit | Free | Paid | Notes |
|
||||
|-------|------|------|-------|
|
||||
| Triggers per Worker | 3 | Unlimited | Maximum cron schedules per Worker |
|
||||
| CPU time | 10ms | 50ms | May need `ctx.waitUntil()` or Workflows |
|
||||
| Execution guarantee | At-least-once | At-least-once | Duplicates possible - use idempotency |
|
||||
| Propagation delay | Up to 15 minutes | Up to 15 minutes | Time for changes to take effect globally |
|
||||
| Min interval | 1 minute | 1 minute | Cannot schedule more frequently |
|
||||
| Cron accuracy | ±1 minute | ±1 minute | Execution may drift slightly |
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
**Unit tests:**
|
||||
- Mock `ScheduledController`, `ExecutionContext`, and bindings
|
||||
- Test each cron expression separately
|
||||
- Verify `noRetry()` is called when expected
|
||||
- Use Vitest with `@cloudflare/vitest-pool-workers` for realistic env
|
||||
|
||||
**Integration tests:**
|
||||
- Test via `/__scheduled` endpoint in dev environment
|
||||
- Verify idempotency logic with duplicate `scheduledTime` values
|
||||
- Test error handling and retry behavior
|
||||
|
||||
**Production:** Start with long intervals (`*/30 * * * *`), monitor Cron Events for 24h, set up alerts before reducing interval
|
||||
|
||||
## Resources
|
||||
|
||||
- [Cron Triggers Docs](https://developers.cloudflare.com/workers/configuration/cron-triggers/)
|
||||
- [Scheduled Handler API](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/)
|
||||
- [Cloudflare Workflows](https://developers.cloudflare.com/workflows/)
|
||||
- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)
|
||||
- [Crontab Guru](https://crontab.guru/) - Validator
|
||||
- [Vitest Pool Workers](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples)
|
||||
Reference in New Issue
Block a user