Skip to content

feat: keep exact redirects out of isolate memory, one KV key each - #553

Open
JonasJesus42 wants to merge 1 commit into
mainfrom
feat/redirects-kv-keys
Open

JonasJesus42 wants to merge 1 commit into
mainfrom
feat/redirects-kv-keys

Conversation

@JonasJesus42

@JonasJesus42 JonasJesus42 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Substitui a decisão que eu tinha tomado errado no #548 ("uma cópia só dentro do snapshot é o ganho inteiro"). Aquilo só valeria se a chave fosse uma lista. Com uma chave por rota, a lista nunca entra em memória — que é onde está o ganho de verdade num site cujo decofile é majoritariamente redirect.

Problema

Dezenas de milhares de regras dentro do decofile ficam residentes duas vezes em cada isolate: no grafo do snapshot parseado, e no RedirectMap construído a partir dele. Megabytes de um orçamento de 128 MB por isolate sem botão de GC, para uma tabela de lookup consultada no máximo uma vez por request e que quase sempre não casa com nada.

Mudança

Cada regra exata sai do decofile e vira uma chave redirect:<deployment-id>:<path>. O request consulta só o path pedido.

Glob fica no decofile. /old/* precisa ser varrido em ordem contra o path — nunca vira lookup por chave. São dezenas, não milhares.

Chaveado por deployment id como todo o resto do fast-deploy: rollback continua coerente (código velho lê o conteúdo dele), o isolamento entre deploys se mantém, e o GC já existente prunes junto.

Três coisas load-bearing e não-óbvias

  1. A ordem é exato → KV → glob, não "matchRedirect e depois KV". Senão um glob ganha de uma regra exata e a precedência inverte. Por isso matchRedirect virou matchExactRedirect + matchPatternRedirect (compostos, sem mudança de comportamento).
  2. <path> é normalizePath(from) nos dois lados. normalizePath passou a ser exportado exatamente porque é o contrato da chave — uma segunda normalização em qualquer ponta grava a regra numa chave que ninguém pergunta.
  3. Edição de redirect propaga por TTL de 60s no isolate, não pelo poll de revisão. As regras estão fora do decofile agora, então um sync só-de-redirect deixa o snapshot byte-idêntico e index:revision:<id> não mexe. Se esse atraso um dia precisar ser ~zero, o caminho é uma chave index:redirects-revision:<id> polada junto — não um TTL menor, que só multiplica leitura de KV. Está comentado no código e no doc.

Add / update / remove

Reconciliado contra um LIST por prefixo, não contra uma chave de manifesto: o KV não tem "substitua tudo sob este prefixo", e manifesto é mais uma coisa que pode divergir da realidade. Update não é detectado por valor — reescrever tudo é um bulk write, ler cada valor de volta pra comparar seria um GET por chave. Só o delete precisa do diff.

GC apaga as chaves de redirect do deployment prunado, senão cada deploy vaza dezenas de milhares de órfãos. putMany/deleteMany no cliente REST (bulk, 10k por request) porque um request por chave não é forma que sobrevive a 18k regras.

Todo modo de falha resolve pra "sem redirect", nunca throw: queda de KV não pode 5xxar uma página que renderizaria normal.

Testes

21 casos novos. Cobrem o round-trip do split (nada matchável se perde), a precedência exato-em-KV sobre glob-em-memória, cache de HIT e de MISS (o miss é o caso comum de todo request), re-leitura após TTL, inércia com fast-deploy desligado, recusa a ler chave de outro deployment, e KV fora do ar / valor malformado.

Depende de #548 (os redirects de CSV precisam estar no que o sync lê antes de serem extraídos).

🤖 Generated with Claude Code


Summary by cubic

Moves exact redirect rules out of the decofile into one KV key per rule (redirect:<id>:<path>), so a site with tens of thousands of redirects no longer holds the full list resident in every isolate. Only the requested path is looked up per request; glob rules stay in the decofile since they must be scanned in order.

Behavior

  • Matching order is now exact (memory) → KV → glob; matchRedirect is split into matchExactRedirect and matchPatternRedirect so a glob never beats an exact rule.
  • normalizePath is now exported as the key contract; the writer and request-time reader must produce identical bytes.
  • Redirect edits propagate on a 60s TTL cache (hits and misses), not the revision poll, since redirect-only syncs leave the snapshot byte-identical.
  • Sync reconciles against a prefix LIST using new bulk put/delete on the KV REST client (10k per request); GC prunes a deployment's redirect keys alongside its snapshot.
  • Every KV failure (outage, malformed value, missing deployment id) resolves to "no redirect", never a throw.

Migration

Written for commit 4ea6df7. Summary will update on new commits.

Review in cubic

A bulk-migration site can carry tens of thousands of redirect rules. Inside
the decofile they are resident in every isolate TWICE — once in the parsed
snapshot graph, once in the RedirectMap built from it — for data consulted
at most once per request and that usually matches nothing. On a 128MB
per-isolate budget with no GC knob, that is megabytes for a lookup table.

So the sync now splits them out: every EXACT rule leaves the decofile and
gets its own `redirect:<deployment-id>:<path>` key, and the request path
looks up only the path being requested. The list is never loaded.

Glob rules stay in the decofile. `/old/*` has to be scanned in order
against the path, so it can never be a key lookup — and there are tens of
them, not thousands.

Three things that are load-bearing and non-obvious:

- Matching order is exact → KV → glob, not "matchRedirect then KV".
  Otherwise a glob wins over an exact rule and the precedence inverts, so
  `matchRedirect` is split into `matchExactRedirect`/`matchPatternRedirect`.
- `<path>` is `normalizePath(from)` on BOTH sides. `normalizePath` is now
  exported precisely because it is the key contract; a second
  normalization anywhere stores rules under keys nothing asks for.
- A redirect edit propagates on a 60s isolate TTL, not the revision poll.
  The rules are outside the decofile now, so a redirect-only sync leaves
  the snapshot byte identical and `index:revision:<id>` never moves. The
  upgrade path, if that delay ever has to be near-zero, is a separate
  redirects-revision key — not a shorter TTL, which just multiplies reads.

Add/update/remove reconciles against a prefix LIST rather than a manifest
key: KV has no "replace everything under this prefix", and a manifest is
one more thing that can drift from reality. GC deletes a pruned
deployment's redirect keys too, or every deploy leaks tens of thousands of
orphans. Bulk put/delete because one request per key is not a shape that
survives 18k rules.

Every failure mode resolves to "no redirect", never a throw: a KV outage
must not 5xx a page that would otherwise render.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JonasJesus42
JonasJesus42 requested a review from a team September 14, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant