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:

The type EmbeddableDocumentStore is gone. But it does not mean that you have no good options to replace his functionality....
If you are interested in performance, you need to know more about CUDA. From the official website: CUDA® is a...
Recentemente, compartilhei uma excelente palestra, do Feredico Lois, colega no desenvolvimento do RavenDB, sobre padrões para alta performance com C#....
Já está disponível o registro da conversa com os meninos da Lambda3, meus amigos Giovanni Bassi e Victor Cavalcante, sobre...
Our goal is to fill a two-dimensional array with 1’s. using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; namespace ToArrays { public class Program...
Compete ao arquiteto corporativo a responsabilidade de descrever, analisar e comunicar os diversos aspectos da arquitetura corporativa, bem como registrar...

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:

Mentoria em
Arquitetura de Software
com ênfase em IA

Aprenda a coordenar equipes de desenvolvimento, aplicar padrões e técnicas de arquitetura e aumentar sua produtividade criando sistemas inovadores com o suporte da IA.

× Precisa de ajuda?