Postgres 기반 백그라운드 작업: 영속 큐, 백오프 재시도, cron 일정, 서버리스 cron 엔드포인트
코드파일 9개컨텍스트약 696토큰검사 통과
설치
$
genpm add @core/jobs포함 내용
- src/lib/jobs/에 소스 코드, 파일 9개. (19.2kB)
- src/lib/jobs/AGENTS.md에 AI 규칙, 그리고 IDE 규칙 파일.
- .env.example에 추가되는 환경 변수: CRON_SECRET.
- @core/db을(를) 자동으로 해결합니다.
README
이 패키지에는 README가 없습니다.
약 696토큰→ src/lib/jobs/AGENTS.md→ .cursor/rules/genpm-core-jobs.mdc
이것이 AI가 src/lib/jobs에서 작업할 때 읽는 내용 그대로입니다. 그 외에는 컨텍스트에 아무것도 추가되지 않습니다.
@core/jobs — rules for AI agents
Purpose
Durable background jobs on the same Postgres: queue (FOR UPDATE SKIP LOCKED), retries with exponential backoff,
timeouts, de-duplication keys, cron schedules (5 fields, with timezone) and an HTTP endpoint so serverless hosts can
run due jobs from their cron. No Redis, no extra service. Table owner of jobs and job_schedules.
Map
index.ts— public API:defineJob,enqueue,schedule,unschedule,runDue,runWorker,retryJob,pruneJobs.queue.ts— claim/run logic.registry.ts— in-memory job types.schema.ts— tables.adapters/hono.ts—cronRoutes().adapters/next.ts—cronRoute(GET/POST).
Integration
- Env:
CRON_SECRET(≥ 32 random chars). Generate and apply migrations (seesrc/lib/db/AGENTS.md). - Define jobs in one module imported by every process (web and worker), e.g.
src/jobs.ts:
Enqueue:import { z } from 'zod'; import { defineJob } from './lib/jobs/index.ts'; export const sendWelcome = defineJob('email.welcome', z.object({ userId: z.string() }), async ({ userId }, { signal }) => { /* … */ });await sendWelcome.enqueue({ userId }, { dedupeKey:welcome:${userId}}). - Run them, one of:
- Serverless (Vercel, Netlify, Workers): mount the cron endpoint (
app.route('/api/cron', cronRoutes())orapp/api/cron/route.tswithexport { cronRoute as GET, cronRoute as POST }) and call it every minute withAuthorization: Bearer $CRON_SECRET(Vercel Cron sends it automatically whenCRON_SECRETis set). - Long-running Node: a worker process calling
runWorker({ concurrency: 4 }).
- Serverless (Vercel, Netlify, Workers): mount the cron endpoint (
- Recurring work:
await schedule('report.daily', '0 8 * * *', { timezone: 'Europe/Madrid' })once at startup. - Verify: enqueue a job, call the cron endpoint, the
jobsrow becomesdone.
Conventions
- Handlers must be idempotent: a job can run more than once (retries, expired locks).
- Expired locks count as attempts: a job whose worker dies on its last attempt is marked
failedwithlastError = 'lock expired'(failExhaustedJobs, run byrunDue) instead of being reclaimed forever. - Each schedule tick is claimed with one conditional
UPDATE(claimScheduleTick), so overlapping cron calls enqueue it once. - Payloads carry IDs and small values; load fresh data inside the handler. Never put secrets in payloads.
- Pass
ctx.signaltofetch()and long operations so timeouts stop them. - Job types are dotted lowercase names owned by a module (
newsletter.send-batch).
Don't
- Don't call slow external APIs (email, suppliers, payments) inside a request when a job fits.
- Don't edit
jobsrows by hand; useretryJobfor failed ones. - Don't expose the cron endpoint without
CRON_SECRET.
# @core/jobs — rules for AI agents
## Purpose
Durable background jobs on the same Postgres: queue (`FOR UPDATE SKIP LOCKED`), retries with exponential backoff,
timeouts, de-duplication keys, cron schedules (5 fields, with timezone) and an HTTP endpoint so serverless hosts can
run due jobs from their cron. No Redis, no extra service. Table owner of `jobs` and `job_schedules`.
## Map
- `index.ts` — public API: `defineJob`, `enqueue`, `schedule`, `unschedule`, `runDue`, `runWorker`, `retryJob`, `pruneJobs`.
- `queue.ts` — claim/run logic. `registry.ts` — in-memory job types. `schema.ts` — tables.
- `adapters/hono.ts` — `cronRoutes()`. `adapters/next.ts` — `cronRoute` (GET/POST).
## Integration
1. Env: `CRON_SECRET` (≥ 32 random chars). Generate and apply migrations (see `src/lib/db/AGENTS.md`).
2. Define jobs in one module imported by every process (web and worker), e.g. `src/jobs.ts`:
```ts
import { z } from 'zod';
import { defineJob } from './lib/jobs/index.ts';
export const sendWelcome = defineJob('email.welcome', z.object({ userId: z.string() }), async ({ userId }, { signal }) => { /* … */ });
```
Enqueue: `await sendWelcome.enqueue({ userId }, { dedupeKey: `welcome:${userId}` })`.
3. Run them, one of:
- Serverless (Vercel, Netlify, Workers): mount the cron endpoint (`app.route('/api/cron', cronRoutes())` or
`app/api/cron/route.ts` with `export { cronRoute as GET, cronRoute as POST }`) and call it every minute with
`Authorization: Bearer $CRON_SECRET` (Vercel Cron sends it automatically when `CRON_SECRET` is set).
- Long-running Node: a worker process calling `runWorker({ concurrency: 4 })`.
4. Recurring work: `await schedule('report.daily', '0 8 * * *', { timezone: 'Europe/Madrid' })` once at startup.
5. Verify: enqueue a job, call the cron endpoint, the `jobs` row becomes `done`.
## Conventions
- Handlers must be idempotent: a job can run more than once (retries, expired locks).
- Expired locks count as attempts: a job whose worker dies on its last attempt is marked `failed` with
`lastError = 'lock expired'` (`failExhaustedJobs`, run by `runDue`) instead of being reclaimed forever.
- Each schedule tick is claimed with one conditional `UPDATE` (`claimScheduleTick`), so overlapping cron calls enqueue it once.
- Payloads carry IDs and small values; load fresh data inside the handler. Never put secrets in payloads.
- Pass `ctx.signal` to `fetch()` and long operations so timeouts stop them.
- Job types are dotted lowercase names owned by a module (`newsletter.send-batch`).
## Don't
- Don't call slow external APIs (email, suppliers, payments) inside a request when a job fits.
- Don't edit `jobs` rows by hand; use `retryJob` for failed ones.
- Don't expose the cron endpoint without `CRON_SECRET`.
.genpmignore 적용 후 주입될 정확한 트리입니다. 고정 대상:
// Protección del endpoint de cron: `Authorization: Bearer <CRON_SECRET>` comparado en tiempo constante.
export function isAuthorizedCron(authorization: string | null | undefined, secret = process.env.CRON_SECRET): boolean {
if (!secret || secret.length < 32) throw new Error('CRON_SECRET must be set (>= 32 chars)');
const given = new TextEncoder().encode(authorization ?? '');
const expected = new TextEncoder().encode(`Bearer ${secret}`);
let diff = given.length ^ expected.length;
for (let i = 0; i < expected.length; i++) diff |= (given[i] ?? 0) ^ expected[i]!;
return diff === 0;
}
이 패키지는 MCP 서버를 선언하지 않습니다.
| 버전 | 커밋 | 게시일 | 검사 |
|---|---|---|---|
| 1.1.0 | eef017d | 4시간 전 | 검사 통과 |
- genpm
- @core/db ^1.0.0
- 제안됨
- GenPM은 npm 명령을 제안하고, 동의한 경우에만 실행합니다.
- 검사
- 검사 통과 · 문제 0건
- 커밋
- v1.1.0 → eef017d6b6e8036d93cc1a5e7941cb9dcc10e15f · 가져온 뒤 검증됨
- 스크립트
- 없음. GenPM은 패키지 코드를 절대 실행하지 않습니다.
- 라이선스
- MIT
- 품질
- 100/100
- 인정된 라이선스충족
- AGENTS.md에 목적 설명충족
- AGENTS.md에 통합 단계충족
- AGENTS.md에 규칙 또는 금지 사항충족
- 테스트 포함충족
- 보안 검사 통과충족
- 최근 6개월 내 게시충족
- 인증된 게시자충족
- 요약과 키워드충족
- 신고
- 문제가 있나요?