A Mitigação Que Especificamos e Jogamos Fora
Existe uma decisão neste projeto da qual me orgulho mais que de qualquer código, e ela não produziu nada.
A situação
Depois que o incidente de Subscribe foi estabilizado, um problema continuou de pé. Inscrever-se numa partida custava uma chamada de API externa com rate limit por tipo de notificação por dispositivo, e essas chamadas aconteciam dentro da requisição HTTP do usuário. Produção tinha isto:
{"msg":"match_subscribed","status":"partial_failure",
"subscription_count":34,"subscription_failed":2,"duration_ms":9347}
Um usuário esperou 9,3 segundos e duas das suas 36 inscrições nunca foram criadas. Nada avisou ele.
A mitigação óbvia é impedir que o backfill em massa deixe o caminho interativo passando fome. Reserve alguma fração do orçamento de taxa para inscrições iniciadas por usuário, deixe o backfill ficar com o resto, e uma pessoa tocando um botão ganha de um job agendado drenando um backlog. É um padrão bem entendido, são talvez dois dias de trabalho, e melhoraria de forma mensurável exatamente aquilo de que os usuários reclamam.
Então eu especifiquei. priority-aware-subscribe-budget: histórias, critérios de aceitação, premissas, tudo. Passou por especificação e design e chegou ao checkpoint de aprovação em 1º de setembro.
Aí eu matei. O manifesto da execução diz por quê, do jeito lacônico que essas coisas dizem:
aborted at checkpoint: interim mitigation rejected in favour of
going straight at ADR-0001 Option B
Por quê
Duas razões, e só a segunda é interessante.
A primeira é que não corrige o problema. Reservar capacidade torna a inscrição de um usuário menos provável de falhar. Não consegue torná-la impossível, porque a operação continua sendo N chamadas externas que podem falhar por conta própria, dentro de uma requisição que alguém está esperando. Escalonamento por prioridade muda as probabilidades. O modo de falha permanece no design. Isso é triagem, e triagem vale comprar quando você precisa do tempo.
A segunda razão é a que quero registrar. Fui verificar o que a mitigação protegeria, contra o que a correção real remove.
O orçamento com prioridade guardaria quatro funções de backfill. Três delas, GeneratePendingMatchSubscriptions, AddMissingHeartTeamSubscriptions e BackfillExtraTimeSubscriptions, são apagadas de vez ao mover o fan-out para dentro do nosso próprio serviço. Elas existem apenas para manter um parque de inscrições no SNS sincronizado, e não há parque depois.
Então a mitigação teria entrado, funcionado, e depois sido apagada junto com as funções que ela foi escrita para proteger. Não descontinuada devagar. Apagada no mesmo trimestre, pela mudança que já estava decidida.
A regra que eu guardaria
Uma mitigação temporária vale construir quando a correção real está longe, e vale pular quando ela protege código que a correção real remove.
As duas metades importam. Já me convenci de trabalho temporário várias vezes só com a primeira metade, porque "a correção real está a meses de distância" é quase sempre verdade quando você diz e frequentemente falso na prática. A segunda metade é verificável hoje. Abra o plano da correção real, liste as funções que a mitigação toca, e veja quantas sobrevivem. Se a maioria não sobrevive, você não está comprando tempo. Está comprando um diff que precisará ser revertido.
A distinção não é sobre quão boa é a mitigação. O orçamento com prioridade era uma peça decente de design. É sobre se a coisa que ele protege tem futuro.
A especificação não foi desperdiçada
Vale ser preciso aqui, porque "jogamos fora" convida à conclusão de que escrever foi um erro.
A especificação é o que tornou o cancelamento defensável. Antes de escrever eu tinha uma sensação vaga de que um orçamento com prioridade ajudaria. Escrever as histórias e os critérios me forçou a enumerar exatamente quais caminhos de código estavam envolvidos, que é o que trouxe à superfície a sobreposição com as remoções. O argumento para não construir saiu do trabalho de especificar.
Prefiro gastar uma manhã numa especificação que termina em cancelamento do que gastar dois dias numa implementação que termina em revert. Essa troca só está disponível se especificar é barato e existe um portão entre especificar e construir.
Que é a outra metade de por que isso aconteceu. Meu fluxo coloca um checkpoint explícito de aprovação entre o plano e a execução. Se o pipeline corresse direto da especificação para a implementação, isso teria entrado, porque tudo a respeito era individualmente razoável e nada na especificação em si dizia "verifique se a correção real apaga isto". O portão não está ali para pegar especificações ruins. Está ali para dar um momento de perguntar se a coisa que você acabou de descrever com cuidado ainda vale a pena ter.
Registrar uma opção abandonada
A última coisa que fiz foi escrever isso no ADR como uma seção própria: o que foi considerado, que chegou a um checkpoint, e por que foi rejeitado.
Trabalho abandonado normalmente não deixa rastro. A branch é apagada, o ticket é fechado como não-faremos, e seis semanas depois alguém propõe a mesma coisa, razoavelmente, porque o argumento contra vive na memória de uma pessoa. Escrever a rejeição onde a decisão vive custa um parágrafo e significa que a próxima pessoa a ter essa ideia recebe o contra-argumento junto.
Também mantém o registro honesto sobre como a decisão foi realmente tomada. O ADR se leria como se a arquitetura tivesse sido decidida de forma limpa e executada em ordem. Não foi. Algo foi especificado, levado quase até o fim, e descartado, e isso vale poder enxergar.
Parte de Removendo um Gargalo, sobre o incidente de throttling no SNS Subscribe e a migração para entrega direta via FCM.