Um Registro de Decisão Que Discute Consigo Mesmo

O registro de decisão de arquitetura desta migração tem 924 linhas, muito mais do que um ADR deveria ter. A maior parte do excesso é uma coisa: seis correções, datadas, inline, acima do raciocínio que elas derrubam.

Eu não planejei isso. Aconteceu porque o documento foi escrito antes do trabalho e consultado durante ele, então cada vez que produção o contradizia eu tinha a escolha entre editar a parte errada para fora ou escrever por que ela estava errada. Escolhi a segunda vezes suficientes para virar o formato, e hoje é a propriedade do documento que eu mais gostaria de manter.

A correção que foi mais difícil de escrever

Essa tem um título próprio no arquivo: este ADR exagerou o que nós possuímos.

O documento inteiro descreve a opção escolhida como entrega direta via FCM e APNs, sob a premissa de que este serviço possuiria as duas pontas. Apple para dispositivos iOS, Google para Android. Esse enquadramento atravessa os fatores de decisão, os prós e contras, e a sequência de migração, e é o que justificou construir um sender APNs como estágio pré-requisito.

Está errado. Toda platform application do SNS na conta de produção é uma aplicação GCM. Não existe nenhuma platform application APNS. Todo dispositivo, iOS incluído, está registrado como endpoint numa única aplicação Firebase, e o Firebase possui a perna até a Apple. Os tokens em r10_device.device_token são tokens de registro do Firebase, não tokens de dispositivo APNs.

Então FCM direto alcança exatamente os mesmos dispositivos que o SNS alcançava, e nenhum roteador de plataforma é necessário. Um roteador estava planejado como estágio de migração, com critérios de aceitação escritos. Seria código morto no dia em que entrasse: um despachante escolhendo entre dois senders onde toda entrada vai para o mesmo.

Existe uma segunda razão, independente, pela qual não teria funcionado. A coluna na qual ele iria ramificar, r10_device.operating_system, nunca é atribuída em nenhum código que não seja de teste. Ela existe, não é consultada em lugar nenhum, e não guarda nada.

Dois fatos, qualquer um deles invalidando um estágio planejado, os dois descobríveis lendo a conta de produção e o schema. Eu havia escrito o plano a partir do design em vez de a partir do sistema, que é o mesmo erro de assumir que o repositório é o sistema em execução.

Correções que mudaram o que foi construído

Três mais, brevemente, porque cada uma redirecionou trabalho.

O harness de comparação deveria esperar a AWS retirar o limite protetivo antes de medir, para que os dois lados fossem comparáveis. Aquele raciocínio estava de trás para frente e teria descartado a janela mais informativa disponível, por razões que são o assunto inteiro de outro post.

O documento afirmava que o FCM oferecia um endpoint multicast aceitando 500 tokens por requisição, e usou isso para argumentar que a cauda do fan-out seria modesta. Aquele endpoint foi descontinuado e parou de funcionar em junho de 2024. Nem Firebase nem Apple têm multicast, então um fan-out de 2.400 dispositivos são 2.400 requisições HTTP em qualquer um dos dois, e o que governa a cauda é concorrência limitada em vez de lote.

E o tipo de notificação sombreado mudou, porque a primeira escolha disparava no máximo uma vez por partida e não teria produzido dado por dias.

Uma previsão errada na direção útil

Depois que o provisionamento foi desligado eu escrevi que o throttling declinaria devagar conforme o parque de tópicos existente envelhecesse.

Ele parou num penhasco, no minuto em que o scheduler pegou a flag: cerca de 44 recusas por minuto antes, 0,07 depois. O throttling vinha sendo dirigido quase inteiramente por provisionamento novo e não pelo milhão de inscrições já de pé, o que significa que as remoções que eu havia agendado como estágio seguinte são arrumação e não a correção.

Manter a previsão errada ao lado da medição vale mais para mim que apagá-la, porque o erro é diagnóstico. Eu tinha um modelo mental em que um parque grande de pé exerce pressão. Não exerce. Só rotatividade exerce. É um fato geral sobre control planes que hoje eu sustento com alguma confiança e não teria notado se o documento tivesse sido simplesmente atualizado para dizer o que acabou sendo verdade.

