Revisitando Sagas

Sidharta Rezende
7 min readDec 27, 2020

Ano passado, nessa época, eu estava escrevendo meu primeiro artigo no Medium, sobre Sagas:

O retorno que tive de colegas querendo entender mais do assunto e aplicar este padrão em suas soluções foi intenso ao longo do ano. Não só pessoas que eu já conhecia e admirava o trabalho, mas também novas conexões que tive o prazer de fazer e ter gratificantes conversas sobre os desafios que enfrentamos hoje com a abordagem de micro-serviços.

Longe de esgotar o assunto, aquele artigo foi apenas introdutório, onde abordei problema que nos leva a recorrer a Sagas, e as abordagens mais comuns no mercado. Pouco falei de soluções, ou de como inovar e melhorar essas abordagens.

Embora no momento em que escrevi aquele artigo eu já houvesse, juntamente com colegas da empresa em que trabalho, feito nossa solução de Saga e a colocado em uso, foi nesse intervalo de 1 ano que conseguimos colocar a prova nosso conceito em diversos projetos, e entender como fomos além do documentado na literatura e alcançado algo que funcionou muito bem para nós. É com essa intenção de discutir estes aspectos novos que resolvi revisitar o tema.

Indo além da orquestração e da coreografia

No artigo original cheguei a arranhar a superfície do assunto e sugerir uma abordagem híbrida entre orquestração e coreografia, conforme cito a seguir:

“Uma outra abordagem que eu prefiro é fazer o nó informar, na própria mensagem, qual o próximo nó a ser executado.
Essa abordagem híbrida entre orquestração e coreografia trás, na minha opinião, o melhor dos dois mundos. O nó ainda detém conhecimento de qual é o próximo nó a ser executado, reduzindo o peso do orquestrador como ponto de falha, e simplifica a infra-estrutura, não requerendo filas adicionais, como no modelo coreografado clássico, nem um recurso de persistência adicional para controlar o estado.”

Para mim, um ponto fraco nos modelos orquestrado e coreografado tradicionais é a falta de flexibilidade e dependência de estruturas adicionais, como bancos de dados. Vou discutir esses pontos a seguir:

Saga deveria ser independente de infra-estrutura adicional

O padrão Saga costuma ser construído em cima de alguma plataforma de mensageria ou streaming, garantindo comunicação assíncrona.

Um message broker e suas filas deveriam ser, na minha opinião, toda a infra-estrutura necessária para rodar o Saga. Precisar adicionar obrigatoriamente outros elementos de infra-estrutura trás sobrepeso, aumentando a complexidade da arquitetura e sobrepondo potenciais pontos de falha.

Um dos pontos fracos da arquitetura de micro-serviços é justamente a complexidade arquitetural que acaba sendo criada para cada serviço, o que acaba acarretando mais trabalho na administração desses recursos.

Como em um motor, onde cada peça móvel aumenta a complexidade e se torna mais um ponto de possível falha, acrescentar elementos de infra-estrutura, como alguma forma de extra de persistência, deve ser feito com muito cuidado e pesando muito bem os prós e contras.

Algumas soluções de Saga bem documentadas fazem uso de banco de dados para manter o estado atual da execução do saga. Acredito que existem formas melhores de controlar isso, como manter o estado atual serializado na própria mensagem, uma técnica que falarei logo adiante.

Dito isso, algumas vezes não conseguiremos escapar de depender do apoio de recursos de infra-estrutura. Por exemplo, algumas vezes podemos estar tratando do consumo de recursos de terceiros que não garantem idempotência, e pode ser necessário usar alguma forma de controle de estado no contexto da nossa aplicação, como um cache.

Isso, entretanto, é uma característica de um problema específico a ser resolvido. O que é negativo é a obrigatoriedade de contar com recursos adicionais para poder executar uma Saga.

O exemplo acima, do uso de cache para controlar a idempotência de um serviço externo, pode deixar de ser necessário em uma eventual melhoria no serviço alvo. Devemos sempre buscar a solução mais simples em termos de arquitetura em micro-serviços.

Saga deveria ser flexível

Situação hipotética:

Trabalhamos em uma loja on-line, e estamos criando uma nova opção para nossos clientes: Permitir que dividam o pagamento em mais de um cartão.

Mas aí vem a pegadinha: Além do valor da compra, o valor do frete também deve ser divido igualmente. Um serviço é responsável por processar o pagamento dos produtos. Um segundo serviço processa o pagamento do frete.

Esquema básico do nosso exemplo

A Saga da transação precisa:

1 - Reservar o item no estoque, através do Inventory Control Service
2- Processar o pagamento no Product Payment Service
3- Processar o pagamento do frete no shipping payment service, o qual consome uma API externa da companhia responsável pela entrega.

Em caso de falha em qualquer um desses nós ações compensatórias devem ser executadas. Por exemplo, supondo que a API da empresa de entrega retorne um status negativo em seu processamento, todos os pagamento feitos do produto, assim como a reserva no estoque, devem ser desfeitos.

Para adicionar uma complexidade em cima, suponha que todos os pagamentos de um mesmo cartão de crédito tenham que ser processados antes de passar para o próximo cartão.
Por exemplo, no caso de dois clientes dividindo a mesma compra, o primeiro cliente precisa ter a cobrança do produto e do frete realizadas antes do segundo cliente ter a cobrança do produto. Ou seja, enviar uma lista de cartões para o serviço não é uma opção.

