@luanpdd/kit-mcp 1.35.0 → 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +2 -2
- package/bin/mcp.js +6 -6
- package/bin/ui.js +74 -74
- package/gates/ai-prompt-stability.md +120 -120
- package/gates/budget-description.md +68 -68
- package/gates/confidence.md +29 -29
- package/gates/dependency-check.md +33 -33
- package/gates/dept-cycle-prevention.md +179 -179
- package/gates/golden-signals-coverage.md +133 -133
- package/gates/legacy-refactor-safety.md +178 -178
- package/gates/multi-tenant-rls-coverage.md +102 -102
- package/gates/no-personal-uuid.md +72 -72
- package/gates/obs-agents-mcp-supabase.md +86 -86
- package/gates/obs-skills-frontmatter.md +76 -76
- package/gates/observability-coverage.md +151 -151
- package/gates/omm-no-regression.md +83 -83
- package/gates/postmortem-template-required.md +127 -127
- package/gates/prr-checklist-coverage.md +128 -128
- package/gates/regression.md +32 -32
- package/gates/release-pipeline-policy.md +132 -132
- package/gates/secrets-scan.md +33 -33
- package/gates/service-role-not-in-user-facing.md +113 -113
- package/gates/skill-must-include.md +71 -71
- package/gates/sync-idempotent.md +62 -62
- package/gates/verify-phase-goal.md +34 -34
- package/kit/agents/designer-ui.md +216 -216
- package/kit/agents/workflow-generator.md +537 -167
- package/kit/commands/adicionar-backlog.md +1 -1
- package/kit/commands/adicionar-fase.md +1 -1
- package/kit/commands/adicionar-tarefa.md +1 -1
- package/kit/commands/auditar-observabilidade.md +103 -103
- package/kit/commands/auditar-toil.md +129 -129
- package/kit/commands/caracterizar-prompt.md +195 -195
- package/kit/commands/criar-workflow.md +158 -158
- package/kit/commands/definir-perfil.md +1 -1
- package/kit/commands/definir-slo.md +108 -108
- package/kit/commands/fio.md +1 -1
- package/kit/commands/golden-signals.md +142 -142
- package/kit/commands/instrumentar-fase.md +200 -200
- package/kit/commands/investigar-producao.md +162 -162
- package/kit/commands/observabilidade.md +118 -118
- package/kit/commands/postmortem.md +179 -179
- package/kit/commands/prr.md +205 -205
- package/kit/commands/publicar-rapido.md +207 -207
- package/kit/commands/risk-budget.md +220 -220
- package/kit/commands/sre.md +230 -230
- package/kit/file-manifest.json +424 -424
- package/kit/framework/references/output-style.md +22 -22
- package/kit/hooks/post-apply-migration.js +199 -199
- package/kit/hooks/sidecar-tool-publisher.js +210 -210
- package/kit/skills/_shared-dados-distribuidos/glossary.md +224 -224
- package/kit/skills/_shared-legacy/glossary.md +389 -389
- package/kit/skills/_shared-multi-tenant/glossary.md +186 -186
- package/kit/skills/_shared-observability/glossary.md +396 -396
- package/kit/skills/_shared-sre/glossary.md +712 -712
- package/kit/skills/_shared-supabase/glossary.md +234 -234
- package/kit/skills/blameless-postmortems/SKILL.md +340 -340
- package/kit/skills/burn-rate-alerting/SKILL.md +258 -258
- package/kit/skills/cascading-failures/SKILL.md +311 -311
- package/kit/skills/core-analysis-loop/SKILL.md +352 -352
- package/kit/skills/distributed-tracing/SKILL.md +362 -362
- package/kit/skills/dynamic-workflow-authoring/SKILL.md +327 -223
- package/kit/skills/eliminating-toil/SKILL.md +243 -243
- package/kit/skills/event-based-slos/SKILL.md +296 -296
- package/kit/skills/four-golden-signals/SKILL.md +314 -314
- package/kit/skills/hermetic-builds/SKILL.md +323 -323
- package/kit/skills/legacy-monster-methods/SKILL.md +444 -444
- package/kit/skills/llm-as-dependency/SKILL.md +436 -436
- package/kit/skills/load-shedding-graceful-degradation/SKILL.md +396 -396
- package/kit/skills/observability-driven-development/SKILL.md +315 -315
- package/kit/skills/observability-maturity-model/SKILL.md +222 -222
- package/kit/skills/opentelemetry-standard/SKILL.md +351 -351
- package/kit/skills/production-readiness-review/SKILL.md +305 -305
- package/kit/skills/release-engineering/SKILL.md +367 -367
- package/kit/skills/retry-strategies/SKILL.md +372 -372
- package/kit/skills/sre-risk-management/SKILL.md +221 -221
- package/kit/skills/structured-events/SKILL.md +265 -265
- package/kit/skills/supabase-cron-queues/SKILL.md +275 -275
- package/kit/skills/supabase-database-functions/SKILL.md +332 -332
- package/kit/skills/supabase-declarative-schema/SKILL.md +183 -183
- package/kit/skills/supabase-pgvector-rag/SKILL.md +253 -253
- package/kit/skills/supabase-postgres-style/SKILL.md +138 -138
- package/kit/skills/supabase-storage/SKILL.md +234 -234
- package/kit/skills/telemetry-pipelines/SKILL.md +259 -259
- package/kit/skills/telemetry-sampling/SKILL.md +256 -256
- package/kit/skills/ui-anti-padroes-ia/SKILL.md +261 -261
- package/kit/skills/ui-contexto-produto/SKILL.md +248 -248
- package/kit/skills/ui-cor-estrategia/SKILL.md +213 -213
- package/kit/skills/ui-critica-auditoria/SKILL.md +260 -260
- package/kit/skills/ui-motion-funcional/SKILL.md +264 -264
- package/kit/skills/ui-ritmo-espacial/SKILL.md +259 -259
- package/kit/skills/ui-tipografia/SKILL.md +211 -211
- package/package.json +1 -1
- package/src/cli/index.js +1114 -1114
- package/src/cli/render.js +194 -194
- package/src/cli/upgrade-check.js +135 -135
- package/src/core/error-redaction.js +76 -76
- package/src/core/failures.js +153 -153
- package/src/core/gate-runner.js +205 -205
- package/src/core/gates.js +82 -82
- package/src/core/logger.js +170 -170
- package/src/core/manifest-verify.js +174 -174
- package/src/core/metrics.js +268 -268
- package/src/core/notify.js +60 -60
- package/src/core/path-safety.js +141 -141
- package/src/core/replays.js +120 -120
- package/src/core/ui.js +185 -185
- package/src/mcp-server/install.js +149 -149
- package/src/mcp-server/roots.js +124 -124
- package/src/ui/auto-spawn.js +113 -113
- package/src/ui/browser.js +78 -78
- package/src/ui/client.js +130 -130
- package/src/ui/events.js +65 -65
- package/src/ui/lockfile.js +191 -191
- package/src/ui/port.js +67 -67
- package/src/ui/server.js +547 -547
- package/src/ui/wrapper.js +129 -129
|
@@ -1,275 +1,275 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: supabase-cron-queues
|
|
3
|
-
description: Use ao orquestrar background jobs — pg_cron + pgmq + pg_net pattern cron → pgmq → Edge Function. Sem dep externa. Postgres 15.6.1.143+.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Supabase — Cron + Queues (background jobs)
|
|
7
|
-
|
|
8
|
-
## Quando usar
|
|
9
|
-
|
|
10
|
-
LLM carrega esta skill quando implementar background jobs, scheduled tasks ou queues em Supabase **sem dependência externa** (Inngest, Trigger.dev, etc.). Trigger phrases:
|
|
11
|
-
|
|
12
|
-
- "pg_cron", "supabase cron job"
|
|
13
|
-
- "pgmq", "Postgres Message Queue"
|
|
14
|
-
- "pg_net", "HTTP from database"
|
|
15
|
-
- "background job Supabase"
|
|
16
|
-
- "scheduled task Supabase"
|
|
17
|
-
|
|
18
|
-
## Regras absolutas
|
|
19
|
-
|
|
20
|
-
- **Extensions necessárias:**
|
|
21
|
-
- **`pg_cron`** — jobs scheduled (cron syntax)
|
|
22
|
-
- **`pgmq`** — Postgres Message Queue (requer Postgres **15.6.1.143+**)
|
|
23
|
-
- **`pg_net`** — HTTP requests do banco (recomendado v0.10.0+)
|
|
24
|
-
- **Pattern canônico:** **`cron → pgmq → Edge Function`** — `pg_cron` enfileira mensagens em `pgmq`, Edge Function consome (via cron ou polling).
|
|
25
|
-
- **Jobs `pg_cron` curtos** (< 10 min) — jobs longos bloqueiam scheduler. Para jobs longos, enfileire em `pgmq` e processe via Edge Function.
|
|
26
|
-
- **`pgmq.send`** para enfileirar; **`pgmq.read` + `pgmq.archive`** para consumir. Visibility timeout previne double processing.
|
|
27
|
-
- **`pg_net` é async** — `net.http_post` retorna `request_id`, response chega em `net._http_response`. Não bloqueia caller.
|
|
28
|
-
- **Idempotência** — Edge Function consumer deve ser idempotente (mesma mensagem pode ser entregue 2× em retry).
|
|
29
|
-
- **Cleanup** — sem `pgmq.archive` ou `pgmq.delete`, mensagem reaparece após visibility timeout (re-processed).
|
|
30
|
-
|
|
31
|
-
## Patterns canônicos
|
|
32
|
-
|
|
33
|
-
### Setup das extensions + criar fila
|
|
34
|
-
|
|
35
|
-
```sql
|
|
36
|
-
-- PT-BR: habilitar extensions (uma vez por projeto)
|
|
37
|
-
create extension if not exists pg_cron;
|
|
38
|
-
create extension if not exists pgmq;
|
|
39
|
-
create extension if not exists pg_net;
|
|
40
|
-
|
|
41
|
-
-- PT-BR: criar fila pgmq
|
|
42
|
-
select pgmq.create('email_jobs');
|
|
43
|
-
|
|
44
|
-
-- PT-BR: opcional — criar fila com retention customizado
|
|
45
|
-
-- select pgmq.create_partitioned('large_jobs');
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Pattern canônico — `cron → pgmq → Edge Function`
|
|
49
|
-
|
|
50
|
-
```sql
|
|
51
|
-
-- PT-BR: 1. cron job a cada 5 min enfileira pendências em pgmq
|
|
52
|
-
select cron.schedule(
|
|
53
|
-
'enqueue-pending-emails',
|
|
54
|
-
'*/5 * * * *', -- a cada 5 min
|
|
55
|
-
$$
|
|
56
|
-
insert into pgmq.q_email_jobs (message)
|
|
57
|
-
select jsonb_build_object(
|
|
58
|
-
'user_id', id,
|
|
59
|
-
'kind', 'reminder',
|
|
60
|
-
'enqueued_at', now()
|
|
61
|
-
)
|
|
62
|
-
from public.users
|
|
63
|
-
where pending_email = true
|
|
64
|
-
limit 1000; -- batch limitado
|
|
65
|
-
$$
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
-- PT-BR: 2. cron job a cada minuto despara processamento via Edge Function
|
|
69
|
-
select cron.schedule(
|
|
70
|
-
'process-email-queue',
|
|
71
|
-
'*/1 * * * *', -- a cada minuto
|
|
72
|
-
$$
|
|
73
|
-
select net.http_post(
|
|
74
|
-
url := 'https://<project-ref>.supabase.co/functions/v1/process-emails',
|
|
75
|
-
headers := jsonb_build_object(
|
|
76
|
-
'Content-Type', 'application/json',
|
|
77
|
-
'Authorization', 'Bearer ' || current_setting('supabase.functions_token', true)
|
|
78
|
-
),
|
|
79
|
-
body := '{}'::jsonb
|
|
80
|
-
);
|
|
81
|
-
$$
|
|
82
|
-
);
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Edge Function consumer — pgmq.read + archive
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
// supabase/functions/process-emails/index.ts
|
|
89
|
-
// PT-BR: consume da fila pgmq, processa, archive
|
|
90
|
-
import { createClient } from 'npm:@supabase/supabase-js@2'
|
|
91
|
-
|
|
92
|
-
Deno.serve(async () => {
|
|
93
|
-
const supabase = createClient(
|
|
94
|
-
Deno.env.get('SUPABASE_URL')!,
|
|
95
|
-
Deno.env.get('SUPABASE_SECRET_KEYS')!
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
// PT-BR: pegar até 10 mensagens com visibility timeout 30s
|
|
99
|
-
const { data: msgs, error } = await supabase.rpc('pgmq_read', {
|
|
100
|
-
queue_name: 'email_jobs',
|
|
101
|
-
vt: 30, // visibility timeout em segundos
|
|
102
|
-
qty: 10, // máximo por chamada
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
if (error || !msgs?.length) {
|
|
106
|
-
return new Response('no jobs', { status: 200 })
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const m of msgs) {
|
|
110
|
-
try {
|
|
111
|
-
// PT-BR: processar mensagem (idempotente!)
|
|
112
|
-
await sendEmail(m.message.user_id, m.message.kind)
|
|
113
|
-
|
|
114
|
-
// PT-BR: archive remove da fila e move para arquivo
|
|
115
|
-
await supabase.rpc('pgmq_archive', {
|
|
116
|
-
queue_name: 'email_jobs',
|
|
117
|
-
msg_id: m.msg_id,
|
|
118
|
-
})
|
|
119
|
-
} catch (err) {
|
|
120
|
-
// PT-BR: erro — não archive; visibility timeout expira e mensagem reaparece
|
|
121
|
-
console.error('processing error', m.msg_id, err)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return new Response(`processed ${msgs.length}`)
|
|
126
|
-
})
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Job cron simples — sem queue (cuidado: < 10 min)
|
|
130
|
-
|
|
131
|
-
```sql
|
|
132
|
-
-- PT-BR: ok para tarefas leves e rápidas (cleanup, agregação)
|
|
133
|
-
select cron.schedule(
|
|
134
|
-
'cleanup-old-sessions',
|
|
135
|
-
'0 3 * * *', -- 3am diário
|
|
136
|
-
$$
|
|
137
|
-
delete from public.sessions where expires_at < now() - interval '30 days';
|
|
138
|
-
$$
|
|
139
|
-
);
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### Listar e remover jobs cron
|
|
143
|
-
|
|
144
|
-
```sql
|
|
145
|
-
-- PT-BR: listar todos os jobs
|
|
146
|
-
select * from cron.job;
|
|
147
|
-
|
|
148
|
-
-- PT-BR: remover job
|
|
149
|
-
select cron.unschedule('process-email-queue');
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### `pg_net.http_post` async
|
|
153
|
-
|
|
154
|
-
```sql
|
|
155
|
-
-- PT-BR: dispara HTTP request, retorna request_id imediatamente
|
|
156
|
-
select net.http_post(
|
|
157
|
-
url := 'https://api.example.com/webhook',
|
|
158
|
-
headers := jsonb_build_object('Authorization', 'Bearer xxx'),
|
|
159
|
-
body := jsonb_build_object('event', 'task_completed'),
|
|
160
|
-
timeout_milliseconds := 5000
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
-- PT-BR: response chega em net._http_response (consultar depois)
|
|
164
|
-
select * from net._http_response order by created desc limit 10;
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Anti-patterns
|
|
168
|
-
|
|
169
|
-
### Anti-pattern 1: Job cron longo (> 10 min)
|
|
170
|
-
|
|
171
|
-
**Errado:**
|
|
172
|
-
```sql
|
|
173
|
-
select cron.schedule(
|
|
174
|
-
'heavy-batch-process',
|
|
175
|
-
'0 * * * *',
|
|
176
|
-
$$ select pg_sleep(900); ... $$ -- ⚠ 15 min em pg_cron
|
|
177
|
-
);
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
**Por quê:** `pg_cron` worker bloqueia outros jobs enquanto roda. Se job > 10 min ou trava, scheduler atrasa cascata. Em retry após failure, pode trancar inteiramente.
|
|
181
|
-
|
|
182
|
-
**Certo:** cron enfileira; Edge Function processa pesado:
|
|
183
|
-
```sql
|
|
184
|
-
-- cron: leve (só enfileira)
|
|
185
|
-
select cron.schedule('enqueue-heavy', '0 * * * *', $$
|
|
186
|
-
insert into pgmq.q_heavy_jobs (message) select ...;
|
|
187
|
-
$$);
|
|
188
|
-
-- Edge Function: pesado (consome com timeout próprio)
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Anti-pattern 2: HTTP síncrono direto de pg_cron
|
|
192
|
-
|
|
193
|
-
**Errado:**
|
|
194
|
-
```sql
|
|
195
|
-
select cron.schedule('call-api', '*/1 * * * *', $$
|
|
196
|
-
-- ⚠ pg_net é async, mas user pode tentar sync com loops
|
|
197
|
-
select net.http_get('https://api.example.com/long');
|
|
198
|
-
$$);
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**Por quê:** HTTP requests podem demorar segundos a minutos. Se response demora, próxima execução do cron empilha. Em alta latência, scheduler fica trancado.
|
|
202
|
-
|
|
203
|
-
**Certo:** enfileire em pgmq + Edge Function processa:
|
|
204
|
-
```sql
|
|
205
|
-
-- cron: enfileira
|
|
206
|
-
insert into pgmq.q_api_calls (message) values ('{"endpoint": "/long"}');
|
|
207
|
-
-- Edge Function: chama API com timeout próprio + archive
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Anti-pattern 3: `pgmq.read` sem `archive` ou `delete`
|
|
211
|
-
|
|
212
|
-
**Errado:**
|
|
213
|
-
```ts
|
|
214
|
-
const { data: msgs } = await supabase.rpc('pgmq_read', { queue_name: 'jobs', vt: 30, qty: 10 })
|
|
215
|
-
for (const m of msgs) {
|
|
216
|
-
await processJob(m.message)
|
|
217
|
-
// ⚠ esqueceu pgmq_archive
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
**Por quê:** após visibility timeout (30s), mensagem reaparece — mesmo job rodado novamente. Em loop, leva a re-processing infinito.
|
|
222
|
-
|
|
223
|
-
**Certo:**
|
|
224
|
-
```ts
|
|
225
|
-
for (const m of msgs) {
|
|
226
|
-
try {
|
|
227
|
-
await processJob(m.message)
|
|
228
|
-
await supabase.rpc('pgmq_archive', { queue_name: 'jobs', msg_id: m.msg_id })
|
|
229
|
-
} catch (err) {
|
|
230
|
-
// PT-BR: NÃO archive; mensagem retorna após vt para retry
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Anti-pattern 4: Edge Function não-idempotente
|
|
236
|
-
|
|
237
|
-
**Errado:**
|
|
238
|
-
```ts
|
|
239
|
-
async function processJob(msg) {
|
|
240
|
-
await sendEmail(msg.user_id) // ⚠ envia email mesmo se já enviado
|
|
241
|
-
await chargeCard(msg.amount) // ⚠ cobra mesmo se já cobrado
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
**Por quê:** retries entregam mesma mensagem 2×+. Sem idempotência, side effects duplicam — usuário recebe 2 emails ou é cobrado 2×.
|
|
246
|
-
|
|
247
|
-
**Certo:** rastreie estado:
|
|
248
|
-
```ts
|
|
249
|
-
async function processJob(msg) {
|
|
250
|
-
const { data: existing } = await supabase
|
|
251
|
-
.from('email_log')
|
|
252
|
-
.select('id')
|
|
253
|
-
.eq('msg_id', msg.id)
|
|
254
|
-
.single()
|
|
255
|
-
if (existing) return // já processado
|
|
256
|
-
await sendEmail(msg.user_id)
|
|
257
|
-
await supabase.from('email_log').insert({ msg_id: msg.id })
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
## Padrões Exactly-Once em pgmq (v1.22+)
|
|
262
|
-
|
|
263
|
-
> Background jobs em pgmq tendem a duplicate processing em retry/timeout. Padrão canônico (DDIA Ch 11):
|
|
264
|
-
> 1. **Dedup table** com `unique(event_id)` — INSERT antes do processamento; falha = já processado.
|
|
265
|
-
> 2. **Idempotency key** no handler — mesmo input → mesmo output (sem efeitos colaterais).
|
|
266
|
-
> 3. **Transactional outbox** — write DB + event em mesma transação atomic; processador async lê outbox e publica.
|
|
267
|
-
>
|
|
268
|
-
> Detalhes completos em [`streams-eventos-cdc`](../streams-eventos-cdc/SKILL.md) (v1.22).
|
|
269
|
-
|
|
270
|
-
## Ver também
|
|
271
|
-
|
|
272
|
-
- [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions consumindo pgmq
|
|
273
|
-
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''` chamadas em cron
|
|
274
|
-
- [supabase-migrations](../supabase-migrations/SKILL.md) — extensions criadas em migrations
|
|
275
|
-
- [glossário](../_shared-supabase/glossary.md) — pg_cron, pgmq, pg_net
|
|
1
|
+
---
|
|
2
|
+
name: supabase-cron-queues
|
|
3
|
+
description: Use ao orquestrar background jobs — pg_cron + pgmq + pg_net pattern cron → pgmq → Edge Function. Sem dep externa. Postgres 15.6.1.143+.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Cron + Queues (background jobs)
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando implementar background jobs, scheduled tasks ou queues em Supabase **sem dependência externa** (Inngest, Trigger.dev, etc.). Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "pg_cron", "supabase cron job"
|
|
13
|
+
- "pgmq", "Postgres Message Queue"
|
|
14
|
+
- "pg_net", "HTTP from database"
|
|
15
|
+
- "background job Supabase"
|
|
16
|
+
- "scheduled task Supabase"
|
|
17
|
+
|
|
18
|
+
## Regras absolutas
|
|
19
|
+
|
|
20
|
+
- **Extensions necessárias:**
|
|
21
|
+
- **`pg_cron`** — jobs scheduled (cron syntax)
|
|
22
|
+
- **`pgmq`** — Postgres Message Queue (requer Postgres **15.6.1.143+**)
|
|
23
|
+
- **`pg_net`** — HTTP requests do banco (recomendado v0.10.0+)
|
|
24
|
+
- **Pattern canônico:** **`cron → pgmq → Edge Function`** — `pg_cron` enfileira mensagens em `pgmq`, Edge Function consome (via cron ou polling).
|
|
25
|
+
- **Jobs `pg_cron` curtos** (< 10 min) — jobs longos bloqueiam scheduler. Para jobs longos, enfileire em `pgmq` e processe via Edge Function.
|
|
26
|
+
- **`pgmq.send`** para enfileirar; **`pgmq.read` + `pgmq.archive`** para consumir. Visibility timeout previne double processing.
|
|
27
|
+
- **`pg_net` é async** — `net.http_post` retorna `request_id`, response chega em `net._http_response`. Não bloqueia caller.
|
|
28
|
+
- **Idempotência** — Edge Function consumer deve ser idempotente (mesma mensagem pode ser entregue 2× em retry).
|
|
29
|
+
- **Cleanup** — sem `pgmq.archive` ou `pgmq.delete`, mensagem reaparece após visibility timeout (re-processed).
|
|
30
|
+
|
|
31
|
+
## Patterns canônicos
|
|
32
|
+
|
|
33
|
+
### Setup das extensions + criar fila
|
|
34
|
+
|
|
35
|
+
```sql
|
|
36
|
+
-- PT-BR: habilitar extensions (uma vez por projeto)
|
|
37
|
+
create extension if not exists pg_cron;
|
|
38
|
+
create extension if not exists pgmq;
|
|
39
|
+
create extension if not exists pg_net;
|
|
40
|
+
|
|
41
|
+
-- PT-BR: criar fila pgmq
|
|
42
|
+
select pgmq.create('email_jobs');
|
|
43
|
+
|
|
44
|
+
-- PT-BR: opcional — criar fila com retention customizado
|
|
45
|
+
-- select pgmq.create_partitioned('large_jobs');
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Pattern canônico — `cron → pgmq → Edge Function`
|
|
49
|
+
|
|
50
|
+
```sql
|
|
51
|
+
-- PT-BR: 1. cron job a cada 5 min enfileira pendências em pgmq
|
|
52
|
+
select cron.schedule(
|
|
53
|
+
'enqueue-pending-emails',
|
|
54
|
+
'*/5 * * * *', -- a cada 5 min
|
|
55
|
+
$$
|
|
56
|
+
insert into pgmq.q_email_jobs (message)
|
|
57
|
+
select jsonb_build_object(
|
|
58
|
+
'user_id', id,
|
|
59
|
+
'kind', 'reminder',
|
|
60
|
+
'enqueued_at', now()
|
|
61
|
+
)
|
|
62
|
+
from public.users
|
|
63
|
+
where pending_email = true
|
|
64
|
+
limit 1000; -- batch limitado
|
|
65
|
+
$$
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
-- PT-BR: 2. cron job a cada minuto despara processamento via Edge Function
|
|
69
|
+
select cron.schedule(
|
|
70
|
+
'process-email-queue',
|
|
71
|
+
'*/1 * * * *', -- a cada minuto
|
|
72
|
+
$$
|
|
73
|
+
select net.http_post(
|
|
74
|
+
url := 'https://<project-ref>.supabase.co/functions/v1/process-emails',
|
|
75
|
+
headers := jsonb_build_object(
|
|
76
|
+
'Content-Type', 'application/json',
|
|
77
|
+
'Authorization', 'Bearer ' || current_setting('supabase.functions_token', true)
|
|
78
|
+
),
|
|
79
|
+
body := '{}'::jsonb
|
|
80
|
+
);
|
|
81
|
+
$$
|
|
82
|
+
);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Edge Function consumer — pgmq.read + archive
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// supabase/functions/process-emails/index.ts
|
|
89
|
+
// PT-BR: consume da fila pgmq, processa, archive
|
|
90
|
+
import { createClient } from 'npm:@supabase/supabase-js@2'
|
|
91
|
+
|
|
92
|
+
Deno.serve(async () => {
|
|
93
|
+
const supabase = createClient(
|
|
94
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
95
|
+
Deno.env.get('SUPABASE_SECRET_KEYS')!
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// PT-BR: pegar até 10 mensagens com visibility timeout 30s
|
|
99
|
+
const { data: msgs, error } = await supabase.rpc('pgmq_read', {
|
|
100
|
+
queue_name: 'email_jobs',
|
|
101
|
+
vt: 30, // visibility timeout em segundos
|
|
102
|
+
qty: 10, // máximo por chamada
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (error || !msgs?.length) {
|
|
106
|
+
return new Response('no jobs', { status: 200 })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const m of msgs) {
|
|
110
|
+
try {
|
|
111
|
+
// PT-BR: processar mensagem (idempotente!)
|
|
112
|
+
await sendEmail(m.message.user_id, m.message.kind)
|
|
113
|
+
|
|
114
|
+
// PT-BR: archive remove da fila e move para arquivo
|
|
115
|
+
await supabase.rpc('pgmq_archive', {
|
|
116
|
+
queue_name: 'email_jobs',
|
|
117
|
+
msg_id: m.msg_id,
|
|
118
|
+
})
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// PT-BR: erro — não archive; visibility timeout expira e mensagem reaparece
|
|
121
|
+
console.error('processing error', m.msg_id, err)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return new Response(`processed ${msgs.length}`)
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Job cron simples — sem queue (cuidado: < 10 min)
|
|
130
|
+
|
|
131
|
+
```sql
|
|
132
|
+
-- PT-BR: ok para tarefas leves e rápidas (cleanup, agregação)
|
|
133
|
+
select cron.schedule(
|
|
134
|
+
'cleanup-old-sessions',
|
|
135
|
+
'0 3 * * *', -- 3am diário
|
|
136
|
+
$$
|
|
137
|
+
delete from public.sessions where expires_at < now() - interval '30 days';
|
|
138
|
+
$$
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Listar e remover jobs cron
|
|
143
|
+
|
|
144
|
+
```sql
|
|
145
|
+
-- PT-BR: listar todos os jobs
|
|
146
|
+
select * from cron.job;
|
|
147
|
+
|
|
148
|
+
-- PT-BR: remover job
|
|
149
|
+
select cron.unschedule('process-email-queue');
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `pg_net.http_post` async
|
|
153
|
+
|
|
154
|
+
```sql
|
|
155
|
+
-- PT-BR: dispara HTTP request, retorna request_id imediatamente
|
|
156
|
+
select net.http_post(
|
|
157
|
+
url := 'https://api.example.com/webhook',
|
|
158
|
+
headers := jsonb_build_object('Authorization', 'Bearer xxx'),
|
|
159
|
+
body := jsonb_build_object('event', 'task_completed'),
|
|
160
|
+
timeout_milliseconds := 5000
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
-- PT-BR: response chega em net._http_response (consultar depois)
|
|
164
|
+
select * from net._http_response order by created desc limit 10;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Anti-patterns
|
|
168
|
+
|
|
169
|
+
### Anti-pattern 1: Job cron longo (> 10 min)
|
|
170
|
+
|
|
171
|
+
**Errado:**
|
|
172
|
+
```sql
|
|
173
|
+
select cron.schedule(
|
|
174
|
+
'heavy-batch-process',
|
|
175
|
+
'0 * * * *',
|
|
176
|
+
$$ select pg_sleep(900); ... $$ -- ⚠ 15 min em pg_cron
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Por quê:** `pg_cron` worker bloqueia outros jobs enquanto roda. Se job > 10 min ou trava, scheduler atrasa cascata. Em retry após failure, pode trancar inteiramente.
|
|
181
|
+
|
|
182
|
+
**Certo:** cron enfileira; Edge Function processa pesado:
|
|
183
|
+
```sql
|
|
184
|
+
-- cron: leve (só enfileira)
|
|
185
|
+
select cron.schedule('enqueue-heavy', '0 * * * *', $$
|
|
186
|
+
insert into pgmq.q_heavy_jobs (message) select ...;
|
|
187
|
+
$$);
|
|
188
|
+
-- Edge Function: pesado (consome com timeout próprio)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Anti-pattern 2: HTTP síncrono direto de pg_cron
|
|
192
|
+
|
|
193
|
+
**Errado:**
|
|
194
|
+
```sql
|
|
195
|
+
select cron.schedule('call-api', '*/1 * * * *', $$
|
|
196
|
+
-- ⚠ pg_net é async, mas user pode tentar sync com loops
|
|
197
|
+
select net.http_get('https://api.example.com/long');
|
|
198
|
+
$$);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Por quê:** HTTP requests podem demorar segundos a minutos. Se response demora, próxima execução do cron empilha. Em alta latência, scheduler fica trancado.
|
|
202
|
+
|
|
203
|
+
**Certo:** enfileire em pgmq + Edge Function processa:
|
|
204
|
+
```sql
|
|
205
|
+
-- cron: enfileira
|
|
206
|
+
insert into pgmq.q_api_calls (message) values ('{"endpoint": "/long"}');
|
|
207
|
+
-- Edge Function: chama API com timeout próprio + archive
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Anti-pattern 3: `pgmq.read` sem `archive` ou `delete`
|
|
211
|
+
|
|
212
|
+
**Errado:**
|
|
213
|
+
```ts
|
|
214
|
+
const { data: msgs } = await supabase.rpc('pgmq_read', { queue_name: 'jobs', vt: 30, qty: 10 })
|
|
215
|
+
for (const m of msgs) {
|
|
216
|
+
await processJob(m.message)
|
|
217
|
+
// ⚠ esqueceu pgmq_archive
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Por quê:** após visibility timeout (30s), mensagem reaparece — mesmo job rodado novamente. Em loop, leva a re-processing infinito.
|
|
222
|
+
|
|
223
|
+
**Certo:**
|
|
224
|
+
```ts
|
|
225
|
+
for (const m of msgs) {
|
|
226
|
+
try {
|
|
227
|
+
await processJob(m.message)
|
|
228
|
+
await supabase.rpc('pgmq_archive', { queue_name: 'jobs', msg_id: m.msg_id })
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// PT-BR: NÃO archive; mensagem retorna após vt para retry
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Anti-pattern 4: Edge Function não-idempotente
|
|
236
|
+
|
|
237
|
+
**Errado:**
|
|
238
|
+
```ts
|
|
239
|
+
async function processJob(msg) {
|
|
240
|
+
await sendEmail(msg.user_id) // ⚠ envia email mesmo se já enviado
|
|
241
|
+
await chargeCard(msg.amount) // ⚠ cobra mesmo se já cobrado
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Por quê:** retries entregam mesma mensagem 2×+. Sem idempotência, side effects duplicam — usuário recebe 2 emails ou é cobrado 2×.
|
|
246
|
+
|
|
247
|
+
**Certo:** rastreie estado:
|
|
248
|
+
```ts
|
|
249
|
+
async function processJob(msg) {
|
|
250
|
+
const { data: existing } = await supabase
|
|
251
|
+
.from('email_log')
|
|
252
|
+
.select('id')
|
|
253
|
+
.eq('msg_id', msg.id)
|
|
254
|
+
.single()
|
|
255
|
+
if (existing) return // já processado
|
|
256
|
+
await sendEmail(msg.user_id)
|
|
257
|
+
await supabase.from('email_log').insert({ msg_id: msg.id })
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Padrões Exactly-Once em pgmq (v1.22+)
|
|
262
|
+
|
|
263
|
+
> Background jobs em pgmq tendem a duplicate processing em retry/timeout. Padrão canônico (DDIA Ch 11):
|
|
264
|
+
> 1. **Dedup table** com `unique(event_id)` — INSERT antes do processamento; falha = já processado.
|
|
265
|
+
> 2. **Idempotency key** no handler — mesmo input → mesmo output (sem efeitos colaterais).
|
|
266
|
+
> 3. **Transactional outbox** — write DB + event em mesma transação atomic; processador async lê outbox e publica.
|
|
267
|
+
>
|
|
268
|
+
> Detalhes completos em [`streams-eventos-cdc`](../streams-eventos-cdc/SKILL.md) (v1.22).
|
|
269
|
+
|
|
270
|
+
## Ver também
|
|
271
|
+
|
|
272
|
+
- [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions consumindo pgmq
|
|
273
|
+
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''` chamadas em cron
|
|
274
|
+
- [supabase-migrations](../supabase-migrations/SKILL.md) — extensions criadas em migrations
|
|
275
|
+
- [glossário](../_shared-supabase/glossary.md) — pg_cron, pgmq, pg_net
|