Um comentário sobre outro sistema, de novo

Uma correção pequena da mesma família de algo sobre o qual escrevi durante a extração de assinaturas.

Um comentário em internal/models/notification.go explicava que o app mobile parseia uma certa chave de wrapper no payload da notificação. Ele não parseia. O SNS parseia aquela chave, desembrulha, e repassa o objeto interno com um token de dispositivo injetado. O cliente Flutter lê os campos planos de dentro.

Errar isso na direção que o comentário sugeria significaria preservar um wrapper que o app nunca vê, ou pior, decidir que o payload tinha que mudar. O que resolveu foi um teste afirmando igualdade byte a byte entre o que o sender direto produz e o que o caminho do SNS produzia, nas formas silenciosa, visível e sem imagem. O payload é comprovadamente idêntico, então o cutover não pode ter mudado o que qualquer aparelho recebe.

Um comentário descrevendo outro sistema é um instantâneo sem dono e sem teste. Este é o segundo desses a me custar uma tarde em dois meses.

Um estágio construído fora de ordem, de propósito

A sequência de migração no documento é numerada, e o estágio 2c foi inserido entre 2 e 3 depois do fato.

A premissa do estágio 3 é que a sombra validou a query de destinatários. A sombra só chegou em produção em 2 de setembro, então por um dia não havia veredito para ler nem forma honesta de iniciar o estágio que depende de um. Em vez de esperar, construí o sender de alerta APNs, que é trabalho de que a migração precisa e que não depende em nada da resposta da sombra.

Aquele sender continua apagado. Não é pré-requisito de cutover, pela razão da primeira correção acima. O que ele é pré-requisito é para um dia remover o salto pelo Firebase no iOS, que é uma decisão separada com justificativa de latência própria e nenhuma urgência atrás.

Numerar uma sequência sugere que a ordem é estrutural. Às vezes só as dependências são, e encontrar a peça de trabalho que não depende daquilo que você está esperando é mais útil que respeitar os números.

Um bug preservado deliberadamente, e escrito como pergunta

A última coisa que vale destacar não é uma correção, e sim uma pergunta aberta que o documento se recusa a fechar.

Desligar a notificação de melhor jogador não impede que ela chegue. Ela é carregada no tópico do time, e o backfill de inscrições inscreve dispositivos naquele tópico sem consultar a tabela de preferências de notificação.

A nova query de fan-out com escopo de time reproduz isso fielmente. Ela lê os mesmos usuários, incluindo os que desligaram a notificação, e envia para eles.

Corrigir durante o cutover era a coisa óbvia e teria sido errado. Uma migração que também muda quem recebe uma notificação torna toda diferença de entrega posterior impossível de atribuir: qualquer mudança nos números pode ser o transporte ou a audiência, e não há como separar depois. Então o comportamento é preservado e o defeito é registrado como pergunta para quem for dono das preferências de notificação, que é uma decisão de produto e não de transporte.

É a mesma regra do teste de igualdade de payload, pela outra direção. Durante uma migração, fidelidade ao comportamento existente é o que torna a migração mensurável.

O que faz isso funcionar

O mecanismo é barato. Corrija no lugar, deixe o original visível, date os dois, diga qual medição ou qual linha de código resolveu.

O valor é que o documento registra raciocínio e não conclusões. Um registro editado para parecer certo é indistinguível de um registro que estava certo, e a diferença importa na próxima vez que alguém tiver que decidir se confia nas estimativas dele. O meu agora diz, em seis lugares, exatamente quão erradas as minhas estimativas estavam e em qual direção, que é a única base honesta para ler as estimativas que ainda não foram testadas.

Também impede o ADR de se tornar um documento que concorda com o que o código faz no momento, que é o destino usual e o torna inútil como registro de qualquer coisa.

O custo é que ele é longo, e lê em alguns trechos como uma discussão entre duas pessoas que por acaso são a mesma pessoa uma semana depois. Eu prefiro isso a um documento arrumado que eu não poderia auditar.


Parte de Removendo um Gargalo, sobre o incidente de throttling no SNS Subscribe e a migração para entrega direta via FCM.