Sei que o processamento real de uma loja online é muito mais complexo do que esse exemplo, mas acredito que servirá para ilustrar a situação que estamos querendo expor.

Exemplo: 2 clientes, Roberto e Fernanda, quiserem dividir o pagamento de um celular xyz que custa R$1.000,00 e o frete R$50,00, teríamos:

1- Dar baixa do produto “celuxar xyz” no Inventory Control Service

2- Processar o pagamento no valor de R$500,00 no cartão do Roberto, através do Product Payment Service

3- Processar o pagamento de metade do frete (R$25,00)no cartão do Roberto, através do Shipping Payment service

4- Processar o pagamento no valor de R$500,00 no cartão da Fernanda, no Product Payment Service

5- Processar o pagamento da segunda metade do frete (R$25,00) no cartão da Fernanda

Essa Saga é única. Caso precisássemos dividir entre qualquer outro número de pessoas, seria uma saga diferente.

Isso, obviamente, trás reflexo também no fluxo de compensação. A sequencia de serviços a serem invocados no caso da falha em um pagamento dividido por 5 pessoas deve ser diferente de um pagamento feito por um único cliente.

A maioria das soluções de Saga orquestrados e coreografados não consegue dar conta dessa flexibilidade. Um orquestrador clássico precisa conhecer, de antemão, os nós a serem executados e sua ordem. Já um modelo coreografado precisa conhecer o próximo nó a ser executado… Mas como fazer como no exemplo acima, quando o próximo nó a ser executado depende de condições previamente desconhecidas?

Para resolver esse desafio, utilizamos um modelo misto entre coregrafia e orquestração.

O orquestrador passa a ser apenas um motor de workflow que só conhece o caminho a ser executado no momento em que recebe a primeira mensagem de um Saga. Nessa mensagem devemos especificar quais os serviços a serem chamados, sua ordem e passos compensatórios.

Conforme a execução do Saga avança, o orquestrador atualiza na própria mensagem o próximo passo a ser executado, em uma máquina de estado serializada na própria mensagem. Quem guarda o estado não é o orquestrador, mas a própria mensagem.

Nessa abordagem, a mensagem trás todas as informações que algumas vezes são colocadas em bancos de dados.

Um exemplo simplificado, para ilustrar um possível formato para essa mensagem, seria o seguinte:

{
"node":"InventoryControlService",
"action":"holdProduct",
"nextNodeOnSuccess":{
"node":"ProductPaymentService",
"action":"processPayment",
"nextNodeOnSuccess":{
"node":"ShippingPaymentService",
"action":"processPayment",
"nextNodeOnSuccess":{
"node":"EndSaga"
},
"nextNodeOnFailure":{
"node":"ProductPaymentService",
"action":"revertPayment",
"nextNodeOnSuccess":{
"node":"InventoryControlService",
"action":"releaseProduct"
}
}
},
"nextNodeOnFailure":{
"node":"InventoryControlService",
"action":"releaseProduct"
}
}
}

Na execução do segundo nó:

{
"node": "ProductPaymentService",
"action": "processPayment",
"nextNodeOnSuccess": {
"node": "ShippingPaymentService",
"action": "processPayment",
"nextNodeOnSuccess": {
"node": "EndSaga"
},
"nextNodeOnFailure":{
"node":"ProductPaymentService",
"action": "revertPayment",
"nextNodeOnSuccess":{
"node":InventoryControlService,
"action": "releaseProduct"
}
}
},
"nextNodeOnFailure":{
"node":"InventoryControlService",
"action": "releaseProduct"
}
}

E, finalmente, na execução do nó final:

{
"node":"ShippingPaymentService",
"action":"processPayment",
"nextNodeOnSuccess":{
"node":"EndSaga"
},
"nextNodeOnFailure":{
"node":"ProductPaymentService",
"action":"revertPayment",
"nextNodeOnSuccess":{
"node":"InventoryControlService",
"action":"releaseProduct"
}
}
}

Esse exemplo de formato para a mensagem deixa de fora alguns aspectos importantes, como transitar os dados necessários para a execução dos participantes, mas acredito que passa o geral da idéia. O orquestrador “entende” o formato da mensagem e executa o nó atual. Em caso de sucesso ou falha, ele atualiza a mensagem com o novo nó a ser executado e volta a enviá-la para a fila, em um ciclo que se repete até o final da execução da saga.

Algumas otimizações interessantes podem ser realizadas, como aplicar compactação na mensagem a ser enviada, criptografia para proteger dados sensíveis, controle de número de re-tentativas com base no número de tentativas de entrega da mensagem e controle manual do acknowledgement da mensagem. Em outra ocasião espero conseguir abordar alguns desses assuntos em novos textos.

Espero ter conseguido passar um pouco da abordagem que fizemos para o padrão Saga, e algumas das lições aprendidas. Como sempre, me coloco a disposição para conversar mais sobre esse e outros temas.

Fiquem a vontade para me contactar pelo linkedin ou pelo email sidharta.rezende@gmail.com

--

--

Sidharta Rezende

Skatista de downhill, amante da vida e Engenheiro de Software