Criar aplicações empresariais orientadas por mensagens com o NServiceBus e Azure Service Bus

O NServiceBus é uma arquitetura de mensagens comerciais fornecida pelo Software Específico. Baseia-se na Azure Service Bus e ajuda os programadores a concentrarem-se na lógica de negócio ao abstrair as preocupações com a infraestrutura. Neste guia, vamos criar uma solução que troca mensagens entre dois serviços. Também vamos mostrar como repetir automaticamente mensagens com falhas e rever opções para alojar estes serviços no Azure.

Nota

O código para este tutorial está disponível no site Documentos de Software Específicos.

Pré-requisitos

O exemplo pressupõe que criou um espaço de nomes Azure Service Bus.

Importante

O NServiceBus requer, pelo menos, o escalão Standard. O escalão Básico não funcionará.

Transferir e preparar a solução

  1. Transfira o código a partir do site Documentos de Software Específicos. A solução SendReceiveWithNservicebus.sln consiste em três projetos:

    • Remetente: uma aplicação de consola que envia mensagens
    • Recetor: uma aplicação de consola que recebe mensagens do remetente e responde
    • Partilhado: uma biblioteca de classes que contém os contratos de mensagens partilhados entre o remetente e o recetor

    O diagrama seguinte, gerado pelo ServiceInsight, uma ferramenta de visualização e depuração de Software Específico, mostra o fluxo de mensagens:

    Imagem a mostrar o diagrama de sequência

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

  3. Abra appsettings.json nos projetos Recetor e Remetente e defina AzureServiceBusConnectionString para a cadeia de ligação do seu espaço de nomes Azure Service Bus.

Definir os contratos de mensagens partilhadas

A biblioteca de classes Partilhadas é onde define os contratos utilizados para enviar as nossas mensagens. Inclui uma referência ao NServiceBus pacote NuGet, que contém interfaces que pode utilizar para identificar as nossas mensagens. As interfaces não são necessárias, mas dão-nos alguma validação extra do NServiceBus e permitem que o código seja auto-documentado.

Primeiro, vamos rever a Ping.cs classe

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

A Ping classe define uma mensagem que o Remetente envia para o Recetor. É uma classe C# simples que implementa NServiceBus.ICommand, uma interface do pacote NServiceBus. Esta mensagem é um sinal para o leitor e para o NServiceBus de que é um comando, embora existam outras formas de identificar mensagens sem utilizar interfaces.

A outra classe de mensagens nos projetos Partilhados é Pong.cs:

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

Pong é também um objeto C# simples, embora este implemente NServiceBus.IMessage. A IMessage interface representa uma mensagem genérica que não é um comando nem um evento e é normalmente utilizada para respostas. No nosso exemplo, é uma resposta que o Recetor envia de volta para o Remetente para indicar que foi recebida uma mensagem.

Pong E Ping são os dois tipos de mensagens que irá utilizar. O próximo passo é configurar o Remetente para utilizar Azure Service Bus e enviar uma Ping mensagem.

Configurar o remetente

O Remetente é um ponto final que envia a nossa Ping mensagem. Aqui, vai configurar o Remetente para utilizar Azure Service Bus como o mecanismo de transporte e, em seguida, construir uma Ping instância e enviá-la.

Main No método de Program.cs, configure o ponto final 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 transport = endpointConfiguration.UseTransport<AzureServiceBusTransport>();
        var connectionString = context.Configuration.GetConnectionString("AzureServiceBusConnectionString");
        transport.ConnectionString(connectionString);

        transport.Routing().RouteToEndpoint(typeof(Ping), "Receiver");

        endpointConfiguration.EnableInstallers();
        endpointConfiguration.AuditProcessedMessagesTo("audit");

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

await host.RunAsync();

Há muito para descompactar aqui, por isso vamos revê-lo passo a passo.

Configurar um anfitrião para o ponto final

O alojamento e o registo são configurados com as opções padrão do Anfitrião Genérico da Microsoft. Por agora, o ponto final está configurado para ser executado como uma aplicação de consola, mas pode ser modificado para ser executado em Funções do Azure com alterações mínimas, que iremos abordar mais adiante neste artigo.

Configurar o ponto final NServiceBus

Em seguida, diga ao anfitrião para utilizar o NServiceBus com o .UseNServiceBus(…) método de extensão. O método utiliza uma função de chamada de retorno que devolve um ponto final que será iniciado quando o anfitrião for executado.

