Compartilhar via


Criar aplicativos de negócios controlados por mensagens com o NServiceBus e o Barramento de Serviço do Azure

O NServiceBus é uma estrutura comercial de mensagens fornecida pela Particular Software. Ele se baseia no Barramento de Serviço do Azure e ajuda os desenvolvedores a se concentrarem na lógica de negócios eliminando as preocupações com a infraestrutura. Neste guia, criaremos uma solução que troca mensagens entre dois serviços. Também mostraremos como tentar novamente o envio de mensagens com falha de modo automático e examinar as opções para hospedar esses serviços no Azure.

Observação

O código deste tutorial está disponível no site do Docs da Particular Software.

Pré-requisitos

O exemplo presume que você já criou um namespace do Barramento de Serviço do Azure.

Importante

O NServiceBus exige pelo menos a camada standard. O nível Básica não funcionará.

Baixar e preparar a solução

  1. Baixe o código do site do Docs da Particular Software. A solução SendReceiveWithNservicebus.sln consiste em três projetos:

    • Remetente: um aplicativo de console que envia mensagens
    • Receptor: um aplicativo de console que recebe mensagens do remetente e as responde
    • Compartilhado: uma biblioteca de classes que contém os contratos de mensagem compartilhados entre o remetente e o receptor

    O seguinte diagrama, gerado pelo ServiceInsight, uma ferramenta de visualização e depuração da Particular Software, mostra o fluxo de mensagem:

    Imagem mostrando o diagrama de sequência

  2. Abra o SendReceiveWithNservicebus.sln no seu editor de código favorito (por exemplo, o Visual Studio 2022).

  3. Abra o appsettings.json nos projetos Receptor e Remetente e defina AzureServiceBusConnectionString como a cadeia de conexão do namespace do Barramento de Serviço do Azure.

    • Isso pode ser encontrado no portal do Azure em Namespace do Barramento de Serviço>Configurações>Políticas de acesso compartilhado>RootManageSharedAccessKey>Cadeia de Conexão Primária.
    • O AzureServiceBusTransport também tem um construtor que aceita um namespace e uma credencial de token, que em um ambiente de produção será mais seguro, no entanto, para fins deste tutorial, a cadeia de conexão de chave de acesso compartilhado será usada.

Definir os contratos de mensagem compartilhada

A biblioteca de classes compartilhada é onde você define os contratos usados para enviar as mensagens. Ela inclui uma referência ao pacote NuGet NServiceBus, que contém interfaces que você pode usar para identificar as mensagens. As interfaces não são necessárias, mas elas oferecem uma validação extra do NServiceBus e permitem que o código seja autodocumentado.

Primeiro, vamos examinar a classe Ping.cs

public class Ping : NServiceBus.ICommand
{
    public int Round { get; set; }
}

A classe Ping define uma mensagem que o Remetente envia ao Receptor. É uma classe C# simples que implementa NServiceBus.ICommand, uma interface do pacote NServiceBus. Essa mensagem é um sinal para o leitor e para o NServiceBus de que se trata de um comando, embora haja outras maneiras de identificar mensagens sem usar interfaces.

A outra classe de mensagem nos projetos compartilhados é Pong.cs:

public class Pong : NServiceBus.IMessage
{
    public string Acknowledgement { get; set; }
}

Pong também é um objeto C# simples, embora implemente NServiceBus.IMessage. A interface IMessage representa uma mensagem genérica que não é um comando nem um evento e geralmente é usada para respostas. No nosso exemplo, ela é uma resposta que o Receptor retorna ao Remetente para indicar que uma mensagem foi recebida.

O Ping e o Pong são os dois tipos de mensagem que você usará. A próxima etapa é configurar o Remetente para usar o Barramento de Serviço do Azure e enviar uma mensagem Ping.

Configurar o remetente

O Remetente é um ponto de extremidade que envia a mensagem Ping. Aqui, você configura o Remetente para usar o Barramento de Serviço do Azure como o mecanismo de transporte e depois cria uma instância de Ping e a envia.

No método Main de Program.cs, você configura o ponto de extremidade do Remetente:

