Olá pessoal,
No post anterior vimos as possíveis situações em que uma chamada a um web service pode falhar. Vimos também que uma possível solução seria utilizar estruturas de enfileiramento de mensagens (MSMQ, WebSphere MQ, por exemplo), e que porém, tais soluções não são indicadas para cenários em que a necessidade de entrega ou recebimento de mensagens ultrapasse o escopo da rede interna da organização.
Outro ponto a se considerar, é que tais abordagens caracterizam o que é conhecido com integração near real-time, onde o dado consumido ou provido chega ao seu destino não em tempo real, mais próximo disso. Existem situações em que requisitos estabelecerão que o dado deverá ser obtido ou entregue ao seu destino em tempo real, o que gera uma demanda por abordagens denominadas real-time. Web service é uma abordagem que permite a implementação de integrações real-time, porém, como visto anteriormente, como endereçar as possíveis falhas que podem ocorrer quando da sua utilização?
No livro Service Design Patterns, de Ian Robinson, o padrão Idempotent Retry promete endereçar tais falhas, sendo descrito da seguinte forma:
“Projete o cliente de forma que problemas comuns de conectividade sejam tratados. Quando um erro de conexão ocorrer, reconecte ao serviço e reenvie à requisição. Limite o número de vezes que tais tentativas sejam feitas. Inclua um identificador único em cada requisição de forma que o serviço possa identificar requisições duplicadas…“ [Minha tradução]
Nada é tão simples quanto parece. Ao implementar este padrão diversos aspectos devem ser considerados. Uma vez que o consumidor do serviço tenha positivamente identificado um erro temporário e recuperável de conexão, como os que vimos no artigo anterior, ele deve iniciar sua lógica de reconexão. Consumidores de serviço devem determinar quanto tempo aguardar antes de fazer uma nova tentativa. Tal tempo pode ser nenhum, ou seja, o serviço é chamado novamente logo após o erro, ou pode haver uma pausa, de forma a endereçar problemas temporários que podem ser recuperados automaticamente em poucos segundos. O tempo de espera pode ser o mesmo para todo tipo de erro ou pode ser diferenciado por tipo de erro. Também é importante que uma quantidade máxima de tentativas seja estabelecida, considerando situações de falhas crônicas, ou seja, falhas que não se resolverão sozinhas. Caso contrário, tentativas de entrega de um número de mensagens de forma indefinida poderá sobrecarregar tanto consumidores quanto o provedores do serviços.
O consumidor do serviço também deverá definir como implementar a espera. Uma possibilidade poderia ser chamar novamente o serviço em threads apartadas da principal, de forma a não causar bloqueios. Tal thread seria colocada em modo “sleep” por um certo tempo, após tentativas malsucedidas. Porém, se o intervalo entre as requisições for longo (algo acima de poucos minutos, ou horas), uma melhor abordagem seria persistir a informação necessária para chamada do serviço em uma fila ou um banco de dados, de forma que tal recurso seja lido com certa frequência por um processo que rode em background. Neste ponto é importante considerar que o cliente também poderá falhar entre uma tentativa de envio de outra, de forma que muito provavelmente haverá perdas caso as mensagens pendentes de envio não estejam persistidas para uma possível retomada de processamento.
Complicou? Note que até agora descrevemos apenas o que seria a parte “Retry” do “Idempotent Retry Pattern”. O que seria então a parte “Idempotent”? Segundo Ian, uma requisição idempotente (idempotent) é aquela que produz o mesmo resultado não importa quantas vezes o serviço for chamado. Um exemplo simples é um serviço que exclui informações. Este tipo de serviço é considerado como idempotente, pois, uma vez que se tenha excluído a informação destino, a informação sempre já estará excluída em chamadas subsequentes, de forma que o efeito esperado pela chamada do serviço será sempre o mesmo – a exclusão bem-sucedida. O problema de não implementar serviços de forma que eles sejam idempotentes, é que efeitos colaterais poderão acontecer se um consumidor assumir erroneamente que uma requisição pode ser reiniciada após a perda de uma conexão. O serviço pode ter processado a requisição anteriormente, mesmo tendo perdido a conexão com o seu consumidor.
Ainda segundo Ian, o serviço deve identificar requisições duplicadas e implementar lógicas apropriadas para garantir sua qualidade. Consumidores de serviços devem gerar ou adquirir identificadores únicos para mensagens, que devem ser utilizados pelo serviço para identificar se a mesma mensagem foi processada anteriormente ou não. Se o identificador é novo, a mensagem deve ser processada, caso contrário, um erro pode ser retornado.
Você confere no diagrama de sequencia ao lado uma representação alto nível do padrão Idempotent Retry Pattern, conforme descrito.
Um tópico a parte, porém, é a melhor forma de implementar este padrão, considerando que as lógicas necessárias não sejam repetidas em todas as requisições de serviços de uma ou mais aplicações consumidoras. Frameworks como o WCF (Windows Communication Foundation) da Microsoft, a WS extension WS-ReliableMessaging, bem como outros padrões de implementação para consumidores de serviços descritos no livro, endereçam bem esta questão. Porém, como dito, este é todo um tópico a parte! Neste ficamos por aqui.
Até a próxima!