Na configuração do ponto final, especifique AzureServiceBus para o nosso transporte, fornecendo uma cadeia de ligação a partir de appsettings.json. Em seguida, irá configurar o encaminhamento para que as mensagens do tipo Ping sejam enviadas para um ponto final com o nome "Recetor". Permite ao NServiceBus automatizar o processo de envio da mensagem para o destino sem que seja necessário o endereço do recetor.

A chamada para irá configurar a EnableInstallers nossa topologia no espaço de nomes Azure Service Bus quando o ponto final for iniciado, criando as filas necessárias sempre que necessário. Nos ambientes de produção, o scripting 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 Ping mensagem 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++ })
                    .ConfigureAwait(false);

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

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

O IMessageSession utilizado no ExecuteAsync é injetado SenderWorker e permite-nos enviar mensagens com o NServiceBus fora de um processador de mensagens. O encaminhamento que configurou especifica Sender o destino das Ping mensagens. Mantém a topologia do sistema (que as mensagens são encaminhadas para os endereços) como uma preocupação separada do código empresarial.

A aplicação Remetente também contém um PongHandler. Voltará a fazê-lo depois de termos discutido o Recetor, o que faremos a seguir.

Configurar o recetor

O Recetor é um ponto final que escuta uma Ping mensagem, regista quando uma mensagem é recebida e responde ao remetente. Nesta secção, vamos rever rapidamente a configuração do ponto final, que é semelhante ao Remetente e, em seguida, virar a nossa atenção para o processador de mensagens.

Tal como o remetente, configure o recetor como uma aplicação de consola com o Anfitrião Genérico da Microsoft. Utiliza a mesma configuração de registo e ponto final (com Azure Service Bus como transporte de mensagens), mas com um nome diferente, para o distinguir do remetente:

var endpointConfiguration = new EndpointConfiguration("Receiver");

Uma vez que este ponto final só responde ao respetivo criador e não inicia novas conversações, não é necessária nenhuma configuração de encaminhamento. Também não precisa de uma função de trabalho em segundo plano como o Remetente, uma vez que só responde quando recebe uma mensagem.

O processador de mensagens ping

O projeto Recetor contém um processador de mensagens com o nome 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 agora; Voltaremos mais tarde quando falarmos sobre a recuperação do fracasso.

A classe implementa IHandleMessages<Ping>, que define um método: Handle. Esta interface indica ao NServiceBus que quando o ponto final recebe uma mensagem do tipo Ping, deve ser processado pelo Handle método neste processador. O Handle método assume a mensagem como um parâmetro e um IMessageHandlerContext, que permite mais operações de mensagens, como responder, enviar comandos ou publicar eventos.

O nosso PingHandler é simples: quando uma Ping mensagem é recebida, registe os detalhes da mensagem e responda ao remetente com uma nova Pong mensagem.

Nota

Na configuração do Remetente, especificou que Ping as mensagens devem ser encaminhadas para o Recetor. NServiceBus adiciona metadados às mensagens que indicam, entre outras coisas, a origem da mensagem. É por isso que não precisa de especificar quaisquer dados de encaminhamento para a Pong mensagem de resposta; são automaticamente encaminhados de volta para a origem: o Remetente.

Com o Remetente e o Recetor configurados corretamente, agora pode executar a solução.

Executar a solução

Para iniciar a solução, tem de executar o Remetente e o Recetor. Se estiver a utilizar o Visual Studio Code, inicie a configuração "Depurar Tudo". Se estiver a utilizar o Visual Studio, configure a solução para iniciar os projetos Remetente e Recetor:

  1. Clique com o botão direito do rato na solução no Explorador de Soluções
  2. Selecione "Definir Projetos de Arranque..."
  3. Selecione Vários projetos de arranque
  4. Para o Remetente e o Recetor, selecione "Iniciar" na lista pendente

Inicie a solução. Serão apresentadas duas aplicações de consola, uma para o Remetente e outra para o Recetor.

No Remetente, repare que é enviada uma Ping mensagem a cada segundo, graças à tarefa SenderWorker em segundo plano. O Recetor apresenta os detalhes de cada Ping mensagem que recebe e o Remetente regista os detalhes de cada Pong mensagem que recebe em resposta.

Agora que tem tudo a funcionar, vamos quebrá-lo.

Resiliência em ação

Os erros são um facto de vida em sistemas de software. É inevitável que o código falhe e pode fazê-lo por vários motivos, tais como falhas de rede, bloqueios de bases de dados, alterações numa API de terceiros e erros de codificação simples e antigos.