var host = Host.CreateDefaultBuilder(args)
    // Configure a host for the endpoint
    .ConfigureLogging((context, logging) =>
    {
        logging.AddConfiguration(context.Configuration.GetSection("Logging"));

        logging.AddConsole();
    })
    .UseConsoleLifetime()
    .UseNServiceBus(context =>
    {
        // Configure the NServiceBus endpoint
        var endpointConfiguration = new EndpointConfiguration("Sender");

        var connectionString = context.Configuration.GetConnectionString("AzureServiceBusConnectionString");
        // If token credentials are to be used, the overload constructor for AzureServiceBusTransport would be used here
        var routing = endpointConfiguration.UseTransport(new AzureServiceBusTransport(connectionString));
        endpointConfiguration.UseSerialization<SystemJsonSerializer>();

        endpointConfiguration.AuditProcessedMessagesTo("audit");
        routing.RouteToEndpoint(typeof(Ping), "Receiver");

        endpointConfiguration.EnableInstallers();

        return endpointConfiguration;
    })
    .ConfigureServices(services => services.AddHostedService<SenderWorker>())
    .Build();

await host.RunAsync();

Há muito para desempacotar aqui, portanto, vamos examinar cada parte passo a passo.

Configurar um host para o ponto de extremidade

A hospedagem e o registro em log são configurados usando as opções padrão do Host Genérico da Microsoft. Por enquanto, o ponto de extremidade está configurado para ser executado como um aplicativo de console, mas pode ser modificado para ser executado no Azure Functions com alterações mínimas, que discutiremos mais adiante neste artigo.

Configurar o ponto de extremidade do NServiceBus

Depois, você informa ao host que ele deve usar o NServiceBus com o método de extensão .UseNServiceBus(…). O método recebe uma função de retorno de chamada que retorna um ponto de extremidade que será iniciado quando o host for executado.

Na configuração do ponto de extremidade, você vai especificar AzureServiceBus para o transporte, fornecendo uma cadeia de conexão do appsettings.json. Depois, você vai configurar o roteamento para que as mensagens do tipo Ping sejam enviadas para um ponto de extremidade chamado "Receptor". Ele permite que o NServiceBus automatize o processo de expedição da mensagem para o destino sem exigir o endereço do receptor.

A chamada para EnableInstallers vai configurar a topologia no namespace do Barramento de Serviço do Azure quando o ponto de extremidade for iniciado, criando as filas necessárias quando necessário. Em ambientes de produção, o script operacional é outra opção para criar a topologia.

Configurar o serviço em segundo plano para enviar mensagens

A parte final do remetente é SenderWorker, um serviço em segundo plano configurado para enviar uma mensagem Ping a cada segundo.

public class SenderWorker : BackgroundService
{
    private readonly IMessageSession messageSession;
    private readonly ILogger<SenderWorker> logger;

    public SenderWorker(IMessageSession messageSession, ILogger<SenderWorker> logger)
    {
        this.messageSession = messageSession;
        this.logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            var round = 0;
            while (!stoppingToken.IsCancellationRequested)
            {
                await messageSession.Send(new Ping { Round = round++ });;

                logger.LogInformation($"Message #{round}");

                await Task.Delay(1_000, stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // graceful shutdown
        }
    }
}

O IMessageSession usado em ExecuteAsync é injetado em SenderWorker e permite enviar mensagens usando o NServiceBus fora de um manipulador de mensagens. O roteamento configurado em Sender especifica o destino das mensagens Ping. Ele mantém a topologia do sistema (quais mensagens são encaminhadas para quais endereços) como uma questão separada do código empresarial.

O aplicativo Remetente também contém um PongHandler. Ele será abordado depois que discutirmos o Receptor a seguir.

Configurar o receptor

O Receptor é um ponto de extremidade que escuta uma mensagem Ping, registra em log quando uma mensagem é recebida e responde ao remetente. Nesta seção, vamos examinar rapidamente a configuração do ponto de extremidade, que é semelhante à do Remetente, e depois daremos atenção ao manipulador de mensagens.

Como o remetente, configure o receptor como um aplicativo de console usando o Host Genérico da Microsoft. Ele usa a mesma configuração de log e ponto de extremidade (com o Barramento de Serviço do Azure como o transporte de mensagem), mas com um nome diferente, para diferenciá-lo do remetente:

var endpointConfiguration = new EndpointConfiguration("Receiver");

Como esse ponto de extremidade responde apenas ao originador e não inicia novas conversas, não é necessária nenhuma configuração de roteamento. Ele também não precisa de um trabalho em segundo plano como o Remetente, pois só responde quando recebe uma mensagem.

O manipulador de mensagens Ping

O projeto Receptor contém um manipulador de mensagens chamado PingHandler:

public class PingHandler : NServiceBus.IHandleMessages<Ping>
{
    private readonly ILogger<PingHandler> logger;

