Blogue ·
Uma junção em vez de uma reconciliação
Um passo de automação congelou no estado que viu. A correção não era um processo de fundo: aquela linha explica uma decisão, e repará-la destrói a explicação.
Por Provibr
Um fluxo de automação pode enviar um comando a uma máquina e esperar por ele. Se a espera esgotar, o passo falha e a execução toma a sua saída de erro. Está certo: o fluxo pediu para esperar até o trabalho estar feito, e não está, portanto ninguém sabe se o nó seguinte vai atuar sobre uma máquina que ainda se está a desligar.
O passo regista o que viu nesse instante. Medido numa ronda anterior contra um agente real, registou sent, e sessenta milissegundos depois o comando passou a tempo esgotado. O passo continuava a dizer sent, um dia depois, para sempre.
A leitura óbvia é que a linha está desatualizada e precisa de reparação. Essa leitura está errada, e perceber porquê ocupou a maior parte do raciocínio desta ronda.
A linha é uma explicação, não um campo de estado
O valor congelado não é uma cópia do estado do comando que tenha derivado. É a resposta a outra pergunta: o que sabia o fluxo quando decidiu tomar a saída de erro.
Escreva-se timeout por cima e a linha passa a dizer que o fluxo reagiu a um tempo esgotado. Não reagiu. Reagiu a uma espera que terminava enquanto o comando ainda estava em voo, um acontecimento diferente com outra causa e outra correção. Um mês depois, quem lesse essa execução tiraria a conclusão errada de uma linha corrigida com boas intenções.
O valor congelado fica portanto congelado. O que estava mesmo mal era o comentário por cima, que prometia deduzir desta linha se o comando acabara por ter êxito. Não se podia, e uma promessa num comentário tem uma validade que dura até ao dia em que alguém confia nela.
A pergunta a sério, se no fim correu bem, resolve-se lendo o estado atual do comando no momento em que alguém olha. A página da execução mostra ambos, lado a lado: o que o fluxo via então e o que é verdade agora.
| De onde vem | A que responde | |
|---|---|---|
| Então | A linha de auditoria, congelada no momento da decisão | Porque é que a execução tomou a saída que tomou |
| Agora | A tabela de comandos, lida ao abrir a página | Se o trabalho acabou por concluir |
| Linha ausente | Nenhuma das duas; mostrada como estado próprio | O comando já não consta dos registos |
Essa última linha conta. Se a linha do comando foi limpa, a vista não recai em repetir o valor congelado. «Já não consta dos registos» e «continua a dizer sent» são dois factos diferentes, e uma página que desenha o segundo quando o verdadeiro é o primeiro mente sem pestanejar.
Três razões por que um processo de fundo perde para uma junção
A alternativa era um processo de fundo que reescrevesse o estado final na linha de auditoria assim que o comando ficasse resolvido. Temos noutros sítios processos exatamente com essa forma, portanto foi uma escolha real e não um espantalho.
Um processo de fundo sobrescreve uma explicação. Como acima. A saída é escrever um segundo campo ao lado, e isso é honesto, mas é uma segunda verdade que seria lida em exatamente um sítio: aquele que agora faz a junção.
Um processo de fundo só repara o que acontece depois dele. A linha que iniciou toda esta investigação já existe. Um processo implantado hoje nunca lhe tocaria, nem a qualquer execução das duas rondas anteriores, porque os seus comandos ficaram resolvidos há muito e nenhum acontecimento futuro voltará a passar por ali. Uma junção mostra-as todas desde o dia em que entra ao serviço, incluindo a que procurávamos.
Um processo de fundo traz um aparelho inteiro. Atualizar linhas em segundo plano exige uma cadência, uma reclamação para que duas instâncias não colidam, uma espera contra a deriva, uma resposta sobre conservação e um caminho de falha próprio, e cada um deles pode estar errado às três da manhã. Procurar um punhado de identificadores por chave primária quando alguém abre uma página não exige nada disso.
A regra geral por baixo: mais vale ler a fonte do que copiá-la, a menos que a cópia responda a uma pergunta a que a fonte não consegue responder. Aqui a cópia responde a outra pergunta e querem-se as duas, que é justamente o caso em que se mantêm separadas.
O identificador veio de uma coluna JSON, portanto a consulta é delimitada
Um detalhe dessa junção não é otimização nem cortesia.
O identificador do comando vive dentro de um bloco JSON na linha de auditoria. As colunas JSON não têm chaves estrangeiras, portanto nada garante que o identificador ali dentro pertença a esta conta. Hoje pertence, porque foi o nosso próprio código que o escreveu. Isso não é uma fronteira, é um hábito.
A procura fica por isso delimitada à organização além do identificador. Sem essa fronteira, um valor editado à mão nesse bloco leria o estado do comando de outra pessoa. Fuga pequena, fácil de escapar, e do tipo que só encontra quem andava à procura dela.
Um fluxo por momento, com o momento dentro da chave
Um plano de alojamento podia ligar um fluxo de automação, numa única coluna. Essa coluna significava «executa isto quando uma máquina for aprovisionada», e isso não estava escrito em lado nenhum a não ser no comportamento da função que a lia.
Os clientes queriam também um fluxo na reinstalação e outro na eliminação. A correção óbvia são três colunas. Está errada por uma razão que merece ser nomeada: cada uma dessas três colunas poderia conter um fluxo cujo próprio despoletador diz outra coisa. Um fluxo que corre na eliminação, sentado na coluna que dispara no aprovisionamento. A invariante «o despoletador diz quando corre» passa a ser violável em três sítios em vez de nenhum, e o esquema não objeta em lado nenhum.
A ligação tornou-se portanto uma tabela própria, com o plano e o momento juntos como chave única. Um fluxo por momento e por plano, imposto por um índice em vez de por cuidado.
Dois detalhes dessa migração merecem ser guardados.
O momento é guardado como texto com uma restrição de verificação que enumera três valores, e não como a enumeração de despoletadores existente. Essa enumeração tem cinco valores, e dois deles são despoletadores que este mecanismo nunca escolhe: uma execução manual é iniciada por uma pessoa, e uma por webhook por um pedido de entrada. Uma coluna enumerada aceitaria de bom grado qualquer um dos dois, e existiria então uma linha de ligação com que nunca acontece nada. Uma definição que em silêncio não faz nada é pior do que uma definição em falta.
E a coluna antiga foi removida na mesma migração que copiou o seu conteúdo. Mantê-la como leitura derivada cómoda é exatamente a segunda verdade que esta tabela vem suprimir. O compilador encontrou cada leitor assim que a coluna saiu do esquema; eram sete. A cópia corre antes da remoção, numa só transação, portanto não existe instante em que a configuração de um cliente não esteja em lado nenhum.
A cópia toma além disso o despoletador próprio de cada fluxo em vez de presumir o sentido da coluna antiga. As linhas cujo fluxo trazia outro momento nunca teriam corrido com o código antigo; agora a ligação que uma pessoa fez faz mesmo o que ela julgava.
Correio por adesão, no grafo e não nas definições
Um fluxo pode escrever um aviso, e nesta ronda esse aviso também pode sair por correio. Revelou-se uma questão de valores por omissão e não de envio.
As categorias de correio da plataforma são uma lista de exclusão: o que não está expressamente desligado está ligado. Está certo para as categorias já existentes, todas elas coisas que um cliente precisa de saber. Não está certo para um aviso cujo texto alguém escreveu num editor de automação, porque um fluxo que corre a cada encomenda escreveria a toda a equipa de cada vez, até alguém encontrar a definição.
Em vez de inverter a lista, o valor por omissão vive no grafo: a caixa no nó está desligada, e ligá-la é um ato deliberado de quem escreveu o texto. A categoria continua a existir para que o correio possa ser desligado no sítio do costume, tal como as outras. Dois mecanismos, cada um para aquilo em que é bom.
Uma execução de ensaio nunca envia correio. Escreve o aviso do painel, que é interno e se fecha com um clique, e não manda nada para fora, pela mesma razão por que uma execução de ensaio nunca toca numa máquina: o correio chega às caixas de pessoas reais, e um botão de teste que surpreenda alguém não é um botão de teste.
Mais uma coisa pequena do mesmo trabalho. Dava-se por certo que o lado do envio era inalcançável a partir de onde os fluxos correm, e por isso isto estava adiado. Medido, era ao contrário: a função que queríamos contornar existe precisamente porque o outro processo não sabe compor correio, e pede ao processo em que os fluxos já correm que o componha. O desvio era mais longo do que o caminho direto, e o caminho direto tem menos um salto HTTP, menos uma assinatura e menos um caminho de falha.
O que anotaríamos
- Uma linha de auditoria explica uma decisão. Não se repara. A pergunta sobre o que é verdade agora tem outra resposta e merece um sítio próprio.
- Uma reparação em segundo plano só arranja o futuro. Se a linha que originou o trabalho não fosse tocada, isso é uma resposta de desenho e não um pormenor.
- Ler a fonte, a menos que a cópia responda ao que a fonte não consegue.
- Um identificador vindo de uma coluna JSON não tem chave estrangeira. Delimitar a consulta à conta; ter sido o nosso código a escrever o valor não é uma fronteira.
- Pôr a distinção na chave e não no nome da coluna. Três colunas são três sítios onde guardar a coisa errada; uma chave não é nenhum.
- Remover a coluna antiga na mesma migração. Uma leitura derivada deixada para trás é a segunda verdade que se queria eliminar.
- Uma lista de exclusão é o valor por omissão errado para texto escrito por um cliente. A adesão fica onde o texto se escreve.
Para que fique claro: tudo isto está construído e a funcionar. O envio em si foi demonstrado com um controlador que apenas regista e não contra um servidor de correio real, e a ligação ao plano ainda não foi exercitada por uma encomenda real.
Do trabalho de plataforma na Provibr. Como se comportam a espera e a saída de erro para começar está em um fio que se arrasta, e exatamente um gatilho, e porque um tempo esgotado não é um botão de paragem está em um prazo não é um botão de paragem.