O NServiceBus tem funcionalidades de recuperação robustas para lidar com falhas. Quando um processador de mensagens falha, as mensagens são repetidas automaticamente com base numa política predefinida. Existem dois tipos de política de repetição: repetições imediatas e repetições atrasadas. A melhor forma de descrever como funcionam é vê-los em ação. Vamos adicionar uma política de repetição ao nosso ponto final do Recetor:

  1. Abrir Program.cs no projeto Remetente
  2. Depois da .EnableInstallers linha, 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 esta política funciona, vamos vê-la em ação. Antes de testar a política de recuperação, tem de simular um erro. Abra o PingHandler código no projeto Recetor e anule o comentário desta linha:

throw new Exception("BOOM");

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

Com as nossas mensagens menos fiáveis PingHandler, todas as nossas mensagens falham. Pode ver a política de repetição a iniciar sessão nessas mensagens. A primeira vez que uma mensagem falha, é repetida imediatamente até três vezes:

Imagem a mostrar a política de repetição imediata que repetiu mensagens até 3 vezes

É claro que continuará a falhar, pelo que, quando as três repetições imediatas forem utilizadas, a política de repetição atrasada é iniciada e a mensagem é adiada por 5 segundos:

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

Após esses 5 segundos terem passado, a mensagem é repetida novamente mais três vezes (ou seja, outra iteração da política de repetição imediata). Estes também falharão e o NServiceBus irá atrasar novamente a mensagem, desta vez durante 10 segundos, antes de tentar novamente.

Se PingHandler , mesmo assim, não for bem-sucedida depois de executar a política de repetição completa, a mensagem é colocada numa fila de erros centralizada, denominadaerror, conforme definido pela chamada para SendFailedMessagesTo.

Imagem a mostrar a mensagem com falha

O conceito de uma fila de erros centralizada difere do mecanismo de letras não entregues no Azure Service Bus, que tem uma fila de letras não entregues para cada fila de processamento. Com o NServiceBus, as filas de letras mortas no Azure Service Bus funcionam como verdadeiras filas de mensagens venenosas, enquanto as mensagens que acabam na fila de erros centralizadas podem ser reprocessadas mais tarde, se necessário.

A política de repetição ajuda a resolver vários tipos de erros que muitas vezes são transitórios ou de natureza semi transitória. Ou seja, os erros que são temporários e muitas vezes desaparecem se a mensagem for simplesmente reprocessada após um curto atraso. Os exemplos incluem falhas de rede, bloqueios de bases de dados e falhas de API de terceiros.

Assim que uma mensagem estiver na fila de erros, pode examinar os detalhes da mensagem na ferramenta à sua escolha e, em seguida, decidir o que fazer com a mesma. Por exemplo, com o ServicePulse, uma ferramenta de monitorização de Software Específico, podemos ver os detalhes da mensagem e o motivo da falha:

Imagem a mostrar ServicePulse, de Software Específico

Depois de examinar os detalhes, pode enviar a mensagem de volta para a fila original para processamento. Também pode editar a mensagem antes de o fazer. Se existirem várias mensagens na fila de erros, que falharam pelo mesmo motivo, todas podem ser enviadas de volta para os destinos originais como um lote.

Em seguida, é hora de descobrir onde implementar a nossa solução no Azure.

Onde alojar os serviços no Azure

Neste exemplo, os pontos finais Remetente e Recetor estão configurados para serem executados como aplicações de consola. Também podem ser alojados em vários serviços do Azure, incluindo Funções do Azure, Aplicação Azure AD Services, Azure Container Instances, Azure Kubernetes Services e VMs do Azure. Por exemplo, eis como o ponto final do Remetente pode ser configurado para ser executado como uma Função do Azure:

[assembly: FunctionsStartup(typeof(Startup))]
[assembly: NServiceBusEndpointName("Sender")]

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.UseNServiceBus(() =>
        {
            var configuration = new ServiceBusTriggeredEndpointConfiguration("Sender");
            var transport = configuration.AdvancedConfiguration.Transport;
            transport.Routing().RouteToEndpoint(typeof(Ping), "Receiver");

            return configuration;
        });
    }
}

Para obter mais informações sobre como utilizar o NServiceBus com Funções, veja Funções do Azure com Azure Service Bus na documentação do NServiceBus.

Passos seguintes

Para obter mais informações sobre como utilizar o NServiceBus com os serviços do Azure, veja os seguintes artigos: