Padrão Saga #2: Transações ACD

Bianca Cristina
7 min readJul 27, 2022

If you’d like, you can read this article in English here

No primeiro artigo da série, discutimos sobre a definição do padrão Saga e como podemos utilizá-lo para prover níveis de consistência de dados entre microsserviços. Entretanto, existe uma diferença importante entre transações “comuns” e transações do padrão Saga: por si só, o padrão Saga não é suficiente para garantir a propriedade isolamento das transações.

Aos esquecidos, a propriedade isolamento provê a ilusão de que cada operação possui acesso exclusivo aos dados mesmo em casos de operações concorrentes.

Se o padrão Saga não garante isolamento, então múltiplas transações locais podem modificar as mesmas informações simultaneamente? 😨

Infelizmente, sim! 😓 E justamente pela falta de isolamento, as execuções das transações locais podem ocasionar inconsistências de dados categorizadas como anomalias. Nesse segundo artigo da série, vamos explorar essas anomalias e suas possíveis soluções.

Não desiste ainda, vem comigo!

Anomalias

Para as próximas explicações, considere a saga “Solicitar Viagem” apresentada no primeiro artigo:

Saga “Solicitar Viagem”

Atualização Perdida

Suponha que o passageiro solicite uma viagem e o motorista aceita, porém ele decide cancelar a viagem antes do motorista chegar.

Saga “Cancelar Viagem”

Nesse caso, a aplicação aciona a saga responsável por cancelar a corrida e voilá corrida cancelada com sucesso, certo? Bom, depende… 😬

No mundo dos eventos, precisamos abraçar o fato de que garantir a ordem dos eventos é desafiador e nem sempre possível. Considerando isso, o seguinte cenário pode acontecer:

  1. A saga “Solicitar Viagem” é iniciada e as transações T1, T2 e T3 são concluídas
  2. Entre as transações T3 e T4, o passageiro decide cancelar a viagem, a saga “Cancelar Viagem” é acionada e a viagem é cancelada antes da conclusão da transação que efetua o pagamento (T4)
  3. Por conta de algum atraso na entrega do evento, a transação T4 é efetuada com sucesso e o passageiro é cobrado pela viagem

Nessa situação, a transação T4 da saga “Solicitar Viagem” ignorou a atualização feita pela saga “Cancelar Viagem” e o passageiro foi cobrado indevidamente. Esse cenário descreve a anomalia conhecida como atualização perdida.

Leitura Suja

Sorte a sua (ou não), o passageiro é um exímio caçador de bugs e encontrou mais uma situação possível de erro: imagine o cenário em que a viagem só pode ser cancelada em, no máximo, cinco minutos após o motorista aceitá-la.

Saga “Cancelar Viagem com Tempo Limite”

Agora, suponha que o usuário solicita viagens usando apenas sua carteira virtual no aplicativo e, no momento, possui saldo suficiente para uma única viagem e a solicita. No entanto, algo acontece e o usuário decide cancelar a viagem após os cinco minutos. Dessa forma, o seguinte cenário é possível:

  1. A saga “Cancelar Viagem com Tempo Limite” é acionada e o reembolso é realizado, aumentando, assim, o crédito disponível na carteira do passageiro
  2. O passageiro muda de ideia novamente e solicita outra viagem utilizando o valor reembolsado anteriormente. Dessa forma, a saga “Solicitar Viagem” é acionada para uma nova viagem, reduzindo, assim, o crédito disponível na carteira do passageiro
  3. A saga da etapa 1 é cancelada porque a transação T3 constatou que o tempo máximo para cancelamento foi excedido. Logo, transações de compensação serão acionadas, reduzindo, assim, o crédito disponível na carteira do passageiro

Nesse caso, a saga “Solicitar Viagem” enxergou um dado temporário da saga “Cancelar Viagem com Tempo Limite” e tomou decisões baseadas nessa informação. Entretanto, a saga “Cancelar Viagem com Tempo Limite” acionou transações de compensação, possibilitando, assim, que o passageiro solicitasse uma viagem mesmo sem saldo suficiente. Esse cenário descreve a anomalia conhecida como leitura suja.

Leitura Difusa

Nem a escolha do trajeto conseguiu escapar dos bugs causados pelo seu fiel passageiro. Suponha que o aplicativo suporte a alteração do trajeto mesmo após a conclusão da transação T1 na saga “Solicitar Viagem”. Essa possibilidade é representada pela saga “Atualizar Trajeto”.

Saga “Atualizar Trajeto”

Além disso, considere que a transação “Procurar Motorista” (T2) da saga “Solicitar Viagem” realiza duas buscas por informações da viagem: uma no início da transação e outra no final. Nesse contexto, o seguinte cenário é possível:

  1. A saga “Solicitar Viagem” é acionada e encontra-se na transação T2 realizando a primeira busca por informações da viagem
  2. Antes da conclusão de T2 e depois da primeira busca da viagem nessa transação, o passageiro decide alterar o trajeto. Dessa forma, a saga “Atualizar Trajeto” é acionada e finalizada antes do início da segunda busca na transação T2 da saga “Solicitar Viagem”
  3. Após a conclusão da saga “Atualizar Trajeto”, a transação T2 realiza a segunda busca por informações da viagem e obtém um dado diferente da primeira

Nesse caso, se as informações obtidas nos diferentes resultados das buscas em T2 forem utilizadas, a aplicação pode assumir um estado inconsistente. Esse cenário descreve a anomalia conhecida como leitura difusa.

Possíveis Soluções

Finalmente a parte boa! Temos soluções para todas as anomalias, certo? 🥳

Então… Resumidamente, é praticamente impossível eliminar todas as anomalias, o que podemos fazer é minimizar os danos causados com técnicas específicas.

Bloqueio Semântico

O bloqueio semântico consiste em adicionar uma flag no dado indicando que ele ainda não foi commitado e outras transações que necessitam desse dado devem tratá-lo como não confiável, minimizando, assim, os impactos causados pelas três anomalias: atualização perdida, leitura suja e leitura difusa.

Na saga “Solicitar Viagem”, por exemplo, poderíamos adicionar uma informação à viagem indicando que seu estado atual é “Aguardando Motorista” quando estivermos na transação T2, logo, se não for possível encontrar um motorista adequado, essa viagem pode ser cancelada.

Mas o que podemos fazer quando o dado estiver nesse estado não confiável? 🤔

Essa é a pergunta de um milhão de dólares e só você pode responder, uma vez que cada aplicação possui seus próprios limites. Por exemplo, uma aplicação que suporta reprocessamento de eventos poderia optar por marcar o evento como não processado ao ler um dado não confiável no meio de uma transação e forçar o reprocessamento desse evento em um momento futuro. Já para uma aplicação que não reprocessa eventos, mas suporta latências um pouco mais altas, uma opção viável é bloquear a transação até que o dado necessário seja commitado. A melhor maneira de lidar com um dado não commitado é aquela que melhor se adequa ao seu negócio.

Atualizações Comutativas

Já as atualizações comutativas consistem em operações que podem ser executadas em qualquer ordem, dado que não impactam uma a outra. Considere a transação “Efetuar Pagamento” (T4) e “Procurar Motorista” (T2) da saga “Solicitar Viagem”, se assumirmos que a saga “Cancelar Viagem” desfaz todas as etapas da “Solicitar Viagem” em caso de problema, então poderíamos inverter a ordem das transações T2 e T4 sem efeitos negativos para a saga “Solicitar Viagem”, logo, essas transações são comutativas. Entender quais transações são comutativas é útil para inverter a ordem de transações que podem causar a anomalia atualização perdida.

Visão Pessimista

A visão pessimista é uma forma de organizar as etapas de uma saga de modo a minimizar os riscos relacionados a anomalia leitura suja. Considere a situação apresentada na explicação da anomalia leitura suja onde foi possível solicitar uma viagem excedendo o limite disponível na carteira do passageiro. Nesse caso, o erro ocorreu devido ao fato de que o reembolso é realizado antes de verificar se a viagem ainda pode ser cancelada. Dessa forma, se reorganizassemos as etapas da saga “Cancelar Viagem com Tempo Limite” posicionando a transação que efetua o reembolso na última etapa, poderíamos minimizar os riscos de uma leitura suja do limite disponível na carteira.

Saga “Cancelar Viagem com Tempo Reorganizada”

Releitura do Valor

Essa técnica é uma forma simples de minimizar os danos causados pelas anomalias atualização perdida e leitura difusa por meio da releitura do valor antes de realizar alguma ação envolvendo esse dado. Dessa forma, se a releitura detectar que o dado foi alterado, então a etapa atual pode ser abortada e possivelmente reiniciada em um momento futuro.

Histórico de Operações

O histórico de operações consiste em gravar todas as ações que já foram executadas em um dado específico de modo tomar decisões baseadas nas operações que já foram efetuadas, reduzindo, dessa forma, os impactos da anomalia atualização perdida.

Considere as sagas “Solicitar Viagem” e “Cancelar Viagem” que realizam, respectivamente, operações para aceitar a viagem e mudar o status da viagem para cancelada. Se ambas as sagas executarem simultaneamente, é possível que a transação para mudar o status da viagem para cancelada na saga “Cancelar Viagem” seja concluída antes da transação para aceitar a viagem. Para evitar que isso ocorra, poderíamos gravar o histórico de operações da viagem e, como a viagem será cancelada antes de ser aceita, então será possível ignorar a transação que aceita a viagem.

Se você chegou até aqui, então provavelmente entendeu que o padrão Saga não é bala de prata: ainda que promova níveis de consistência de dados, existem desvantagens associadas e é responsabilidade do desenvolvedor implementar as técnicas mais adequadas para minimizá-las.

Por hoje é isso, espero que esses dois primeiros artigos tenham sido úteis e para o próximo (e finalmente o último da série! 🎉) pretendo mostrar como implementar o padrão Saga utilizando AWS Lambda. Até mais! 🙂

--

--