[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
---
|
||||
id: backend-cron-workflows-inngest
|
||||
title: Inngest / Trigger.dev — function-as-a-workflow
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, workflow, vibe-coding]
|
||||
tech_stack: { language: "TS", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [Inngest, Trigger.dev, function workflow, durable function, event-driven, cron, retry]
|
||||
---
|
||||
|
||||
# Inngest / Trigger.dev
|
||||
|
||||
> Modern async / cron / workflow. **Code 가 function, retry / step / wait 자동**. Temporal 의 simple version.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Function = workflow.
|
||||
- Step = retry / dedup unit.
|
||||
- Event 가 trigger.
|
||||
- Cron / wait 가 native.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Inngest function
|
||||
```ts
|
||||
import { Inngest } from 'inngest';
|
||||
const inngest = new Inngest({ id: 'my-app' });
|
||||
|
||||
export const dailyReport = inngest.createFunction(
|
||||
{ id: 'daily-report' },
|
||||
{ cron: '0 9 * * *' },
|
||||
async ({ event, step }) => {
|
||||
const data = await step.run('fetch', async () => fetchData());
|
||||
|
||||
await step.sleep('wait', '1m');
|
||||
|
||||
await step.run('email', async () => sendEmail(data));
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
→ 매 step 가 retry / dedup 자동.
|
||||
|
||||
### Event-triggered
|
||||
```ts
|
||||
export const onUserSignup = inngest.createFunction(
|
||||
{ id: 'welcome-email' },
|
||||
{ event: 'user/signup' },
|
||||
async ({ event, step }) => {
|
||||
await step.run('send', () => sendWelcome(event.data.email));
|
||||
|
||||
await step.sleep('wait-3-days', '3d');
|
||||
|
||||
await step.run('check-active', async () => {
|
||||
const user = await getUser(event.data.userId);
|
||||
if (!user.active) await sendReengagement(user);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Trigger
|
||||
await inngest.send({ name: 'user/signup', data: { userId, email } });
|
||||
```
|
||||
|
||||
### Step.sleep (durable wait)
|
||||
```ts
|
||||
await step.sleep('wait', '1d');
|
||||
// → Function 가 종료. 1 day 후 resume.
|
||||
// → Server restart 가 OK (state durable).
|
||||
```
|
||||
|
||||
→ Long-running workflow.
|
||||
|
||||
### Step.waitForEvent
|
||||
```ts
|
||||
const event = await step.waitForEvent('payment-confirmed', {
|
||||
event: 'payment/confirmed',
|
||||
match: 'data.userId',
|
||||
timeout: '1h',
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
// Timeout
|
||||
}
|
||||
```
|
||||
|
||||
→ "다음 event 가 도착 까지 wait".
|
||||
|
||||
### Trigger.dev
|
||||
```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', { name: 'send', payload: { data } });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
→ Inngest 와 매우 비슷.
|
||||
|
||||
### Concurrency limit
|
||||
```ts
|
||||
{ concurrency: { limit: 10 } }
|
||||
// → 10 instance max parallel.
|
||||
```
|
||||
|
||||
### Throttle
|
||||
```ts
|
||||
{ throttle: { limit: 100, period: '1m' } }
|
||||
// → 1 min 안 100 max.
|
||||
```
|
||||
|
||||
### Retry policy
|
||||
```ts
|
||||
inngest.createFunction(
|
||||
{ id: 'task', retries: 5 },
|
||||
...
|
||||
async ({ step }) => {
|
||||
await step.run('flaky', async () => {
|
||||
// Auto retry 5 times.
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Cancel running function
|
||||
```ts
|
||||
{
|
||||
cancelOn: [{ event: 'user/deleted', if: 'event.data.userId == async.data.userId' }]
|
||||
}
|
||||
```
|
||||
|
||||
→ 외부 event 가 cancel.
|
||||
|
||||
### Local dev
|
||||
```bash
|
||||
npx inngest-cli dev
|
||||
# → Dashboard at localhost:8288.
|
||||
```
|
||||
|
||||
→ Function trace + replay + UI.
|
||||
|
||||
### Production deploy
|
||||
```ts
|
||||
// Vercel / Cloudflare / Lambda
|
||||
// Inngest 가 serverless 친화.
|
||||
|
||||
// app/api/inngest/route.ts (Next.js)
|
||||
import { serve } from 'inngest/next';
|
||||
import { dailyReport, onUserSignup } from '../../inngest';
|
||||
|
||||
export const { GET, POST, PUT } = serve({
|
||||
client: inngest,
|
||||
functions: [dailyReport, onUserSignup],
|
||||
});
|
||||
```
|
||||
|
||||
### vs Temporal
|
||||
```
|
||||
Temporal:
|
||||
- 강력 (workflow language).
|
||||
- Self-host 가 가능.
|
||||
- 큰 enterprise.
|
||||
- Steeper.
|
||||
|
||||
Inngest:
|
||||
- TS-friendly.
|
||||
- Managed 친화.
|
||||
- Simple.
|
||||
- 작은 / 중간.
|
||||
|
||||
→ Modern serverless = Inngest.
|
||||
큰 / on-prem = Temporal.
|
||||
```
|
||||
|
||||
### vs BullMQ
|
||||
```
|
||||
BullMQ:
|
||||
- Redis-backed.
|
||||
- Self-host.
|
||||
- 더 raw.
|
||||
|
||||
Inngest:
|
||||
- Managed.
|
||||
- Workflow primitive.
|
||||
- Step / sleep native.
|
||||
|
||||
→ Inngest 가 더 production friendly.
|
||||
```
|
||||
|
||||
### vs AWS Step Functions
|
||||
```
|
||||
Step Functions:
|
||||
- AWS native.
|
||||
- ASL (state machine).
|
||||
- Managed scaling.
|
||||
|
||||
Inngest:
|
||||
- TS code (no DSL).
|
||||
- Multi-cloud.
|
||||
|
||||
→ AWS-only = Step Functions.
|
||||
Multi-cloud / TS = Inngest.
|
||||
```
|
||||
|
||||
### Use case
|
||||
```
|
||||
- Email sequence (welcome → 3 day → 7 day).
|
||||
- LLM agent workflow.
|
||||
- Image processing pipeline.
|
||||
- Daily report.
|
||||
- Subscription billing.
|
||||
- Scheduled cleanup.
|
||||
- Data sync (매 hour).
|
||||
```
|
||||
|
||||
### LLM agent (Inngest)
|
||||
```ts
|
||||
export const agent = inngest.createFunction(
|
||||
{ id: 'agent' },
|
||||
{ event: 'agent/start' },
|
||||
async ({ event, step }) => {
|
||||
let context = event.data.task;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const action = await step.run(`think-${i}`, async () => llm.complete(...));
|
||||
|
||||
if (action.type === 'done') return action.result;
|
||||
|
||||
const result = await step.run(`act-${i}`, async () => execute(action));
|
||||
context += result;
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
→ Multi-step agent + retry / pause / resume.
|
||||
|
||||
### Idempotency
|
||||
```ts
|
||||
{
|
||||
idempotency: 'event.data.orderId',
|
||||
}
|
||||
// → 같은 orderId 의 event 가 1번만.
|
||||
```
|
||||
|
||||
### Batching
|
||||
```ts
|
||||
{
|
||||
batchEvents: { maxSize: 100, timeout: '5s' }
|
||||
}
|
||||
// → 5 sec 또는 100 event 마다 batch.
|
||||
```
|
||||
|
||||
### Cost
|
||||
```
|
||||
Inngest free: 1k step / month.
|
||||
Pro: $20-200 / month.
|
||||
|
||||
Trigger.dev: 비슷.
|
||||
|
||||
→ Self-host alternative 가 BullMQ + cron.
|
||||
```
|
||||
|
||||
### Anti-pattern
|
||||
```
|
||||
- Step 안에 step.run 가 안: nested 안 됨.
|
||||
- Side effect 가 step.run 외: retry 시 중복.
|
||||
- Long step (>30 sec): timeout (function level).
|
||||
- Throw vs return error: 매 다름.
|
||||
```
|
||||
|
||||
### Best practice
|
||||
```
|
||||
1. 매 side effect 가 step.run.
|
||||
2. Idempotent step (key).
|
||||
3. Concurrency / throttle (외부 API rate limit).
|
||||
4. Timeout (긴 wait 도).
|
||||
5. Error monitoring.
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| Modern serverless | Inngest / Trigger.dev |
|
||||
| 큰 / on-prem | Temporal |
|
||||
| Self-host simple | BullMQ |
|
||||
| AWS-only | Step Functions |
|
||||
| LLM agent | Inngest (durable + retry) |
|
||||
| Email sequence | Inngest (sleep) |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Side effect 가 step 외**: retry 중복.
|
||||
- **Long sleep 가 step.sleep 안**: function timeout.
|
||||
- **No idempotency**: replay 시 중복.
|
||||
- **No concurrency limit**: external API 폭발.
|
||||
- **Step 가 nested**: 안 됨.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Inngest = durable function + step.
|
||||
- Cron / event / wait native.
|
||||
- LLM agent 에 강함.
|
||||
- Temporal 의 simple alternative.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Backend_Cron_Scheduler_Patterns]]
|
||||
- [[Backend_Job_Scheduling_Temporal]]
|
||||
- [[Backend_Saga_Choreography_vs_Orchestration]]
|
||||
Reference in New Issue
Block a user