    public PingHandler(ILogger<PingHandler> logger)
    {
        this.logger = logger;
    }

    public async Task Handle(Ping message, IMessageHandlerContext context)
    {
        logger.LogInformation($"Processing Ping message #{message.Round}");

        // throw new Exception("BOOM");

        var reply = new Pong { Acknowledgement = $"Ping #{message.Round} processed at {DateTimeOffset.UtcNow:s}" };

        await context.Reply(reply);
    }
}

Vamos ignorar o código comentado por enquanto. Voltaremos a ele mais tarde ao discutir a recuperação de uma falha.

A classe implementa IHandleMessages<Ping>, que define um método: Handle. Essa interface informa ao NServiceBus que, quando o ponto de extremidade recebe uma mensagem do tipo Ping, ela deve ser processada pelo método Handle nesse manipulador. O método Handle usa a própria mensagem como um parâmetro e um IMessageHandlerContext, que permite mais operações de mensagens, como responder, enviar comandos ou publicar eventos.

Nosso PingHandler é simples: quando uma mensagem Ping é recebida, ele registra os detalhes da mensagem e responde ao remetente com uma nova mensagem Pong, que é posteriormente tratada no PongHandler do Remetente.

Observação

Na configuração do Remetente, você especificou que as mensagens Ping devem ser encaminhadas ao Receptor. O NServiceBus adiciona metadados às mensagens que indicam, entre outras coisas, a origem da mensagem. É por isso que você não precisa especificar nenhum dado de roteamento para a mensagem de resposta Pong. Ela é encaminhada automaticamente de volta para a origem: o Remetente.

Com o Remetente e o Receptor configurados corretamente, agora você pode executar a solução.

Executar a solução

Para iniciar a solução, você precisa executar o Remetente e o Receptor. Se você estiver usando o Visual Studio Code, inicie a configuração "Depurar Tudo". Se você estiver usando o Visual Studio, configure a solução para iniciar os projetos Remetente e Receptor:

  1. No Gerenciador de Soluções, clique com o botão direito do mouse na solução
  2. Selecione "Definir Projetos de Inicialização..."
  3. Selecione Vários projetos de inicialização
  4. Para o Remetente e o Receptor, selecione "Iniciar" na lista suspensa

Inicie a solução. Dois aplicativos de console serão exibidos, um para o Remetente e outro para o Receptor.

No Remetente, observe que uma mensagem Ping é expedida a cada segundo, graças ao trabalho em segundo plano SenderWorker. O Receptor exibe os detalhes de cada mensagem Ping que recebe e o Remetente registra em log os detalhes de cada mensagem Pong que recebe em resposta.

Agora que tudo está funcionando, vamos detalhar tudo.

Resiliência em ação

Os erros são um fato real em sistemas de software. É inevitável que o código falhe e isso pode ocorrer por vários motivos, como falhas de rede, bloqueios de banco de dados, alterações em uma API de terceiros e erros de codificação simples antigos.

O NServiceBus tem recursos robustos de capacidade de recuperação para lidar com falhas. Quando um manipulador de mensagens falha, as mensagens são automaticamente repetidas novamente com base em uma política predefinida. Há dois tipos de política de repetição: repetições imediatas e repetições atrasadas. A melhor maneira de descrever como elas funcionam é vê-las em ação. Vamos adicionar uma política de repetição ao ponto de extremidade Receptor:

  1. Abra Program.cs no projeto Remetente
  2. Após a linha .EnableInstallers, adicione o seguinte código:
endpointConfiguration.SendFailedMessagesTo("error");
var recoverability = endpointConfiguration.Recoverability();
recoverability.Immediate(
    immediate =>
    {
        immediate.NumberOfRetries(3);
    });
recoverability.Delayed(
    delayed =>
    {
        delayed.NumberOfRetries(2);
        delayed.TimeIncrease(TimeSpan.FromSeconds(5));
    });

Antes de discutirmos como essa política funciona, vamos vê-la em ação. Para testar a política de capacidade de recuperação, você precisa simular um erro. Abra o código PingHandler no projeto Receptor e remova a marca de comentário desta linha:

throw new Exception("BOOM");

Agora, quando o Receptor processar uma mensagem Ping, ela falhará. Inicie a solução novamente e vamos ver o que acontece no Receptor.

Com nosso PingHandler menos confiável, todas as mensagens falham. Você pode ver a política de repetição em vigor para essas mensagens. Na primeira vez que uma mensagem falha, são feitas três novas tentativas imediatas:

Imagem mostrando a política de repetição imediata que tenta enviar as mensagens novamente até três vezes

É claro que ela continuará falhando, portanto, quando as três novas tentativas imediatas são feitas, a política de repetição atrasada é iniciada e a mensagem é atrasada por cinco segundos:

Imagem mostrando a política de repetição atrasada que atrasa as mensagens em incrementos de cinco segundos antes de tentar outra rodada de repetições imediatas

Depois que esses cinco segundos são decorridos, o envio da mensagem é tentado novamente três vezes (ou seja, outra iteração da política de repetição imediata). Isso também falhará e o NServiceBus atrasará a mensagem novamente, desta vez por dez segundos, antes de tentar novamente.

Se o PingHandler ainda não tiver êxito após executar a política de repetição completa, a mensagem será colocada em uma fila de erros centralizada, chamada error, conforme a definição na chamada a SendFailedMessagesTo.

Imagem mostrando a mensagem com falha

O conceito de fila de erros centralizada difere do mecanismo de mensagens mortas no Barramento de Serviço do Azure, que tem uma fila de mensagens mortas para cada fila de processamento. Com o NServiceBus, as filas de mensagens mortas no Barramento de Serviço do Azure atuam como filas de mensagens suspeitas reais, enquanto as mensagens que caem na fila de erros centralizada podem ser reprocessadas mais tarde, se necessário.

A política de repetição ajuda a resolver vários tipos de erros que geralmente são transitórios ou semitransitórios por natureza. Ou seja, erros que são temporários e que geralmente desaparecem quando a mensagem é simplesmente reprocessada após um pequeno atraso. Exemplos incluem falhas de rede, bloqueios de banco de dados e interrupções de API de terceiros.

Quando uma mensagem está na fila de erros, você pode examinar os detalhes da mensagem na ferramenta da sua escolha e decidir o que fazer com ela. Por exemplo, usando o ServicePulse, uma ferramenta de monitoramento da Particular Software, podemos ver os detalhes da mensagem e o motivo da falha:

Imagem mostrando o ServicePulse, da Particular Software

Depois de examinar os detalhes, você pode retornar a mensagem à respectiva fila original para processamento. Você também pode editar a mensagem antes de fazer isso. Quando houver várias mensagens na fila de erros que falharam pelo mesmo motivo, todas elas poderão ser retornadas aos respectivos destinos originais em lote.

Agora, é hora de descobrir onde implantar a solução no Azure.

Onde hospedar os serviços no Azure

Neste exemplo, os pontos de extremidade Remetente e Receptor são configurados para serem executados como aplicativos de console. Eles também podem ser hospedados em vários serviços do Azure, incluindo o Azure Functions, os Serviços de Aplicativos do Azure, as Instâncias de Contêiner do Azure, os Serviços de Kubernetes do Azure e as VMs do Azure. Por exemplo, veja como o ponto de extremidade do Remetente pode ser configurado para ser executado como uma Função do Azure:

[assembly: NServiceBusTriggerFunction("Sender")]
public class Program
{
    public static async Task Main()
    {
        var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults()
            .UseNServiceBus(configuration =>
            {
                configuration.Routing().RouteToEndpoint(typeof(Ping), "Receiver");
            })
            .Build();

        await host.RunAsync();
    }
}

Para obter mais informações sobre como usar o NServiceBus com o Functions, confira Azure Functions com Barramento de Serviço do Azure na documentação do NServiceBus.

Próximas etapas

Para saber mais sobre como usar o NServiceBus com os serviços do Azure, confira os seguintes artigos: