Lidando com falhas de conexão em microsserviços

Um erro imperdoável, na implementação de microsserviços é considerar que a conexão é estável e confiável. Por razões variadas, a rede pode falhar tanto ao enviar uma requisição quanto ao trazer uma resposta.

No lado cliente (microsserviço que está fazendo uma requisição) é fundamental implementar:

  • estratégias de proteção ao servidor, como a implementação do padrão Circuit Breaker
  • estretégias de repetição de requisição, como a implementação do padrão Retry

No lado servidor (microsserviço que está atendendo uma requisição) é indispensável garantir que todas as requisições sejam idempotentes. Em cenários onde isso não for absolutamente possível, é fundamental garantir que o “estado” do servidor não seja comprometido.

Se tiver interesse em entender mais sobre microsserviços, recomendo que acesse o Guia de Conteúdo para Microsserviços deste site.

O problema com POST

O verbo POST é, por definição, não idempotente.

Considere o seguinte cenário:

MOMENTO 1 – Uma aplicação cliente chama um microsserviço para realizar a inclusão de uma informação. Esta chamada está disponível através de uma operação POST.

MOMENTO 2 – A aplicação servidor recebe a requisição e executa o processamento de forma adequada. Entretanto, a mensagem de resposta acaba se perdendo na rede e, por alguma razão, não chega a aplicação cliente.

MOMENTO 3 – Depois de algum tempo a aplicação cliente identifica um time-out. E, por definição, faz uma nova tentativa de inclusão de informação.

MOMENTO 4 – A aplicação servidor recebe a nova requisição e, inadvertidamente, inclui a mesma informação, uma segunda vez, na base.

Assustador, não acha? Esse é um caso extremamente difícil de detectar e raramente é “pego” em testes de QA.

Uma possível solução

De forma radical, o cenário acima me faz evitar utilizar o verbo post em minhas aplicações. Acabo utilizando um “PUT identificado” (essa solução, aliás vem sendo bastante aplicada estando presente, inclusive em uma aplicação de referência da Microsoft).

[HttpPut]
[ProducesResponseType(typeof(string), (int) HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task GetEcho(
    [FromBody] EchoCommand command,
    [FromHeader(Name = "x-requestid")] string requestId
)
{
    if (!Guid.TryParse(requestId, out var guid))
    {
        return BadRequest();
    }

    var identifiedCommand = new IdentifiedCommand<EchoCommand, string>(
        command,
        guid
    );

    return Ok(await _mediator.Send(identifiedCommand));
}

O caminho que venho adotando é pedir, em operações que mudam o estado do servidor, um identificador da requisição. Este identificador é então ligado ao comando.

A aplicação cliente precisará adicionar um header para identificar a requisição.

 

Um handler global para comandos identificados checa se o identificador está associado a um comando já executado. Caso esteja, gera um erro.

public class IdentifiedCommandHandler<TCommand, TResult>
    : IRequestHandler<IdentifiedCommand<TCommand, TResult>, TResult>
    where TCommand : IRequest
{
    private readonly IMediator _mediator;
    private readonly IRequestManager _requestManager;

    public IdentifiedCommandHandler(IMediator mediator, IRequestManager requestManager)
    {
        _mediator = mediator;
        _requestManager = requestManager;
    }

    public async Task Handle(
        IdentifiedCommand<TCommand, TResult> message,
        CancellationToken cancellationToken = default(CancellationToken)
        )
    {
        if (message.Id == Guid.Empty)
        {
            ThrowMediatrPipelineException.IdentifiedCommandWithoutId();
        }

        if (message.Command == null)
        {
            ThrowMediatrPipelineException.IdentifiedCommandWithoutInnerCommand();
        }

        var alreadyRegistered = await _requestManager.IsRegistered(message.Id, cancellationToken);
        if (alreadyRegistered)
        {
            ThrowMediatrPipelineException.CommandWasAlreadyExecuted();
        }

        await _requestManager.Register(message.Id, cancellationToken);
        var result = await _mediator.Send(message.Command, cancellationToken);
        return result;
    }
}

Todas as exceptions relacionadas a uma falha por duplicidade são mapeadas adequadamente para um Bad Request.

public class HttpGlobalExceptionFilter : IExceptionFilter
{
    private readonly IHostingEnvironment _env;
    private readonly ILogger _logger;

    public HttpGlobalExceptionFilter(
        IHostingEnvironment env,
        ILogger logger
        )
    {
        _env = env;
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        _logger.LogError(new EventId(context.Exception.HResult),
            context.Exception,
            context.Exception.Message);

        if (context.Exception.GetType() == typeof(MediatrPipelineException))
        {
            var validationException = context.Exception.InnerException as ValidationException;
            if (validationException != null)
            {
                var json = new JsonErrorResponse
                {
                    Messages = validationException.Errors
                        .Select(e => e.ErrorMessage)
                        .ToArray()
                };

                context.Result = new BadRequestObjectResult(json);
            }
            context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        else
        {
            var json = new JsonErrorResponse
            {
                Messages = new[]
                {
                    "Internal Error. Try again later.",
                    context.Exception.GetType().ToString(),
                    context.Exception.Message
                }
            };

            context.Result = new ObjectResult(json) { StatusCode = 500 };
            context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        }
        context.ExceptionHandled = true;
    }

    public class JsonErrorResponse
    {
        public string[] Messages { get; set; }
    }
}

Que geram um Bad Request para o cliente.

Concluindo

A solução perfeita entregaria idempotência. Mas, para isso, eu precisaria armazenar a resposta do servidor e isso poderia conduzir a erros de interpretação. Aqui, opto por deixar claro o que houve de errado.

Mais uma vez, é importante ressaltar que, implementando microsserviços, é fundamental estar preparado para instabilidades de conexão. A questão não é se ocorrerá uma falha, mas quando essa falha irá ocorrer.

O código fonte desse exemplo está disponível no github.

Créditos da imagem da capa para Park Troopers

Compartilhe este insight:

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mais insights para o seu negócio

Veja mais alguns estudos e reflexões que podem gerar alguns insights para o seu negócio:

Nesses últimos tempos, com a pandemia, inaugurei novos hábitos e aposentei outros. Estou trabalhando muito mais, mas também, agora que...
O desenvolvimento de uma aplicação com ótima performance só é possível mediante considerações desde sua arquitetura. Otimizações pontuais de código,...
I wrote this post in 2016. Unfortunately, I lost it when I “rebooted” my blog. Anyway, I have a good...
Situações como a que estamos vivendo nos “empurram” para algumas reflexões. De certa forma, paramos de reagir e começamos a...
I believe that, from time to time, it is interesting to learn a new language or framework that takes us...
C++ é uma linguagem de programação velha, charmosa para os iniciados, assustadora para aqueles que conhecem pouco dela. Bjarne Stroustrup desenvolveu...
Masterclass

O Poder do Metamodelo para Profissionais Técnicos Avançarem

Nesta masterclass aberta ao público, vamos explorar como o Metamodelo para a Criação, desenvolvido por Elemar Júnior, pode ser uma ferramenta poderosa para alavancar sua carreira técnica em TI.

Crie sua conta

Preencha os dados para iniciar o seu cadastro no plano anual do Clube de Estudos:

Crie sua conta

Preencha os dados para iniciar o seu cadastro no plano mensal do Clube de Estudos:

× Precisa de ajuda?