[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-10 22:08:15 +09:00
parent 21ac3ed255
commit 504fd5fb42
3011 changed files with 380280 additions and 206977 deletions
@@ -0,0 +1,410 @@
---
id: backend-cron-scheduler-patterns
title: Cron / Scheduler — Quartz / cron / Inngest / Trigger.dev
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [backend, cron, scheduler, vibe-coding]
tech_stack: { language: "TS / Python", applicable_to: ["Backend"] }
applied_in: []
aliases: [cron, scheduler, Inngest, Trigger.dev, BullMQ, Sidekiq, Temporal cron, distributed cron]
---
# Cron / Scheduler
> "매 X 시간 작업 실행". **Cron 가 simple, BullMQ / Inngest / Trigger.dev / Temporal 가 modern**. Distributed lock + retry + observability.
## 📖 핵심 개념
- Cron expression: `0 * * * *`.
- Single instance vs distributed.
- Missed run 처리.
- Retry / dedup / idempotency.
## 💻 코드 패턴
### Linux cron (가장 simple)
```cron
# crontab -e
0 2 * * * /usr/bin/node /app/cleanup.js >> /var/log/cleanup.log 2>&1
```
→ 매일 2 AM. 1 server 만.
### Cron expression
```
* * * * *
| | | | |
| | | | +-- Day of week (0-6, Sun=0)
| | | +---- Month (1-12)
| | +------ Day of month (1-31)
| +-------- Hour (0-23)
+---------- Minute (0-59)
0 * * * * 매 hour
*/5 * * * * 매 5 min
0 9 * * 1-5 평일 9 AM
0 0 1 * * 매월 1일 자정
```
→ crontab.guru 가 검증.
### Node-cron (in-process)
```ts
import cron from 'node-cron';
cron.schedule('0 * * * *', async () => {
await cleanupOldData();
});
```
→ Single instance only. Restart 시 잃음.
### BullMQ (Redis-backed)
```ts
import { Queue, Worker } from 'bullmq';
const queue = new Queue('jobs', { connection: { host: 'redis' } });
// Repeating job
await queue.add('cleanup', null, {
repeat: { pattern: '0 * * * *' },
});
// Worker
new Worker('jobs', async (job) => {
if (job.name === 'cleanup') await cleanup();
}, { connection });
```
→ Multi-instance OK (Redis 가 lock). Persistent.
### Distributed cron 의 문제
```
2 instance × 같은 cron = 2번 실행.
해결:
1. Lock (Redis / DB).
2. Single leader.
3. Job queue.
```
### Distributed lock (Redis)
```ts
async function withLock(key: string, ttl: number, fn: () => Promise<void>) {
const got = await redis.set(`lock:${key}`, 'taken', 'EX', ttl, 'NX');
if (!got) return; // 다른 instance
try { await fn(); } finally { await redis.del(`lock:${key}`); }
}
cron.schedule('0 * * * *', async () => {
await withLock('cleanup', 3600, async () => {
await cleanup();
});
});
```
### Inngest (modern, function-as-a-service)
```ts
import { Inngest } from 'inngest';
const inngest = new Inngest({ name: 'My App' });
export const dailyReport = inngest.createFunction(
{ id: 'daily-report' },
{ cron: '0 9 * * *' },
async ({ event, step }) => {
const data = await step.run('fetch', () => fetchData());
await step.run('email', () => sendEmail(data));
}
);
```
→ Step = retry + dedup automatic.
### Trigger.dev (alternative)
```ts
import { client } from './trigger';
client.defineJob({
id: 'daily-report',
trigger: cronTrigger({ cron: '0 9 * * *' }),
run: async (payload, io) => {
const data = await io.runTask('fetch', async () => fetchData());
await io.sendEvent('email', { data });
},
});
```
### Temporal (workflow engine)
```ts
// Schedule
import { Connection, ScheduleClient } from '@temporalio/client';
const client = new ScheduleClient({ connection });
await client.create({
scheduleId: 'daily-cleanup',
spec: { intervals: [{ every: '1 day' }] },
action: {
type: 'startWorkflow',
workflowType: 'cleanupWorkflow',
},
});
```
→ Cron + workflow + retry.
### Sidekiq (Ruby)
```ruby
# Sidekiq scheduler
Sidekiq.configure_server do |config|
config.on(:startup) do
Sidekiq.schedule = YAML.load_file('config/schedule.yml')
Sidekiq::Scheduler.reload_schedule!
end
end
```
```yaml
# schedule.yml
daily_report:
cron: '0 9 * * *'
class: ReportWorker
```
### AWS EventBridge + Lambda
```yaml
# Serverless
functions:
daily:
handler: handler.daily
events:
- schedule:
rate: cron(0 9 * * ? *)
```
→ Managed cron. Lambda 친화.
### GCP Cloud Scheduler
```bash
gcloud scheduler jobs create http daily-report \
--schedule "0 9 * * *" \
--uri https://api.example.com/cron/daily \
--http-method POST
```
→ HTTP trigger.
### Cloudflare Workers Cron
```toml
# wrangler.toml
[triggers]
crons = ["0 9 * * *", "*/5 * * * *"]
```
```ts
export default {
async scheduled(event, env, ctx) {
if (event.cron === '0 9 * * *') await dailyReport();
if (event.cron === '*/5 * * * *') await checkHealth();
},
};
```
→ Edge cron.
### Vercel Cron
```json
// vercel.json
{
"crons": [
{ "path": "/api/cron", "schedule": "0 9 * * *" }
]
}
```
```ts
// app/api/cron/route.ts
export async function GET(request) {
const auth = request.headers.get('authorization');
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
await dailyReport();
return Response.json({ ok: true });
}
```
### Idempotency
```ts
async function cleanup() {
// 매 run 가 idempotent — retry safe.
await db.exec('DELETE FROM logs WHERE created_at < NOW() - INTERVAL 30 DAY');
}
```
→ "다시 실행해도 OK".
### Missed run
```
Server down 1 hour → 1 cron 가 missed.
처리:
1. 무시 (간단).
2. Catch up (다음 run 가 이전 work 도).
3. Manual trigger.
→ Inngest / Temporal 가 missed-run 처리.
```
### Backfill
```ts
// 옛 날짜 cron 재실행
for (const date of getDateRange('2026-04-01', '2026-04-30')) {
await dailyReport({ date });
}
```
### Timezone
```cron
0 9 * * *
# Server timezone (UTC).
```
```ts
// Asia/Seoul = UTC+9
// 9 AM KST = 0 AM UTC
'0 0 * * *' in UTC = 9 AM KST.
```
→ Inngest / Trigger.dev 가 timezone 옵션.
### Frequency limit
```
✓ 매일, 매 시간: 일반.
✗ 매 분: high-volume — queue 더 좋음.
✗ 매 초: bad pattern.
→ "매 분 미만" = queue / streaming.
```
### Job 의 visibility
```
- 매 run 의 status (running / done / failed)
- Duration / retry count
- Last successful run
- Next scheduled
→ Inngest / Trigger.dev / Temporal UI.
```
### DB 의 schedule (own)
```sql
CREATE TABLE scheduled_jobs (
id TEXT PRIMARY KEY,
cron TEXT,
last_run TIMESTAMP,
next_run TIMESTAMP,
status TEXT
);
-- 매 분 worker
SELECT * FROM scheduled_jobs WHERE next_run <= NOW();
-- → 매 job 실행, last_run / next_run update.
```
→ DIY 의 simple version.
### Retry policy
```ts
// BullMQ
queue.add('cleanup', null, {
repeat: { pattern: '0 * * * *' },
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
});
```
### Dependent cron
```
dailyReport 가 dailyClean 후.
→ Workflow (Temporal / Inngest) 가 좋음.
순서 + dependency + retry 가 자동.
```
### Cron alert
```
- 매 run 의 metric (Prometheus).
- "Last run > expected interval" alert.
- Failure count threshold.
→ Datadog / Grafana.
```
### Cost
```
Lambda + EventBridge: $ 작음 ($0.01 / month / cron).
Inngest: $0 (1k step / month free).
Trigger.dev: 비슷.
Vercel: 무료 (Pro 가 1 cron job).
Self-host: server cost.
→ Cron 가 비싼 보다 missing run 가 비싼.
```
### When?
```
Simple (1 server): node-cron.
Multi-instance: BullMQ / Redis lock.
Modern serverless: Inngest / Trigger.dev / Vercel Cron.
Workflow: Temporal.
Cloud: AWS EventBridge / GCP Scheduler.
Edge: Cloudflare Cron.
```
### Real-world
```
Examples:
- Daily report email (9 AM)
- Weekly digest
- Monthly billing
- Cleanup old data (1 AM)
- Health check (매 5 min)
- Sync (매 hour)
```
## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 1 server | node-cron / Linux cron |
| Multi-instance | BullMQ / Redis lock |
| Modern serverless | Inngest / Trigger.dev |
| Workflow | Temporal |
| AWS | EventBridge + Lambda |
| Vercel | Vercel Cron |
| Edge | Cloudflare Cron |
| 매우 frequent | Queue / streaming |
## ❌ 안티패턴
- **No lock (multi-instance)**: 중복.
- **No idempotency**: retry = corrupt.
- **No alert**: silent miss.
- **매 1 sec cron**: queue 사용.
- **Timezone confusion**: 9 AM ≠ 9 AM.
- **No visibility**: blind.
- **Missed run 무시**: data 잃음.
## 🤖 LLM 활용 힌트
- 단순 = node-cron / Linux.
- Modern = Inngest / Trigger.dev (function-as-a-service).
- Multi-instance = BullMQ + lock.
- Workflow = Temporal.
## 🔗 관련 문서
- [[Backend_Cron_Patterns]]
- [[Backend_Job_Queue_Patterns]]
- [[Backend_Job_Scheduling_Temporal]]