Always-on
Todo componente Koder é desenhado pra estar **sempre no ar e operante**, em todos os sentidos: usuários em qualquer versão (N, N-1, N-2 dentro da janela suportada) conseguem usar; rollout de nova versão atravessando milhares de servidores por dias não causa quebra em ponta alguma; servidor cai, rede oscila, dependência falha — produto degrada com graça, nunca trava nem perde dado. Maintenance window é anti-padrão. Flag day é anti-padrão. "Atualize todo mundo antes de mudar" é anti-padrão. A inviolabilidade da experiência atravessa **versão, topologia, conectividade e surface**.
Policy: always-on
*rincípio mandatory* a Koder Stack está *empre no ar e operante* Toda decisão de design considera, desde o primeiro commit, que diferentes versões do mesmo componente vão coexistir em produção, que rede e dependências falham, que clientes ficam offline, que rollouts levam dias, e que o usuário não pode perceber nada disso. Disponibilidade não é feature — é piso.
Por que
Alvo da Stack: *00M+ contas ativas, multiregião, multisurface* Nesse regime, três fatos são inescapáveis:
- *ollout não é instantâneo.*Atualizar uma frota de milhares de
servidores + bilhões de clients (mobiledesktopTV/web) leva *ias ou semanas* Durante essa janela, N, N
1, e às vezes N2 coexistem em produção. Nenhuma versão da matriz pode quebrar. - *alhas são contínuas, não excepcionais.*Em qualquer minuto,
algum DC tá com latência elevada, algum link cai, algum hardware morre, alguma dependência rate-limita. "Funciona quando tudo tá ok" é equivalente a "não funciona".
- * usuário não negocia.*O usuário com Koder Talk v3.2 no celular
antigo e Koder Flow v4.1 no servidor self-hosted da empresa *ão tem que saber disso* Ele aperta o botão. Tem que funcionar.
Custo de *azer certo desde o começo* discipline + cobertura de testes. Custo de *etrofit* incidentes, suporte 24/7 caro, perda de confiança, churn. Esta policy existe pra que toda IA / engenheiro saiba que disponibilidade é loadbearing igual segurança ou multitenancy — não opcional, não "a gente vê depois".
Dimensões — visão geral
| # | Dimensão | O que protege |
|---|---|---|
| §1 | Coexistência de versões N / N |
Rollout multi-dia atravessa a frota sem quebrar nenhuma combinação |
| §2 | Wire |
Cliente novo lê payload velho; cliente velho lê payload novo |
| §3 | Migrações de schema zero-downtime | Schema evolui sem janela, sem flag day, sem coordenação global |
| §4 | Sem janela de manutenção | Deploy nunca derruba; restart nunca é "todo mundo ao mesmo tempo" |
| §5 | Graceful degradation | Dependência caída → feature desabilita com mensagem clara, resto opera |
| §6 | Resumabilidade + tolerância offline | Operação interrompida continua de onde parou; cliente offline guarda fila |
| §7 | Multi-região / sem SPOF | Queda de DC degrada latência, não derruba o produto |
| §8 | Cross-surface compatibility | Mobile v5 conversa com desktop v3; handshake negocia capacidade |
Cada dimensão tem cláusulas normativas (**, templates de teste (** e gate de release.
§1. Coexistência de versões N, N1, N2
R1.1 — Janela suportada
Todo componente declara explicitamente sua *anela de versões coexistentes*em koder.toml:
[compat]
window_minor_versions = 2 # N, N-1, N-2 devem interoperar
window_major_versions = 1 # major bump é breaking — separado
window_duration_days = 180 # tempo mínimo de suporte a N-1 em produçãoDefault Stack-wide: * minor + 1 major + 180 dias* Componentes podem TIGHTEN (mais janela), nunca LOOSEN.
R1.2 — Capability handshake
Toda conexão cliente↔servidor inicia com *andshake de capability*
- Cliente envia:
{client_version, supported_features: [...]} - Servidor responde:
{server_version, accepted_features: [...]} - Cliente opera no *ntersection*das features aceitas
Anti-padrão: cliente assume features pelo número de versão do servidor. Versão é informativa; *apability é normativo*
R1.3 — Nenhum endpoint exige "estamos todos na mesma versão"
Endpoints, RPCs, protocolos, eventos: todos têm que tolerar contraparte em qualquer versão dentro da janela. *unca*
- "404 because client is too old" → degrade, não falhe
- "500 because field is missing" → trate como ausente, não como erro
- "Force upgrade" pop-up bloqueante → só permitido após R1.1 expirar
R1.4 — Deprecation com prazo, não com flip
Remover featurecampoendpoint segue 3 fases:
- *nnouce*(release N): marca deprecated, log warning server-side,
doc no CHANGELOG, ticket de migração consumidores
- *uiet*(release N+1, ≥ 90 dias): feature ainda funciona, sem
warning ruidoso; consumidores conhecidos já migraram
- *emove*(release N+2, ≥ 180 dias após N): feature sai
Pular fase = violação. Bug fix de segurança que requer remoção imediata vai pro exception path (§ Exceptions).
§2. Wireformat estável + forwardcompat
R2.1 — Unknown-field tolerance é mandatório
Toda biblioteca de serialização usada por componente Koder DEVE preservar campos desconhecidos em round-trip:
- Protobuf:
unknown_fieldspreservado (default proto3 ≥ 3.5) - JSON: parser preserva extra keys e re-serializa; nunca
strict: trueem path crítico de wire
- KMD / KVG / KPKG: parser TEM que skipar diretivas desconhecidas (já
é spec — ver
specs/document-format.kmd) - gRPC / RPC custom: idem proto
Anti-padrão: json.Unmarshal em struct sem json:",omitempty" + re-serialização dropando campos do payload original. Cliente N+1 manda campo novo, gateway N strips, servidor N+1 não recebe → bug.
R2.2 — Versão de schema é first-class
Todo payload de wire (RPC, evento, file format) carrega *chema version tag*no envelope:
{ "v": 3, "kind": "MessageDelivered", "payload": { ... } }Schema bump é semver:
- *atch*(3.0 → 3.1): só adiciona campos opcionais
- *inor*(3.x → 4.0): adiciona campos required dentro da janela
(com default sane no parser legado)
- *ajor*(3.x → 4.0 breaking): rara; passa por R1.4 deprecation
R2.3 — Enum é extensível
Enums em wire format SEMPRE têm valor unknown = 0 (ou equivalente) e parser trata unknown como graceful degradation:
enum MessageKind {
MESSAGE_KIND_UNSPECIFIED = 0; // forward-compat reserved
MESSAGE_KIND_TEXT = 1;
MESSAGE_KIND_IMAGE = 2;
// future: MESSAGE_KIND_VIDEO = 3 (cliente N entende como UNSPECIFIED)
}Anti-padrão: enum fechado sem fallback → cliente velho recebe valor novo, crash em switch exhaustive.
R2.4 — Forbidden breaking wire changes (sem deprecation)
| Mudança | Status |
|---|---|
| Adicionar campo opcional | ✓ OK qualquer momento |
| Adicionar enum value | ✓ OK qualquer momento |
| Adicionar endpoint novo | ✓ OK qualquer momento |
| Mudar tipo de campo existente | ✗ Major + deprecation |
| Renomear campo | ✗ Adicionar novo + deprecate antigo (R1.4) |
| Remover campo | ✗ Major + deprecation |
| Mudar semântica sem mudar tipo | ✗ Pior caso possível — proibido |
§3. Migrações de schema zero-downtime
R3.1 — Padrão expand → migrate → contract
Toda migração de schema (SQL, kdb, KV namespace, file layout) que muda forma de dado segue * releases*mínimo:
- *xpand*(release N): adiciona nova forma; código N escreve em
*mbas*as formas; lê preferencialmente da nova, fallback pra antiga
- *igrate*(entre N e N+1): backfill assíncrono converte rows
antigas pra forma nova (job em background, idempotente, resumível, per-tenant)
- *ontract*(release N+1, depois de migrate confirmado): código
pára de escrever na forma antiga; opcionalmente dropa coluna/tabela
Validar que *+1 e N coexistem*durante o intervalo de rollout.
R3.2 — NOT NULL só depois de backfill
Adicionar NOT NULL a coluna existente é proibido sem 3 passos:
- ADD COLUMN (nullable, com default sane se aplicável)
- Código novo preenche em todo write; backfill async preenche rows
velhos
- *epois de confirmar 100% preenchido*→ ALTER TO
NOT NULL
Postgres / kdb: ALTER NOT NULL direto em tabela grande TRAVA escritas — é um maintenance window disfarçado. Proibido.
R3.3 — DROP é a coisa mais cara
Dropar coluna, tabela, índice, KV namespace, file blob:
- Stop reading (release N): código não lê mais; ainda escreve
- Stop writing (release N+1): código não toca mais
- *ait ≥ janela R1.1*depois do release N+1 entrar em 100% da
frota
- Drop fisicamente (release N+2 ou migration manual)
Se a janela R1.1 é 180 dias, dropar coluna leva ~6 meses calendartime. Esse é o preço de alwayson.
R3.4 — Online DDL obrigatório
Toda DDL em tabela ativa usa modo online do DB:
- Postgres:
CREATE INDEX CONCURRENTLY,ALTER TABLE ... ADD COLUMN ... DEFAULT ...sem rewrite (Postgres ≥ 11),pg_repackpra reorder - kdb: schema change via versioned descriptor; nunca lock global
- KV: novo namespace + dual-write + cutover
DDL bloqueante em tabela > 10k rows em produção = release block.
§4. Sem janela de manutenção
R4.1 — Deploy é rolling, sempre
Nenhum componente Koder tem caminho legítimo de "stop everything, deploy, start". Toda release suporta:
- *olling restart* instâncias atualizam uma de cada vez, traffic
drena, healthcheck confirma antes de seguir
- *oexistência durante rollout* instâncias N e N-1 servindo tráfego
ao mesmo tempo, possivelmente por horas
R4.2 — Healthcheck reflete prontidão real
/healthz (ou equivalente) só responde 200 quando:
- Dependências essenciais respondem
- DB pool aquecido
- Cache pré-populado (se aplicável)
- Capability handshake operacional
Anti-padrão: /healthz retorna 200 desde o main() — load balancer manda tráfego pra instância que ainda não conecta no DB → 500 pro usuário.
R4.3 — Graceful shutdown
SIGTERM → drain phase (≥ 30s default, configurable):
- Pára de aceitar conexões novas
- Termina conexões em-flight (com timeout sane)
- Flush de buffers / cache write-back / commit de transações
- Exit clean
Conexão long-lived (WebSocket, gRPC stream, SSE): server manda *econnect hint*ao client antes de derrubar; client reconecta em instância nova.
R4.4 — "Maintenance mode" é proibido como default
Página/banner "estamos em manutenção" só é aceitável em *ncidente declarado*(perda de DC, exploit ativo). Nunca como ferramenta de rollout. Toda PR que introduz maintenance toggle em path normal = block.
§5. Graceful degradation
R5.1 — Falha de dependência opcional ≠ falha do produto
Dependência indisponível → feature dependente desabilita com mensagem clara; o resto do produto continua funcional.
Exemplos concretos:
| Dependência cai | Comportamento correto |
|---|---|
| AI gateway lento | Botão "Gerar com IA" mostra spinner + fallback "tente mais tarde"; resto do editor funciona |
| Search index morto | Busca avisa "indisponível"; navegação manual funciona |
| Push notification rejeita | Mensagem chega quando user abre o app (pull funciona) |
| Telemetry endpoint 503 | Buffer local + retry; *unca*bloqueia ação do user |
| Auto-update server timeout | App roda na versão atual; tenta de novo depois |
Anti-padrão: erro 500 do gateway de notificação → app trava tela do user.
R5.2 — Dependência essencial: fail-fast + circuit breaker
Dependência sem a qual a feature não pode operar:
- Circuit breaker (típico: 5 falhas em 30s → abre por 60s)
- Erro user
facing claro (`specserrorsuserfacing-messages.kmd`) - Telemetria de degradação para oncall
- Re-tenta no fundo; recupera sozinho quando dep volta
R5.3 — Timeout em toda I/O network
context.WithTimeout (Go) / equivalente em qualquer linguagem: *enhuma*chamada de rede sem timeout. Default sane (5s pra intraDC, 30s pra widearea). Timeout estourar é graceful degradation, não panic.
R5.4 — Retry com backoff exponencial + jitter
Retry sem jitter cria *hundering herd*depois de incidente. Mandatório:
- Backoff exponencial (base 100ms, factor 2, max 30s)
- Jitter ≥ 25% (decorrelação)
- Cap de retries (5 default)
- Idempotency-key em retry de mutation
§6. Resumabilidade + tolerância offline
R6.1 — Operações longas são resumíveis
Upload, download, sync, import, export, batch processing: cada um tem checkpoint. Conexão cai / processo morre / power cut → operação retoma de onde parou na próxima tentativa.
Protocolo:
- Upload: chunked (chunks ≤ 8 MiB), server confirma cada chunk,
cliente persiste cursor local; resume = "continue do cursor"
- Download: HTTP Range request (
Range: bytes=N-) - Sync: vector clock / lamport timestamp / opaque cursor + tombstones
- Batch: per
item commit + pertenant checkpoint
R6.2 — Cliente offline é estado normal
Mobile / desktop apps assumem que *onectividade é intermitente*
- Toda ação grava local primeiro (SQLite, kdb embarcado, file)
- Fila de outbound operations persistente
- Sync ao reconectar; conflict resolution explícita (last
writerwins é raramente correto) - UI mostra estado: synced / pending / failed
- *ead*continua funcionando 100% offline em features locais
Anti-padrão: app sem conexão mostra tela branca / erro genérico / desabilita botão sem explicação.
R6.3 — Idempotência de mutations
Toda operação que altera estado server-side aceita *dempotency key*(cliente gera UUID v4 / ULID, repete em retry):
POST /messages
Idempotency-Key: 01HX...
{ "thread_id": ..., "text": "..." }Servidor garante: 2 requests com mesma key → mesmo efeito + mesma resposta. Sem idempotency: retry de cliente offline duplica mensagem quando volta online.
R6.4 — Sessão sobrevive a reconnect
WebSocket / SSE / gRPC stream: cliente reconecta dentro de janela (≥ 5 min default) e recupera state sem re-autenticar.
§7. Multi-região / sem SPOF
R7.1 — Nada que toca usuário fica single-region
Storage de dado de usuário, identity service, gateway de tráfego, DNS: tudo replicado entre ≥ 2 regiões geograficamente distintas.
Single-region OK só pra:
- Componente interno (build farm, CI)
- Sandbox / dev / staging
- Catálogo público read-only (com CDN cache global)
R7.2 — Failover é automático
DC down → traffic re-routa em ≤ 60s sem ação humana:
- DNS com health-check (≤ 30s TTL pra A/AAAA críticos)
- Anycast onde aplicável
- Replicação de DB async cross-DC; failover promove read replica
- Healthcheck em cada região; orchestrator (ou Jet) tira região doente
R7.3 — Read replicas próximas do usuário
Latência percebida é orçamento finito. Reads críticos (homepage, feed, search) servem da região mais próxima do user (geo-DNS ou anycast).
R7.4 — Backups são uma forma de "always-on"
Perda de dado é o oposto extremo de always-on. Backup:
- Snapshot incremental ≥ 1×/hora pra dado mutable
- Off
site (crossregion + cold storage) ≥ 1×/dia - Restore testado mensalmente em ambiente isolado
- Ver
policies/checkpoint-retention.kmdpra retenção
§8. Cross-surface compatibility
R8.1 — Surfaces conversam por contrato, não por versão
Mobile / desktop / TV / web / CLI do mesmo produto Koder podem estar em versões diferentes do SDK (koder_kit, koder_web_kit, etc.) e do produto. Quando trocam state (sync, IPC, shared session, KMD doc compartilhado), trocam por *ontrato versionado*(R2.2 + R1.2).
R8.2 — Format de documento é forward-compat por construção
KMD, KVG, KPKG, qualquer file format Koder: parser ignora diretivas desconhecidas e preserva no save round-trip. Doc criado em desktop v3 abre em mobile v2 (sem features novas, sem perder bytes); abre em desktop v4 (com features novas) sem upgrade ritual.
R8.3 — IPC entre apps Koder (ver specs/ipc/protocol.kmd)
Protocolo IPC negocia versão no handshake. App receiver descobre que sender tá em versão antiga → entrega só features comuns. Nunca crasha por payload "muito novo" ou "muito velho".
R8.4 — Coverage matrix
Cada produto Koder publica em registries/variant-compat-matrix.md qual combinação de surfaces × versões foi testada. Combinações não testadas dentro da janela R1.1 = bug latente; CI bloqueia release sem coverage mínimo (T8 abaixo).
Templates de teste mandatórios
T1 — Matriz de versões N × N-1
Para cada componente client+server: rodar suite em *odas*as combinações dentro da janela R1.1.
client v3.2 ↔ server v3.2 ✓
client v3.2 ↔ server v3.1 ✓
client v3.1 ↔ server v3.2 ✓
client v3.0 ↔ server v3.2 ✓ (N-2)
client v3.2 ↔ server v3.0 ✓Implementação: tests/compat/ com dockercompose multiversion ou matrix CI.
T2 — Unknownfield roundtrip
Parser recebe payload com campo novo desconhecido → preserva no reemit. Asserção byteequal (depois de normalização) em todas as libs de serialização que o componente usa.
T3 — Rolling upgrade simulado
Em ambiente de teste: 3 réplicas, fazer rolling restart com tráfego ativo (load generator), assertir 0 erros 5xx percebidos pelo cliente durante a janela.
T4 — Schema migration em produção-likeness
Migration aplicada em snapshot de prod-like (millions of rows), medir locks, downtime, falha de queries concorrentes. Reject se qualquer query bloqueia > 100ms.
T5 — Chaos test de dependência opcional
Mata dependência opcional (AI gateway, search, notification): produto continua respondendo 200 nas features que não dependem dela; feature dependente degrada conforme R5.1.
T6 — Resumability de upload
Upload de 100 MiB. A 50%: kill -9 cliente. Reabrir cliente, retomar upload, assertir bytes finais iguais ao original (bytebybyte).
T7 — Offline tolerance de mobile/desktop
Disable network → execute fluxo principal → re-enable network → assert sync completa sem perda nem duplicação.
T8 — Cross-surface compat coverage
Matrix de surfaces (mobile × desktop × web × TV × CLI) × pares de versões. Mínimo: surface mais recente × surface mais antiga da janela.
T9 — Failover regional
Simula queda da region primária; mede tempo até traffic re-roteado; assertir < 60s e zero data loss.
Gate de release
Componente NÃO entra em release pipeline se:
- Janela R1.1 não declarada em
koder.toml - T1 falha em qualquer combinação na janela
- Schema migration sem plano expandmigratecontract documentado
/healthzretorna 200 antes de dep essencial estar pronta (T3 detecta)- Operação longa sem checkpoint (T6 detecta no path crítico)
- I/O network sem timeout (audit estático)
- Cliente sem retry com jitter (audit estático)
- Coverage matrix T8 < 80% da janela
- Sem failover testado (T9) pra componente single-region servindo user data
Severity:
- *rror*(block release): R1.1, R2.1, R2.3, R2.4, R3.1, R3.2, R4.1, R5.3, R6.3
- *arning*(advisory): R5.4, R6.4, R7.3, T8 < 100%
Anti-padrões explícitos
| Anti-pattern | Por que dói | Correto |
|---|---|---|
| "Atualizem todos os clientes antes do dia X" | Flag day — impossível em escala | R1.1 + R1.2 |
if version != current { return 426 Upgrade Required } |
Quebra metade da frota durante rollout | Capability handshake R1.2 |
json.Unmarshal strict mode em wire path |
Drops campos novos | R2.1 |
ALTER TABLE ADD COLUMN NOT NULL DEFAULT now() em tabela ativa |
Lock até backfill, downtime | R3.2 expandmigratecontract |
| Maintenance page "voltamos em 2h" pra rollout | Default = não disponível | R4.4 |
panic em código de produção |
Mata processo, mata user | Recovery + erro user-facing |
| Upload de 1 GB sem chunk | Falha 80% das vezes em conexão móvel | R6.1 chunked + resumable |
| Cron global sem checkpoint per-tenant | Reprocessa tudo após crash | R6.1 + multi-tenant |
| Single-region DB pra dado de user | Queda do DC = perda do usuário | R7.1 |
| App offline mostra tela branca | Sem comunicação = sem confiança | R6.2 |
| Retry sem jitter | Thundering herd | R5.4 |
| Enum exhaustive switch sem default | Crash em valor novo | R2.3 |
Exceptions
Caminho legítimo pra burlar always-on:
- *ecurity fix com remoção imediata necessária*— CVE crítico
exige drop de feature antes da janela expirar. Aprovado caso
acaso pelo owner; documentado emmeta/context/incidents/. - *ev / staging environments*— testes destrutivos OK em ambientes
internos.
- *omponentes pré
policy (≤ 202605-24)*— não exige refactorimediato; próximo refactor substancial aplica. Lista em
meta/docs/stack/registries/always-on-debt.md(a ser criado quando primeira violação for descoberta).
Exception NÃO é:
- "Tô com pressa pra essa release" → no.
- "Esse componente é só interno" → ainda assim, dev experience é always-on.
- "Mas é só um experimento" → experimentos vivem em
Koder/sandbox,fora do gate de release; produção tem o gate.
Conflicts with / composes with
- *policies/hyperscale
first.kmd`*(escala): alwayson é o eixode disponibilidade; hyperscale é o eixo de throughput. Os dois são ortogonais e ambos mandatórios.
- *policies/multi
tenantbydefault.kmd`* migrações zerodowntimeda §3 aplicam *er-tenant* nunca derrubam ou trancam tenants alheios à mudança.
- *policies/headless
first.kmd`* testes T1T9 acima são headlesspor construção; usa o mesmo SDK de teste canônico.
- *policies/releases.kmd`* rolling deploy + janela R1.1 entram
no pipeline.
- *policies/stack
principles.kmd`*(Speed > Quality): alwaysonvence; "ship rápido" não justifica flag day.
- *specsvariantstaxonomy.kmd`* matriz de variantes × versões
é o que T8 cobre.
Migration path
Componentes prépolicy (existentes em 202605-24): não exige refactor imediato.
- Próxima release MAJOR de cada componente aplica esta policy
- Novos endpoints / wire formats / schemas em qualquer componente
seguem a policy desde o primeiro commit (mesmo que o resto do componente esteja em débito)
- Débitos catalogados em
meta/docs/stack/registries/always-on-debt.md - Auditoria automática (futura):
koder-spec-audit always-on --scope <component>
Significance
Sem essa policy, o Stack pode passar todos os outros gates (hyperscale, multi-tenant, security, headless) e ainda assim viver em incidentemododefault: cada release é uma manhã estressante, cada servidor antigo é uma bomba-relógio, cada cliente desatualizado é um ticket de suporte, cada DC com problema é um post-mortem. * diferença entre "produto Koder" e "produto Koder operável em escala planetária" mora aqui.*
A policy é o equivalente de "always use TLS" pra cleartext network: todo mundo sabe; precisa estar escrito formalmente pra ser enforced.