@luanpdd/kit-mcp 1.21.0 → 1.22.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/LICENSE +21 -21
- package/README.md +648 -648
- package/kit/COMANDOS.md +138 -138
- package/kit/README.md +76 -52
- package/kit/agents/advisor-researcher.md +106 -106
- package/kit/agents/assumptions-analyzer.md +107 -107
- package/kit/agents/auditor-consistencia-isolamento.md +380 -0
- package/kit/agents/codebase-mapper.md +768 -768
- package/kit/agents/crm-pipeline-implementer.md +17 -0
- package/kit/agents/debugger.md +772 -772
- package/kit/agents/detector-tenant-quente.md +337 -0
- package/kit/agents/example-reviewer.md +21 -21
- package/kit/agents/executor.md +523 -523
- package/kit/agents/integration-checker.md +200 -200
- package/kit/agents/multi-tenant-isolation-auditor.md +10 -0
- package/kit/agents/nyquist-auditor.md +178 -178
- package/kit/agents/phase-researcher.md +696 -696
- package/kit/agents/plan-checker.md +272 -272
- package/kit/agents/planner.md +891 -891
- package/kit/agents/project-researcher.md +652 -652
- package/kit/agents/research-synthesizer.md +245 -245
- package/kit/agents/roadmapper.md +677 -677
- package/kit/agents/supabase-architect.md +10 -0
- package/kit/agents/supabase-migration-writer.md +12 -0
- package/kit/agents/ui-auditor.md +437 -437
- package/kit/agents/ui-checker.md +302 -302
- package/kit/agents/ui-researcher.md +355 -355
- package/kit/agents/user-profiler.md +175 -175
- package/kit/agents/validador-evolucao-schema.md +335 -0
- package/kit/agents/verifier.md +728 -728
- package/kit/commands/adicionar-backlog.md +75 -75
- package/kit/commands/adicionar-fase.md +42 -42
- package/kit/commands/adicionar-tarefa.md +45 -45
- package/kit/commands/adicionar-testes.md +41 -41
- package/kit/commands/ajuda.md +21 -21
- package/kit/commands/atualizar.md +37 -37
- package/kit/commands/auditar-marco.md +179 -179
- package/kit/commands/auditar-uat.md +23 -23
- package/kit/commands/autonomo.md +40 -40
- package/kit/commands/branch-pr.md +24 -24
- package/kit/commands/concluir-marco.md +247 -247
- package/kit/commands/configuracoes.md +36 -36
- package/kit/commands/dados-distribuidos.md +188 -0
- package/kit/commands/definir-perfil.md +10 -10
- package/kit/commands/depurar.md +190 -190
- package/kit/commands/discutir-fase.md +131 -131
- package/kit/commands/entrar-discord.md +17 -17
- package/kit/commands/estatisticas.md +18 -18
- package/kit/commands/example-greeting.md +33 -33
- package/kit/commands/executar-fase.md +58 -58
- package/kit/commands/expresso.md +56 -56
- package/kit/commands/fase-ui.md +34 -34
- package/kit/commands/fazer.md +57 -57
- package/kit/commands/fio.md +125 -125
- package/kit/commands/fluxos-trabalho.md +64 -64
- package/kit/commands/forense.md +176 -176
- package/kit/commands/gerenciador.md +38 -38
- package/kit/commands/inserir-fase.md +31 -31
- package/kit/commands/limpeza.md +17 -17
- package/kit/commands/listar-hipoteses-fase.md +45 -45
- package/kit/commands/listar-workspaces.md +18 -18
- package/kit/commands/mapear-codebase.md +70 -70
- package/kit/commands/nota.md +33 -33
- package/kit/commands/novo-marco.md +43 -43
- package/kit/commands/novo-projeto.md +41 -41
- package/kit/commands/novo-workspace.md +43 -43
- package/kit/commands/pausar-trabalho.md +37 -37
- package/kit/commands/perfil-usuario.md +45 -45
- package/kit/commands/pesquisar-fase.md +195 -195
- package/kit/commands/planejar-fase.md +67 -67
- package/kit/commands/planejar-lacunas.md +33 -33
- package/kit/commands/plantar-ideia.md +25 -25
- package/kit/commands/progresso.md +24 -24
- package/kit/commands/proximo.md +30 -30
- package/kit/commands/publicar.md +490 -490
- package/kit/commands/rapido.md +35 -35
- package/kit/commands/reaplicar-patches.md +124 -124
- package/kit/commands/relatorio-sessao.md +19 -19
- package/kit/commands/remover-fase.md +31 -31
- package/kit/commands/remover-workspace.md +26 -26
- package/kit/commands/resumo-marco.md +50 -50
- package/kit/commands/retomar-trabalho.md +40 -40
- package/kit/commands/revisar-backlog.md +60 -60
- package/kit/commands/revisar-ui.md +32 -32
- package/kit/commands/revisar.md +37 -37
- package/kit/commands/saude.md +21 -21
- package/kit/commands/setup-notion.md +93 -93
- package/kit/commands/sync-main.md +68 -68
- package/kit/commands/validar-fase.md +35 -35
- package/kit/commands/verificar-tarefas.md +44 -44
- package/kit/commands/verificar-trabalho.md +64 -64
- package/kit/file-manifest.json +27 -15
- package/kit/framework/bin/lib/commands.cjs +959 -959
- package/kit/framework/bin/lib/config.cjs +442 -442
- package/kit/framework/bin/lib/core.cjs +1230 -1230
- package/kit/framework/bin/lib/frontmatter.cjs +336 -336
- package/kit/framework/bin/lib/init.cjs +1442 -1442
- package/kit/framework/bin/lib/milestone.cjs +252 -252
- package/kit/framework/bin/lib/model-profiles.cjs +68 -68
- package/kit/framework/bin/lib/phase.cjs +888 -888
- package/kit/framework/bin/lib/profile-output.cjs +952 -952
- package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
- package/kit/framework/bin/lib/roadmap.cjs +329 -329
- package/kit/framework/bin/lib/security.cjs +382 -382
- package/kit/framework/bin/lib/state.cjs +1031 -1031
- package/kit/framework/bin/lib/template.cjs +222 -222
- package/kit/framework/bin/lib/uat.cjs +282 -282
- package/kit/framework/bin/lib/verify.cjs +888 -888
- package/kit/framework/bin/lib/workstream.cjs +491 -491
- package/kit/framework/bin/tools.cjs +918 -918
- package/kit/framework/commands/workstreams.md +63 -63
- package/kit/framework/references/checkpoints.md +778 -778
- package/kit/framework/references/continuation-format.md +249 -249
- package/kit/framework/references/decimal-phase-calculation.md +64 -64
- package/kit/framework/references/git-integration.md +295 -295
- package/kit/framework/references/git-planning-commit.md +38 -38
- package/kit/framework/references/model-profile-resolution.md +36 -36
- package/kit/framework/references/model-profiles.md +139 -139
- package/kit/framework/references/phase-argument-parsing.md +61 -61
- package/kit/framework/references/planning-config.md +202 -202
- package/kit/framework/references/questioning.md +162 -162
- package/kit/framework/references/tdd.md +263 -263
- package/kit/framework/references/ui-brand.md +160 -160
- package/kit/framework/references/user-profiling.md +657 -657
- package/kit/framework/references/verification-patterns.md +612 -612
- package/kit/framework/references/workstream-flag.md +58 -58
- package/kit/framework/templates/DEBUG.md +164 -164
- package/kit/framework/templates/UAT.md +265 -265
- package/kit/framework/templates/UI-SPEC.md +100 -100
- package/kit/framework/templates/VALIDATION.md +76 -76
- package/kit/framework/templates/claude-md.md +122 -122
- package/kit/framework/templates/codebase/architecture.md +185 -185
- package/kit/framework/templates/codebase/concerns.md +205 -205
- package/kit/framework/templates/codebase/conventions.md +204 -204
- package/kit/framework/templates/codebase/integrations.md +192 -192
- package/kit/framework/templates/codebase/stack.md +158 -158
- package/kit/framework/templates/codebase/structure.md +199 -199
- package/kit/framework/templates/codebase/testing.md +301 -301
- package/kit/framework/templates/config.json +44 -44
- package/kit/framework/templates/context.md +352 -352
- package/kit/framework/templates/continue-here.md +78 -78
- package/kit/framework/templates/copilot-instructions.md +7 -7
- package/kit/framework/templates/debug-subagent-prompt.md +91 -91
- package/kit/framework/templates/dev-preferences.md +20 -20
- package/kit/framework/templates/discovery.md +146 -146
- package/kit/framework/templates/discussion-log.md +63 -63
- package/kit/framework/templates/milestone-archive.md +123 -123
- package/kit/framework/templates/milestone.md +115 -115
- package/kit/framework/templates/phase-prompt.md +610 -610
- package/kit/framework/templates/planner-subagent-prompt.md +117 -117
- package/kit/framework/templates/project.md +186 -186
- package/kit/framework/templates/requirements.md +231 -231
- package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
- package/kit/framework/templates/research-project/FEATURES.md +147 -147
- package/kit/framework/templates/research-project/PITFALLS.md +200 -200
- package/kit/framework/templates/research-project/STACK.md +120 -120
- package/kit/framework/templates/research-project/SUMMARY.md +170 -170
- package/kit/framework/templates/research.md +419 -419
- package/kit/framework/templates/retrospective.md +54 -54
- package/kit/framework/templates/roadmap.md +202 -202
- package/kit/framework/templates/state.md +176 -176
- package/kit/framework/templates/summary-complex.md +59 -59
- package/kit/framework/templates/summary-minimal.md +41 -41
- package/kit/framework/templates/summary-standard.md +48 -48
- package/kit/framework/templates/summary.md +209 -209
- package/kit/framework/templates/user-profile.md +146 -146
- package/kit/framework/templates/user-setup.md +256 -256
- package/kit/framework/templates/verification-report.md +258 -258
- package/kit/framework/workflows/add-phase.md +112 -112
- package/kit/framework/workflows/add-tests.md +351 -351
- package/kit/framework/workflows/add-todo.md +158 -158
- package/kit/framework/workflows/audit-milestone.md +340 -340
- package/kit/framework/workflows/audit-uat.md +109 -109
- package/kit/framework/workflows/autonomous.md +891 -891
- package/kit/framework/workflows/check-todos.md +177 -177
- package/kit/framework/workflows/cleanup.md +152 -152
- package/kit/framework/workflows/complete-milestone.md +696 -696
- package/kit/framework/workflows/diagnose-issues.md +231 -231
- package/kit/framework/workflows/discovery-phase.md +289 -289
- package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
- package/kit/framework/workflows/discuss-phase.md +784 -784
- package/kit/framework/workflows/do.md +104 -104
- package/kit/framework/workflows/execute-phase.md +838 -838
- package/kit/framework/workflows/execute-plan.md +510 -510
- package/kit/framework/workflows/fast.md +102 -102
- package/kit/framework/workflows/forensics.md +265 -265
- package/kit/framework/workflows/health.md +181 -181
- package/kit/framework/workflows/help.md +619 -619
- package/kit/framework/workflows/insert-phase.md +130 -130
- package/kit/framework/workflows/list-phase-assumptions.md +178 -178
- package/kit/framework/workflows/list-workspaces.md +56 -56
- package/kit/framework/workflows/manager.md +362 -362
- package/kit/framework/workflows/map-codebase.md +377 -377
- package/kit/framework/workflows/milestone-summary.md +223 -223
- package/kit/framework/workflows/new-milestone.md +486 -486
- package/kit/framework/workflows/new-project.md +1159 -1159
- package/kit/framework/workflows/new-workspace.md +237 -237
- package/kit/framework/workflows/next.md +97 -97
- package/kit/framework/workflows/node-repair.md +92 -92
- package/kit/framework/workflows/note.md +156 -156
- package/kit/framework/workflows/pause-work.md +176 -176
- package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
- package/kit/framework/workflows/plan-phase.md +765 -765
- package/kit/framework/workflows/plant-seed.md +169 -169
- package/kit/framework/workflows/pr-branch.md +129 -129
- package/kit/framework/workflows/profile-user.md +450 -450
- package/kit/framework/workflows/progress.md +507 -507
- package/kit/framework/workflows/quick.md +757 -757
- package/kit/framework/workflows/remove-phase.md +155 -155
- package/kit/framework/workflows/remove-workspace.md +90 -90
- package/kit/framework/workflows/research-phase.md +82 -82
- package/kit/framework/workflows/resume-project.md +326 -326
- package/kit/framework/workflows/review.md +228 -228
- package/kit/framework/workflows/session-report.md +146 -146
- package/kit/framework/workflows/settings.md +283 -283
- package/kit/framework/workflows/ship.md +228 -228
- package/kit/framework/workflows/stats.md +60 -60
- package/kit/framework/workflows/transition.md +671 -671
- package/kit/framework/workflows/ui-phase.md +302 -302
- package/kit/framework/workflows/ui-review.md +165 -165
- package/kit/framework/workflows/update.md +323 -323
- package/kit/framework/workflows/validate-phase.md +174 -174
- package/kit/framework/workflows/verify-phase.md +252 -252
- package/kit/framework/workflows/verify-work.md +637 -637
- package/kit/hooks/check-update.js +118 -118
- package/kit/hooks/context-monitor.js +163 -163
- package/kit/hooks/prompt-guard.js +103 -103
- package/kit/hooks/statusline.js +125 -125
- package/kit/hooks/workflow-guard.js +101 -101
- package/kit/settings.json +45 -45
- package/kit/skills/_shared-dados-distribuidos/glossary.md +224 -0
- package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -0
- package/kit/skills/audit-log-multi-tenant/SKILL.md +6 -0
- package/kit/skills/cascading-failures/SKILL.md +4 -0
- package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -0
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +17 -0
- package/kit/skills/escolha-modelo-consistencia/SKILL.md +495 -0
- package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -0
- package/kit/skills/example-skill/SKILL.md +42 -42
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +4 -0
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +4 -0
- package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -0
- package/kit/skills/streams-eventos-cdc/SKILL.md +712 -0
- package/kit/skills/supabase-cron-queues/SKILL.md +9 -0
- package/kit/skills/supabase-migrations/SKILL.md +10 -0
- package/kit/skills/super-admin-platform-pattern/SKILL.md +4 -0
- package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -0
- package/package.json +63 -63
- package/src/core/kit.js +216 -216
- package/src/core/reflect.js +247 -247
- package/src/core/reverse-sync.js +372 -372
- package/src/core/sync.js +418 -418
- package/src/core/watch.js +121 -121
|
@@ -258,6 +258,15 @@ async function processJob(msg) {
|
|
|
258
258
|
}
|
|
259
259
|
```
|
|
260
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
|
+
|
|
261
270
|
## Ver também
|
|
262
271
|
|
|
263
272
|
- [supabase-edge-functions](../supabase-edge-functions/SKILL.md) — Edge Functions consumindo pgmq
|
|
@@ -166,6 +166,16 @@ using (auth.uid() = user_id)
|
|
|
166
166
|
using ((select auth.uid()) = user_id)
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
## Padrão Rolling-Upgrade para Migrations Arriscadas (v1.22+)
|
|
170
|
+
|
|
171
|
+
> Migrations que adicionam `NOT NULL` em coluna existente, mudam tipo, ou removem column quebram backward compat com app rodando V1+V2 em paralelo. Padrão canônico **3-passos** (DDIA Ch 4):
|
|
172
|
+
> 1. `ALTER TABLE ... ADD COLUMN x text` (nullable)
|
|
173
|
+
> 2. `UPDATE ... SET x = ... WHERE x IS NULL LIMIT 10000` em loop até 100% backfill
|
|
174
|
+
> 3. `ALTER TABLE ... ALTER COLUMN x SET NOT NULL` apenas após verificação
|
|
175
|
+
>
|
|
176
|
+
> Padrão completo em [`evolucao-schema-compativel`](../evolucao-schema-compativel/SKILL.md) (v1.22).
|
|
177
|
+
> Validação automática via agent [`validador-evolucao-schema`](../../agents/validador-evolucao-schema.md) (v1.22).
|
|
178
|
+
|
|
169
179
|
## Ver também
|
|
170
180
|
|
|
171
181
|
- [supabase-postgres-style](../supabase-postgres-style/SKILL.md) — convenção de naming + style aplicada
|
|
@@ -312,6 +312,10 @@ using ((auth.jwt()->'user_metadata'->>'super_admin')::boolean = true)
|
|
|
312
312
|
|
|
313
313
|
**Certo:** REGRA #6 — modal exige typed slug + reason + checkbox + RPC valida tudo server-side. Soft delete preferred.
|
|
314
314
|
|
|
315
|
+
## Fencing Token para TTL de Impersonação (v1.22+)
|
|
316
|
+
|
|
317
|
+
> A TTL de 30min de impersonação é vulnerável a split-brain durante GC pause: super-admin A inicia impersonação, sofre GC pause de 35min, TTL expira, super-admin B inicia outra impersonação no mesmo target, A volta ainda achando que tem sessão. Mitigação canônica: fencing token monotônico em `super_admin_impersonations` table — storage rejeita writes com token < último visto. Padrão completo em [`armadilhas-sistemas-distribuidos`](../armadilhas-sistemas-distribuidos/SKILL.md) (v1.22 — DDIA Ch 8).
|
|
318
|
+
|
|
315
319
|
## Ver também
|
|
316
320
|
|
|
317
321
|
- [audit-log-multi-tenant](../audit-log-multi-tenant/SKILL.md) — Phase 109, audit_logs + event `super_admin_action` (REGRA #1)
|
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tenant-quente-mitigacao
|
|
3
|
+
description: Use ao escalar Postgres multi-tenant em Supabase quando 1 tenant consome >>> que outros (problema "Justin Bieber tenant" do DDIA Ch 6) — métricas de detecção (queries/min, storage GB, slots conexão), 5 estratégias de mitigação com tradeoffs (rate limit por tenant, pool isolado, read replica dedicada, desnormalização MVs, request shaping pgmq), particionamento range vs hash para tenant_id, índices document-partitioned vs term-partitioned, rebalanceamento sem downtime.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tenant Quente — Mitigação (DDIA Ch 6 aplicado a Postgres + Supabase)
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando há **suspeita ou evidência de skewed workload em B2B SaaS multi-tenant** — i.e. um tenant (ou pequeno conjunto) consome desproporcionalmente recursos vs P50 dos demais. DDIA Ch 6 chama isso de **hot spot**, e o anchor narrativo canônico é o "Justin Bieber tenant" — referência ao caso Twitter onde 3% dos servidores ficaram dedicados a 1 celebrity user (DDIA p.196 nota [13]). Em B2B SaaS, o equivalente é **1 cliente enterprise** (ou anchor tenant) que escala 10× mais rápido que o restante da base.
|
|
11
|
+
|
|
12
|
+
Trigger phrases:
|
|
13
|
+
|
|
14
|
+
- "tenant Justin Bieber", "hot tenant", "skewed multi-tenant"
|
|
15
|
+
- "1 cliente consumindo a base inteira", "tenant dominante", "anchor tenant"
|
|
16
|
+
- "particionamento por tenant", "PARTITION BY HASH/RANGE org_id"
|
|
17
|
+
- "scatter-gather Postgres super-admin"
|
|
18
|
+
- "rebalancear tenant sem downtime", "mover tenant para schema dedicado"
|
|
19
|
+
- "MV per-tenant pesada", "queue priority por tenant"
|
|
20
|
+
|
|
21
|
+
Esta skill é consumida por `multi-tenant-isolation-auditor` (v1.21) ao detectar tabelas suspeitas de skew, por `omm-auditor` (v1.10) ao avaliar capacidade de escala, e por `b2b-saas-architect` (v1.21) ao desenhar schema de novo cliente enterprise grande.
|
|
22
|
+
|
|
23
|
+
## Regras absolutas
|
|
24
|
+
|
|
25
|
+
**REGRA #1 (medir antes de mitigar):** **NUNCA** aplicar mitigação sem coletar baseline 30d das 3 métricas canônicas (REQ TENANT-01). Mitigação prematura = otimização cega. Threshold canônico: WARN >3× P50, CRITICAL >10× P50.
|
|
26
|
+
|
|
27
|
+
**REGRA #2 (default document-partitioned):** Índices secundários em tabelas particionadas devem ser **document-partitioned (local)** por default. Term-partitioned (global) **só** em query path crítica onde scatter-gather é o gargalo medido.
|
|
28
|
+
|
|
29
|
+
**REGRA #3 (hash quando uniforme, range quando skewed conhecido):** Particionar por `HASH (org_id)` quando workload é uniforme cross-tenant. Particionar por `RANGE (org_id)` apenas quando hot tenants são **conhecidos a priori** (anchor tenant enterprise onboarded com SLA dedicado).
|
|
30
|
+
|
|
31
|
+
**REGRA #4 (rebalanceamento manual, nunca automático):** Mover tenant para schema/instância dedicada **NUNCA** automaticamente. Sempre humano-no-loop com janela de manutenção comunicada — DDIA p.204 ("Operations: automatic or manual rebalancing") documenta o risco de cascading failure quando rebalance auto reage a node lento.
|
|
32
|
+
|
|
33
|
+
**REGRA #5 (cleanup conservador):** Após mover tenant, **NUNCA** dropar schema/dados antigos antes de **7d sem queries** confirmados via `pg_stat_user_tables.last_seq_scan` + `last_idx_scan`. Defesa contra rollback emergencial.
|
|
34
|
+
|
|
35
|
+
## Patterns canônicos
|
|
36
|
+
|
|
37
|
+
### REQ TENANT-01 — Detecção do "tenant Justin Bieber"
|
|
38
|
+
|
|
39
|
+
Três métricas canônicas, todas com baseline 30d e threshold relativo ao P50 da base de tenants ativos:
|
|
40
|
+
|
|
41
|
+
#### Métrica 1 — Ratio queries/min via `pg_stat_statements`
|
|
42
|
+
|
|
43
|
+
```sql
|
|
44
|
+
-- Pré-requisito: pg_stat_statements habilitado (Supabase: Settings → Database → Extensions)
|
|
45
|
+
-- Helper: extrai org_id do parameter da query (assume RLS sempre filtra por org_id literal/parameter)
|
|
46
|
+
create or replace function private.extract_org_id_from_query(p_query text)
|
|
47
|
+
returns uuid
|
|
48
|
+
language plpgsql
|
|
49
|
+
immutable
|
|
50
|
+
set search_path = ''
|
|
51
|
+
as $$
|
|
52
|
+
declare
|
|
53
|
+
m text[];
|
|
54
|
+
begin
|
|
55
|
+
-- Casa UUID em formato canônico no texto da query (parameter-bound)
|
|
56
|
+
m := regexp_match(p_query, '''([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})''');
|
|
57
|
+
if m is null then
|
|
58
|
+
return null;
|
|
59
|
+
end if;
|
|
60
|
+
return m[1]::uuid;
|
|
61
|
+
end;
|
|
62
|
+
$$;
|
|
63
|
+
|
|
64
|
+
-- View canônica: queries/min por org_id sobre janela 24h
|
|
65
|
+
create or replace view private.hot_tenant_query_rate as
|
|
66
|
+
with per_org as (
|
|
67
|
+
select
|
|
68
|
+
private.extract_org_id_from_query(query) as org_id,
|
|
69
|
+
sum(calls) / nullif(extract(epoch from (now() - stats_reset)) / 60, 0) as queries_per_min
|
|
70
|
+
from pg_stat_statements
|
|
71
|
+
where private.extract_org_id_from_query(query) is not null
|
|
72
|
+
group by 1
|
|
73
|
+
),
|
|
74
|
+
stats as (
|
|
75
|
+
select
|
|
76
|
+
percentile_cont(0.5) within group (order by queries_per_min) as p50
|
|
77
|
+
from per_org
|
|
78
|
+
)
|
|
79
|
+
select
|
|
80
|
+
per_org.org_id,
|
|
81
|
+
per_org.queries_per_min,
|
|
82
|
+
stats.p50,
|
|
83
|
+
round((per_org.queries_per_min / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
|
|
84
|
+
case
|
|
85
|
+
when per_org.queries_per_min > 10 * stats.p50 then 'CRITICAL'
|
|
86
|
+
when per_org.queries_per_min > 3 * stats.p50 then 'WARN'
|
|
87
|
+
else 'OK'
|
|
88
|
+
end as severity
|
|
89
|
+
from per_org cross join stats
|
|
90
|
+
order by ratio_vs_p50 desc nulls last;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Métrica 2 — Ratio storage GB via `pg_total_relation_size`
|
|
94
|
+
|
|
95
|
+
```sql
|
|
96
|
+
-- View: storage por tenant agregando tabelas particionadas + tabelas não-particionadas
|
|
97
|
+
-- Assume convenção de naming partição: <tabela_base>_<org_id_underscore>
|
|
98
|
+
create or replace view private.hot_tenant_storage as
|
|
99
|
+
with per_partition as (
|
|
100
|
+
select
|
|
101
|
+
-- Extrai org_id do nome da partição (audit_logs_<uuid_underscore> -> uuid)
|
|
102
|
+
replace(
|
|
103
|
+
regexp_replace(c.relname, '^[a-z_]+_([0-9a-f_]{36})$', '\1'),
|
|
104
|
+
'_', '-'
|
|
105
|
+
)::uuid as org_id,
|
|
106
|
+
pg_total_relation_size(c.oid) as bytes
|
|
107
|
+
from pg_class c
|
|
108
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
109
|
+
where n.nspname = 'public'
|
|
110
|
+
and c.relkind = 'r' -- tabelas regulares
|
|
111
|
+
and c.relname ~ '_[0-9a-f]{8}_[0-9a-f]{4}_[0-9a-f]{4}_[0-9a-f]{4}_[0-9a-f]{12}$'
|
|
112
|
+
),
|
|
113
|
+
per_org as (
|
|
114
|
+
select
|
|
115
|
+
org_id,
|
|
116
|
+
sum(bytes) / (1024.0^3) as storage_gb
|
|
117
|
+
from per_partition
|
|
118
|
+
group by 1
|
|
119
|
+
),
|
|
120
|
+
stats as (
|
|
121
|
+
select percentile_cont(0.5) within group (order by storage_gb) as p50 from per_org
|
|
122
|
+
)
|
|
123
|
+
select
|
|
124
|
+
per_org.org_id,
|
|
125
|
+
round(per_org.storage_gb::numeric, 3) as storage_gb,
|
|
126
|
+
round(stats.p50::numeric, 3) as p50_gb,
|
|
127
|
+
round((per_org.storage_gb / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
|
|
128
|
+
case
|
|
129
|
+
when per_org.storage_gb > 10 * stats.p50 then 'CRITICAL'
|
|
130
|
+
when per_org.storage_gb > 3 * stats.p50 then 'WARN'
|
|
131
|
+
else 'OK'
|
|
132
|
+
end as severity
|
|
133
|
+
from per_org cross join stats
|
|
134
|
+
order by storage_gb desc;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Métrica 3 — Ratio conn slots via `pg_stat_activity`
|
|
138
|
+
|
|
139
|
+
```sql
|
|
140
|
+
-- Pré-requisito: app seta application_name com org context, ex: 'app:org=<uuid>:edge=lead-create'
|
|
141
|
+
-- Convenção canônica documentada em b2b-saas-architecture
|
|
142
|
+
create or replace view private.hot_tenant_conn_slots as
|
|
143
|
+
with per_org as (
|
|
144
|
+
select
|
|
145
|
+
-- Extrai uuid do application_name após 'org='
|
|
146
|
+
(regexp_match(application_name, 'org=([0-9a-f-]{36})'))[1]::uuid as org_id,
|
|
147
|
+
count(*) as active_slots
|
|
148
|
+
from pg_stat_activity
|
|
149
|
+
where state = 'active'
|
|
150
|
+
and application_name ~ 'org=[0-9a-f-]{36}'
|
|
151
|
+
group by 1
|
|
152
|
+
),
|
|
153
|
+
stats as (
|
|
154
|
+
select percentile_cont(0.5) within group (order by active_slots) as p50 from per_org
|
|
155
|
+
)
|
|
156
|
+
select
|
|
157
|
+
per_org.org_id,
|
|
158
|
+
per_org.active_slots,
|
|
159
|
+
stats.p50,
|
|
160
|
+
round((per_org.active_slots::numeric / nullif(stats.p50, 0))::numeric, 2) as ratio_vs_p50,
|
|
161
|
+
case
|
|
162
|
+
when per_org.active_slots > 10 * stats.p50 then 'CRITICAL'
|
|
163
|
+
when per_org.active_slots > 3 * stats.p50 then 'WARN'
|
|
164
|
+
else 'OK'
|
|
165
|
+
end as severity
|
|
166
|
+
from per_org cross join stats
|
|
167
|
+
order by ratio_vs_p50 desc nulls last;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Hot tenant é confirmado quando ≥ 2 das 3 métricas estão em WARN+ simultaneamente** — uma só métrica sozinha pode ser falso positivo (batch job, importação, migração). Triangulação reduz noise.
|
|
171
|
+
|
|
172
|
+
### REQ TENANT-02 — 5 estratégias de mitigação (tabela canônica)
|
|
173
|
+
|
|
174
|
+
| # | Estratégia | Quando usar | Tradeoff principal | Config / SQL exemplo |
|
|
175
|
+
|---|---|---|---|---|
|
|
176
|
+
| 1 | **Rate limit por tenant** | Picos imprevisíveis de write/read em hot tenant que prejudicam P95 dos demais | Impacto UX no tenant target — usuário vê HTTP 429; precisa coordenar com customer success | RLS reject + `pg_cron` throttle counter (abaixo) |
|
|
177
|
+
| 2 | **Pool conexão isolado (Supavisor multi-pool)** | Conn starvation — hot tenant esgota slots na pool compartilhada | Custo Supavisor multi-pool (Pro+) + complexidade de routing | Supavisor config `[pools.org_<uuid>]` |
|
|
178
|
+
| 3 | **Read replica dedicada** | Tenant read-heavy (dashboards, exports) que não precisa de write strong consistency | Custo Supabase Pro+ + lag replicação aceitável (centenas ms) | Supavisor `read.*` routing + `application_name` hint |
|
|
179
|
+
| 4 | **Desnormalização (MV per-tenant)** | Query repetitiva pesada (agregações, joins 5+ tabelas) que rodam 100× / hora p/ mesmo tenant | Refresh complexity + staleness window aceitável (5-15min) | `CREATE MATERIALIZED VIEW ... REFRESH CONCURRENTLY` + `pg_cron` |
|
|
180
|
+
| 5 | **Request shaping (pgmq priority)** | Picos previsíveis batch (relatório fim-de-mês, importação) — work é assíncrono | Complexidade fila + worker; latency aumenta para hot tenant | `pgmq` priority queue + worker que drena LOW após HIGH |
|
|
181
|
+
|
|
182
|
+
#### Estratégia 1 — Rate limit por tenant (exemplo)
|
|
183
|
+
|
|
184
|
+
```sql
|
|
185
|
+
-- Tabela counter: bucket por org × minuto
|
|
186
|
+
create table private.tenant_rate_limit_buckets (
|
|
187
|
+
org_id uuid not null,
|
|
188
|
+
bucket_minute timestamptz not null,
|
|
189
|
+
request_count int not null default 0,
|
|
190
|
+
primary key (org_id, bucket_minute)
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
-- Função: incrementa counter e retorna se excedeu limite
|
|
194
|
+
create or replace function private.check_tenant_rate_limit(
|
|
195
|
+
p_org_id uuid,
|
|
196
|
+
p_limit_per_min int default 1000
|
|
197
|
+
)
|
|
198
|
+
returns boolean
|
|
199
|
+
language plpgsql
|
|
200
|
+
security definer
|
|
201
|
+
set search_path = ''
|
|
202
|
+
as $$
|
|
203
|
+
declare
|
|
204
|
+
v_count int;
|
|
205
|
+
v_bucket timestamptz;
|
|
206
|
+
begin
|
|
207
|
+
v_bucket := date_trunc('minute', now());
|
|
208
|
+
insert into private.tenant_rate_limit_buckets (org_id, bucket_minute, request_count)
|
|
209
|
+
values (p_org_id, v_bucket, 1)
|
|
210
|
+
on conflict (org_id, bucket_minute)
|
|
211
|
+
do update set request_count = tenant_rate_limit_buckets.request_count + 1
|
|
212
|
+
returning request_count into v_count;
|
|
213
|
+
return v_count <= p_limit_per_min;
|
|
214
|
+
end;
|
|
215
|
+
$$;
|
|
216
|
+
|
|
217
|
+
-- Cleanup buckets > 1h (pg_cron)
|
|
218
|
+
select cron.schedule('cleanup-rate-limit-buckets', '*/15 * * * *', $$
|
|
219
|
+
delete from private.tenant_rate_limit_buckets
|
|
220
|
+
where bucket_minute < now() - interval '1 hour';
|
|
221
|
+
$$);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Estratégia 4 — MV per-tenant (exemplo agregação leads)
|
|
225
|
+
|
|
226
|
+
```sql
|
|
227
|
+
-- MV agregando métricas pesadas só para hot tenant
|
|
228
|
+
-- (Para os demais tenants, query original direto na tabela ainda é rápida)
|
|
229
|
+
create materialized view public.lead_metrics_org_<uuid_underscore> as
|
|
230
|
+
select
|
|
231
|
+
l.stage,
|
|
232
|
+
count(*) as count,
|
|
233
|
+
count(*) filter (where l.created_at > now() - interval '7 days') as last_7d
|
|
234
|
+
from public.leads l
|
|
235
|
+
where l.org_id = '<uuid>'
|
|
236
|
+
group by l.stage;
|
|
237
|
+
|
|
238
|
+
create unique index lead_metrics_org_<uuid_underscore>_stage_idx
|
|
239
|
+
on public.lead_metrics_org_<uuid_underscore> (stage);
|
|
240
|
+
|
|
241
|
+
-- Refresh concurrent a cada 10min
|
|
242
|
+
select cron.schedule(
|
|
243
|
+
'refresh-lead-metrics-org-<uuid_underscore>',
|
|
244
|
+
'*/10 * * * *',
|
|
245
|
+
$$ refresh materialized view concurrently public.lead_metrics_org_<uuid_underscore>; $$
|
|
246
|
+
);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Estratégia 5 — Request shaping (pgmq priority)
|
|
250
|
+
|
|
251
|
+
```sql
|
|
252
|
+
-- 2 filas: high (pequenos clientes) + low (hot tenant batch)
|
|
253
|
+
select pgmq.create('exports_high');
|
|
254
|
+
select pgmq.create('exports_low');
|
|
255
|
+
|
|
256
|
+
-- Enqueue: hot tenant vai para low, demais para high
|
|
257
|
+
create or replace function public.enqueue_export(p_org_id uuid, p_payload jsonb)
|
|
258
|
+
returns bigint
|
|
259
|
+
language plpgsql
|
|
260
|
+
security invoker
|
|
261
|
+
set search_path = ''
|
|
262
|
+
as $$
|
|
263
|
+
declare
|
|
264
|
+
v_is_hot boolean;
|
|
265
|
+
begin
|
|
266
|
+
-- Lookup hot tenant (refresh por job separado)
|
|
267
|
+
select exists (select 1 from private.hot_tenant_registry where org_id = p_org_id and active)
|
|
268
|
+
into v_is_hot;
|
|
269
|
+
if v_is_hot then
|
|
270
|
+
return (select msg_id from pgmq.send('exports_low', p_payload));
|
|
271
|
+
else
|
|
272
|
+
return (select msg_id from pgmq.send('exports_high', p_payload));
|
|
273
|
+
end if;
|
|
274
|
+
end;
|
|
275
|
+
$$;
|
|
276
|
+
|
|
277
|
+
-- Worker: drena high primeiro, low só quando high vazio
|
|
278
|
+
-- (implementação Edge Function com Deno cron)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### REQ TENANT-03 — Particionamento range vs hash para `tenant_id`
|
|
282
|
+
|
|
283
|
+
#### Decision tree
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
Tabela > 50k rows/tenant OU > 5M rows total?
|
|
287
|
+
├── Não → SEM particionamento (overhead > benefit). Use partial indexes.
|
|
288
|
+
└── Sim → particionar
|
|
289
|
+
├── Workload uniforme cross-tenant (P95 ratio < 2× P50)?
|
|
290
|
+
│ ├── Sim → HASH (org_id) com 16-64 partições fixas
|
|
291
|
+
│ └── Não → continuar abaixo
|
|
292
|
+
└── Hot tenants conhecidos a priori (anchor tenant onboarded com SLA)?
|
|
293
|
+
├── Sim → RANGE (org_id) com partição manual para cada hot
|
|
294
|
+
└── Não → HASH (default seguro) + monitor com REQ TENANT-01
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Hash partitioning — workload uniforme
|
|
298
|
+
|
|
299
|
+
```sql
|
|
300
|
+
-- Tabela particionada por HASH em 16 partições (typical sweet spot Postgres 16+)
|
|
301
|
+
create table public.events (
|
|
302
|
+
id uuid not null,
|
|
303
|
+
org_id uuid not null,
|
|
304
|
+
event_type text not null,
|
|
305
|
+
payload jsonb,
|
|
306
|
+
created_at timestamptz not null default now(),
|
|
307
|
+
primary key (org_id, id)
|
|
308
|
+
) partition by hash (org_id);
|
|
309
|
+
|
|
310
|
+
-- Cria 16 partições — Postgres distribui via hash modulo 16
|
|
311
|
+
do $$
|
|
312
|
+
declare
|
|
313
|
+
i int;
|
|
314
|
+
begin
|
|
315
|
+
for i in 0..15 loop
|
|
316
|
+
execute format(
|
|
317
|
+
'create table public.events_p%s partition of public.events for values with (modulus 16, remainder %s)',
|
|
318
|
+
lpad(i::text, 2, '0'), i
|
|
319
|
+
);
|
|
320
|
+
end loop;
|
|
321
|
+
end $$;
|
|
322
|
+
|
|
323
|
+
-- Index local em cada partição (document-partitioned — REQ TENANT-04)
|
|
324
|
+
create index events_org_created_idx on public.events (org_id, created_at desc);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Por que 16 partições:** sweet spot empírico Postgres 16+ — partição management overhead negligível, paralelização de scans efetiva. Acima de 64 partições, planner começa a sofrer (citação DDIA p.202 — "each partition also has management overhead").
|
|
328
|
+
|
|
329
|
+
#### Range partitioning — anchor tenant conhecido
|
|
330
|
+
|
|
331
|
+
```sql
|
|
332
|
+
-- Tabela particionada por RANGE — partição dedicada para anchor tenant + default p/ os demais
|
|
333
|
+
create table public.audit_logs (
|
|
334
|
+
id uuid not null,
|
|
335
|
+
org_id uuid not null,
|
|
336
|
+
event_type text not null,
|
|
337
|
+
actor_id uuid,
|
|
338
|
+
payload jsonb,
|
|
339
|
+
created_at timestamptz not null default now(),
|
|
340
|
+
primary key (org_id, id)
|
|
341
|
+
) partition by range (org_id);
|
|
342
|
+
|
|
343
|
+
-- Partição dedicada para anchor tenant (uuid conhecido)
|
|
344
|
+
create table public.audit_logs_anchor_acme
|
|
345
|
+
partition of public.audit_logs
|
|
346
|
+
for values from ('11111111-1111-1111-1111-111111111111')
|
|
347
|
+
to ('11111111-1111-1111-1111-111111111112');
|
|
348
|
+
|
|
349
|
+
-- Partição default para todos os demais — rebalancear manualmente quando outro tenant virar hot
|
|
350
|
+
create table public.audit_logs_default
|
|
351
|
+
partition of public.audit_logs
|
|
352
|
+
default;
|
|
353
|
+
|
|
354
|
+
-- Índice local em cada partição
|
|
355
|
+
create index audit_logs_anchor_acme_created_idx on public.audit_logs_anchor_acme (created_at desc);
|
|
356
|
+
create index audit_logs_default_org_created_idx on public.audit_logs_default (org_id, created_at desc);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Vantagem range para anchor:** isola I/O do anchor tenant. Bloat/vacuum/analyze de outras orgs não bloqueia o anchor. Permite tablespace dedicado em disco SSD separado.
|
|
360
|
+
|
|
361
|
+
**Risco range:** se outro tenant escalar inesperadamente, partição default fica skewed. Mitigação: REQ TENANT-01 monitor + script de migração para nova partição range.
|
|
362
|
+
|
|
363
|
+
### REQ TENANT-04 — Índices secundários document-partitioned vs term-partitioned
|
|
364
|
+
|
|
365
|
+
DDIA p.197-200 distingue duas estratégias para índices secundários em tabelas particionadas. Aplicado a queries cross-tenant em views super-admin (caso canônico em B2B SaaS):
|
|
366
|
+
|
|
367
|
+
| Aspecto | Document-partitioned (local) | Term-partitioned (global) |
|
|
368
|
+
|---|---|---|
|
|
369
|
+
| **Topologia** | 1 índice por partição (default Postgres) | 1 índice global cobrindo todas as partições (não-default Postgres — exige extensão pg_partman ou abordagem manual) |
|
|
370
|
+
| **Write cost** | Barato — 1 partição afetada | Caro — N partições do índice afetadas + lock cross-partição |
|
|
371
|
+
| **Read cost (single tenant)** | O(log n) na partição alvo | O(log n) no índice global |
|
|
372
|
+
| **Read cost (cross-tenant super-admin)** | **scatter-gather** — todas as partições consultadas em paralelo | O(log n) — 1 lookup direto |
|
|
373
|
+
| **Aplicação canônica** | RLS queries normais (filter por `org_id` → 1 partição) | Super-admin views que listam todas orgs por critério (ex: "todas as leads created_at > X") |
|
|
374
|
+
|
|
375
|
+
#### Recomendação default: document-partitioned
|
|
376
|
+
|
|
377
|
+
```sql
|
|
378
|
+
-- Index local em cada partição da tabela events (REQ TENANT-03)
|
|
379
|
+
-- Postgres cria automaticamente em cada partição quando criado na tabela parent
|
|
380
|
+
create index events_event_type_idx on public.events (event_type);
|
|
381
|
+
|
|
382
|
+
-- Verificar que cada partição tem o index
|
|
383
|
+
select
|
|
384
|
+
pi.indrelid::regclass as partition_name,
|
|
385
|
+
pi.indexrelid::regclass as index_name
|
|
386
|
+
from pg_inherits inh
|
|
387
|
+
join pg_index pi on pi.indrelid = inh.inhrelid
|
|
388
|
+
where inh.inhparent = 'public.events'::regclass;
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Query super-admin sobre `event_type` faz scatter-gather** — Postgres pruner não consegue eliminar partições (filter não inclui `org_id`). Custo: tail latency amplification (DDIA p.198) — query é tão lenta quanto a partição mais lenta. Aceitável para super-admin (queries raras, async, não user-facing).
|
|
392
|
+
|
|
393
|
+
#### Term-partitioned (quando query path é crítico)
|
|
394
|
+
|
|
395
|
+
Postgres não suporta nativamente índice global em tabela particionada. Opções:
|
|
396
|
+
|
|
397
|
+
1. **Tabela auxiliar de lookup** — manualmente mantida via trigger:
|
|
398
|
+
|
|
399
|
+
```sql
|
|
400
|
+
-- Lookup table cross-tenant: term → (org_id, event_id)
|
|
401
|
+
-- Mantida via trigger nas partições filhas
|
|
402
|
+
create table private.events_event_type_global_idx (
|
|
403
|
+
event_type text not null,
|
|
404
|
+
org_id uuid not null,
|
|
405
|
+
event_id uuid not null,
|
|
406
|
+
created_at timestamptz not null,
|
|
407
|
+
primary key (event_type, created_at desc, org_id, event_id)
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
create or replace function private.events_sync_global_idx()
|
|
411
|
+
returns trigger
|
|
412
|
+
language plpgsql
|
|
413
|
+
security definer
|
|
414
|
+
set search_path = ''
|
|
415
|
+
as $$
|
|
416
|
+
begin
|
|
417
|
+
if (tg_op = 'INSERT') then
|
|
418
|
+
insert into private.events_event_type_global_idx
|
|
419
|
+
(event_type, org_id, event_id, created_at)
|
|
420
|
+
values (new.event_type, new.org_id, new.id, new.created_at);
|
|
421
|
+
elsif (tg_op = 'DELETE') then
|
|
422
|
+
delete from private.events_event_type_global_idx
|
|
423
|
+
where event_type = old.event_type and event_id = old.id;
|
|
424
|
+
end if;
|
|
425
|
+
return null;
|
|
426
|
+
end;
|
|
427
|
+
$$;
|
|
428
|
+
|
|
429
|
+
-- Trigger replicado em cada partição (script bash gera a partir de pg_inherits)
|
|
430
|
+
-- Custo: 2× write (tabela + lookup) + lock cross-partição quando lookup é atualizado
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
2. **Aceitar staleness via job batch** — DDIA p.200 nota que DynamoDB GSI tem propagação assíncrona "within a fraction of a second". Mesmo trade-off vale aqui:
|
|
434
|
+
|
|
435
|
+
```sql
|
|
436
|
+
-- Refresh global index via pg_cron a cada 30s
|
|
437
|
+
select cron.schedule('refresh-events-global-idx', '*/30 * * * * *', $$
|
|
438
|
+
insert into private.events_event_type_global_idx
|
|
439
|
+
(event_type, org_id, event_id, created_at)
|
|
440
|
+
select event_type, org_id, id, created_at
|
|
441
|
+
from public.events
|
|
442
|
+
where created_at > coalesce((select max(created_at) from private.events_event_type_global_idx), 'epoch')
|
|
443
|
+
on conflict do nothing;
|
|
444
|
+
$$);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Recomendação canônica:** começar com document-partitioned. Migrar para term-partitioned **somente** quando query path super-admin específica for medida em > 5s P95 e não-async-tolerant.
|
|
448
|
+
|
|
449
|
+
### REQ TENANT-05 — Rebalanceamento sem downtime (4 passos)
|
|
450
|
+
|
|
451
|
+
DDIA p.201-204 documenta que rebalancing tem 3 requisitos não-negociáveis: load fair pós-rebalance, sem downtime durante rebalance, mover só o necessário. Aplicado a Postgres + Supavisor:
|
|
452
|
+
|
|
453
|
+
#### Passo 1 — Detectar tenant alvo via thresholds (REQ TENANT-01)
|
|
454
|
+
|
|
455
|
+
Confirmado quando ≥ 2 das 3 métricas em CRITICAL por > 7 dias consecutivos. Decisão: humano (DBA + customer success) revisa antes de prosseguir. **NÃO automatizar.**
|
|
456
|
+
|
|
457
|
+
#### Passo 2 — Dump do tenant para schema isolado
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
# Pré-requisito: app está em modo read-only para o hot tenant durante 30min de janela de manutenção
|
|
461
|
+
# (controlado por feature flag — coordenado com customer success)
|
|
462
|
+
|
|
463
|
+
# Dump apenas tabelas do tenant (assumindo convenção partition naming)
|
|
464
|
+
pg_dump \
|
|
465
|
+
--schema=public \
|
|
466
|
+
--table='*tenant_<uuid_underscore>*' \
|
|
467
|
+
--table='public.events_<uuid_underscore>' \
|
|
468
|
+
--table='public.audit_logs_<uuid_underscore>' \
|
|
469
|
+
--no-owner \
|
|
470
|
+
--no-acl \
|
|
471
|
+
--file=/tmp/tenant_<uuid>_dump.sql \
|
|
472
|
+
postgresql://postgres:<password>@db.<source_project_ref>.supabase.co:5432/postgres
|
|
473
|
+
|
|
474
|
+
# Restaurar em nova instância Supabase dedicada (criada previamente)
|
|
475
|
+
psql \
|
|
476
|
+
postgresql://postgres:<password>@db.<dedicated_project_ref>.supabase.co:5432/postgres \
|
|
477
|
+
< /tmp/tenant_<uuid>_dump.sql
|
|
478
|
+
|
|
479
|
+
# Validar row count match
|
|
480
|
+
psql <source> -c "select count(*) from public.events where org_id = '<uuid>';"
|
|
481
|
+
psql <dedicated> -c "select count(*) from public.events;"
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
#### Passo 3 — Supavisor redirect via routing config
|
|
485
|
+
|
|
486
|
+
```toml
|
|
487
|
+
# supavisor.toml (ou config UI Supabase Dashboard)
|
|
488
|
+
# Routing rule: requests com header X-Org-Id=<uuid> vão para instância dedicada
|
|
489
|
+
[[routes]]
|
|
490
|
+
match.header = "X-Org-Id"
|
|
491
|
+
match.value = "<uuid>"
|
|
492
|
+
target = "dedicated_<uuid>"
|
|
493
|
+
priority = 100
|
|
494
|
+
|
|
495
|
+
[pools.dedicated_<uuid>]
|
|
496
|
+
host = "db.<dedicated_project_ref>.supabase.co"
|
|
497
|
+
port = 5432
|
|
498
|
+
database = "postgres"
|
|
499
|
+
mode = "transaction"
|
|
500
|
+
pool_size = 50
|
|
501
|
+
|
|
502
|
+
# Default route para os demais tenants (instância original)
|
|
503
|
+
[[routes]]
|
|
504
|
+
match.default = true
|
|
505
|
+
target = "shared"
|
|
506
|
+
priority = 1
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// App: setar header X-Org-Id em toda request
|
|
511
|
+
// Supabase JS client custom header (versão >= 2.x)
|
|
512
|
+
const supabase = createClient(url, anon_key, {
|
|
513
|
+
global: {
|
|
514
|
+
headers: { 'X-Org-Id': activeOrgId }
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Após reload Supavisor (zero downtime — connections drain gracefully), tráfego do tenant alvo vai para instância dedicada. Demais tenants seguem na instância original.**
|
|
520
|
+
|
|
521
|
+
#### Passo 4 — Cleanup conservador (após 7d sem queries)
|
|
522
|
+
|
|
523
|
+
```sql
|
|
524
|
+
-- Verificar que nenhuma query tocou as partições antigas nos últimos 7d
|
|
525
|
+
select
|
|
526
|
+
schemaname, relname,
|
|
527
|
+
last_seq_scan,
|
|
528
|
+
last_idx_scan,
|
|
529
|
+
greatest(coalesce(last_seq_scan, 'epoch'::timestamptz),
|
|
530
|
+
coalesce(last_idx_scan, 'epoch'::timestamptz)) as last_access
|
|
531
|
+
from pg_stat_user_tables
|
|
532
|
+
where relname like '%<uuid_underscore>%'
|
|
533
|
+
order by last_access;
|
|
534
|
+
-- Esperado: last_access < now() - interval '7 days' para todas
|
|
535
|
+
|
|
536
|
+
-- Apenas após confirmação manual humana, dropar
|
|
537
|
+
begin;
|
|
538
|
+
drop table if exists public.events_<uuid_underscore> cascade;
|
|
539
|
+
drop table if exists public.audit_logs_<uuid_underscore> cascade;
|
|
540
|
+
-- ... outras tabelas particionadas do tenant
|
|
541
|
+
commit;
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**Por que 7d:** janela de defesa contra rollback emergencial. Se a instância dedicada falhar por bug não detectado em customer testing, voltar tráfego para instância original em < 5min via reverter Supavisor config — só funciona se dados antigos ainda existem.
|
|
545
|
+
|
|
546
|
+
## Anti-patterns
|
|
547
|
+
|
|
548
|
+
### Anti-pattern 1: Mitigar antes de medir (sem baseline 30d)
|
|
549
|
+
|
|
550
|
+
**Errado:** "Cliente reclamou de lentidão — vamos criar MV per-tenant para ele agora."
|
|
551
|
+
|
|
552
|
+
**Por quê:** sem baseline 30d das 3 métricas (REQ TENANT-01), não dá pra distinguir hot tenant real de pico transitório (importação CSV grande, batch fim-de-mês). Mitigação prematura adiciona MV refresh overhead permanente para uma situação possivelmente pontual.
|
|
553
|
+
|
|
554
|
+
**Certo:** coletar 30d de baseline, identificar via REQ TENANT-01, confirmar com ≥ 2 das 3 métricas em WARN+ por > 7d. Só então aplicar mitigação.
|
|
555
|
+
|
|
556
|
+
### Anti-pattern 2: Particionar tabela com poucos rows
|
|
557
|
+
|
|
558
|
+
**Errado:**
|
|
559
|
+
```sql
|
|
560
|
+
-- 5 tenants, 200 rows/tenant
|
|
561
|
+
create table public.events (...) partition by hash (org_id);
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Por quê:** overhead de partition pruning + planner trabalho > benefit. Cada query passa por partition routing, dump/restore mais lento, manutenção complexa. Premature optimization clássica — DDIA p.202 nota que "each partition also has management overhead".
|
|
565
|
+
|
|
566
|
+
**Certo:** começar com tabela regular + index `(org_id, created_at desc)`. Particionar quando atingir threshold real (> 50k rows/tenant OU > 5M total).
|
|
567
|
+
|
|
568
|
+
### Anti-pattern 3: Term-partitioned como default
|
|
569
|
+
|
|
570
|
+
**Errado:** criar lookup table global (term-partitioned) já no MVP "para evitar scatter-gather no futuro".
|
|
571
|
+
|
|
572
|
+
**Por quê:** writes ficam 2× mais caros desde dia 1. Cross-partition lock complica. DDIA p.200 documenta que mesmo DynamoDB GSI (term-partitioned built-in) tem trade-off de propagation delay assíncrono. Você está pagando custo agora para benefício hipotético futuro.
|
|
573
|
+
|
|
574
|
+
**Certo:** document-partitioned como default. Migrar para term-partitioned **somente** quando query path super-admin medir > 5s P95 e for user-facing crítico.
|
|
575
|
+
|
|
576
|
+
### Anti-pattern 4: Rebalancing automático
|
|
577
|
+
|
|
578
|
+
**Errado:** script bash que detecta hot tenant via REQ TENANT-01 e automaticamente roda passos 2-3 do REQ TENANT-05.
|
|
579
|
+
|
|
580
|
+
**Por quê:** DDIA p.204 documenta cascading failure clássica — node lento detectado como dead → rebalance automático → carga extra no resto do cluster → mais nodes ficam lentos → mais rebalance → cascade. Em B2B SaaS, equivalente: importação CSV grande detectada como hot → rebalance triggered → aplicação volta-volta no meio de transação user-facing → erros 500 em produção.
|
|
581
|
+
|
|
582
|
+
**Certo:** detecção automática gera **alerta** (Slack/PagerDuty). Decisão de rebalance é humana (DBA + customer success), executada em janela de manutenção pré-comunicada.
|
|
583
|
+
|
|
584
|
+
### Anti-pattern 5: Cleanup imediato após move (sem 7d)
|
|
585
|
+
|
|
586
|
+
**Errado:**
|
|
587
|
+
```sql
|
|
588
|
+
-- Logo após Supavisor reroute (REQ TENANT-05 passo 3)
|
|
589
|
+
drop schema tenant_<uuid> cascade;
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Por quê:** se instância dedicada tiver bug não detectado (RLS quebrada, schema diverge, performance pior), você não consegue rollback. Customer fica fora do ar até nova restore from backup (RTO horas).
|
|
593
|
+
|
|
594
|
+
**Certo:** 7d de monitoring ativo (`pg_stat_user_tables.last_seq_scan`/`last_idx_scan` confirmados zero) antes do drop. Custo: 7d de storage duplicado (negligível vs custo de outage).
|
|
595
|
+
|
|
596
|
+
## Ver também
|
|
597
|
+
|
|
598
|
+
- [`../_shared-dados-distribuidos/glossary.md`](../_shared-dados-distribuidos/glossary.md) — glossário compartilhado da Suíte DDIA Foundations v1.22 (define `hot spot`, `scatter-gather`, `consistent hashing`, `key range partitioning`, etc.)
|
|
599
|
+
- [`../multi-tenant-performance-scaling/SKILL.md`](../multi-tenant-performance-scaling/SKILL.md) — Supavisor pooling, partial indexes, helper functions STABLE (skill irmã v1.21 — base de scaling antes de mitigação de hot tenant)
|
|
600
|
+
- [`../supabase-postgres-style/SKILL.md`](../supabase-postgres-style/SKILL.md) — style guide SQL canônico (snake_case, schema-qualified, `private.*` para helpers)
|
|
601
|
+
- [`../multi-tenant-rls-hierarchy/SKILL.md`](../multi-tenant-rls-hierarchy/SKILL.md) — RLS hierarchical policies que coexistem com partições (RLS aplicada na tabela parent propaga para todas as partições)
|
|
602
|
+
- [`../super-admin-platform-pattern/SKILL.md`](../super-admin-platform-pattern/SKILL.md) — cross-tenant views super-admin (caso canônico para REQ TENANT-04 term-partitioned trade-off)
|
|
603
|
+
- DDIA Ch 6 (Designing Data-Intensive Applications, Martin Kleppmann) — Partitioning. Justin Bieber tenant: p.196 nota [13]. Hash vs range: p.194-196. Secondary indexes: p.197-200. Rebalancing: p.201-204.
|
|
604
|
+
- [Postgres Declarative Partitioning Docs](https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE)
|
|
605
|
+
- [Supavisor Multi-Pool Docs](https://supabase.com/docs/guides/database/connecting-to-postgres#supavisor)
|