Migrar do Orleans 3.x para o 7.0

O Orleans 7.0 apresenta várias alterações benéficas, incluindo melhorias na hospedagem, serialização personalizada, imutabilidade e abstrações de granularidade.

Migração

Aplicativos existentes que usam lembretes, fluxos ou persistência de granularidade não podem ser migrados facilmente para o Orleans 7.0 devido a alterações na forma como o Orleans identifica granularidades e fluxos. Planejamos oferecer incrementalmente um caminho de migração para esses aplicativos.

Os aplicativos que executam versões anteriores do Orleans não podem ser atualizados sem problemas por meio de uma atualização sem interrupção para o Orleans 7.0. Portanto, uma estratégia de atualização diferente deve ser usada, como implantar um novo cluster e desativar o cluster anterior. O Orleans 7.0 altera o protocolo de transmissão de forma incompatível, o que significa que os clusters não podem conter uma combinação de hosts do Orleans 7.0 e hosts que executam versões anteriores do Orleans.

Evitamos as alterações interruptivas por muitos anos, mesmo em versões principais, então por que agora? Há dois motivos principais: identidades e serialização. Em relação a identidades, as identidades de granularidade e fluxo agora são compostas por cadeias de caracteres, permitindo que as granularidades codifiquem corretamente informações de tipo genérico e permitindo que os fluxos sejam mapeados com mais facilidade para o domínio do aplicativo. Os tipos de granularidade foram identificados anteriormente usando uma estrutura de dados complexa que não podia representar granularidades genéricas, o que resultava em casos extremos. Os fluxos foram identificados por um string namespace e uma Guid chave, o que foi difícil para os desenvolvedores mapearem para o domínio do aplicativo, por mais eficiente que fosse. A serialização agora é tolerante a versões, o que significa que você pode modificar seus tipos de determinadas maneiras compatíveis, seguindo um conjunto de regras e ter certeza de que pode atualizar seu aplicativo sem erros de serialização. Isso era especialmente problemático quando os tipos de aplicativo persistiam em fluxos ou em armazenamento de granularidade. As seções a seguir detalham as principais alterações e as discutem com mais detalhes.

Alterações de pacote

Se você estiver atualizando um projeto para o Orleans 7.0, precisará executar as seguintes ações:

  • Todos os clientes devem referenciar Microsoft.Orleans.Client.
  • Todos os silos (servidores) devem referenciar Microsoft.Orleans.Server.
  • Todos os outros pacotes devem referenciar Microsoft.Orleans.Sdk.
  • Remova todas as referências a Microsoft.Orleans.CodeGenerator.MSBuild e Microsoft.Orleans.OrleansCodeGenerator.Build.
  • Remova todas as referências a Microsoft.Orleans.OrleansRuntime.
  • Remova chamadas para ConfigureApplicationParts. As Partes do Aplicativo foram removidas. O Gerador de Origem C# para Orleans é adicionado a todos os pacotes (incluindo o cliente e o servidor) e gerará o equivalente das Partes do Aplicativo automaticamente.
  • Substitua referências a Microsoft.Orleans.OrleansServiceBus por Microsoft.Orleans.Streaming.EventHubs
  • Se você estiver usando lembretes, adicione uma referência a Microsoft.Orleans.Reminders
  • Se você estiver usando fluxos, adicione uma referência a Microsoft.Orleans.Streaming

Dica

Todos os exemplos do Orleans foram atualizados para o Orleans 7.0 e podem ser usados como referência para as alterações que forem feitas. Para mais informações, consulte o Problema do Orleans #8035, que especifica as alterações feitas a cada exemplo.

Diretivas Orleansglobal using

Todos os projetos do Orleans referenciam direta ou indiretamente o pacote NuGet Microsoft.Orleans.Sdk. Quando um projeto do Orleans é configurado para habilitar usos implícitos (por exemplo <ImplicitUsings>enable</ImplicitUsings>), os namespaces Orleans e Orleans.Hosting são usados implicitamente. Isso significa que o seu código do aplicativo não precisa dessas diretivas.

Para obter mais informações, consulte ImplicitUsings e dotnet/orleans/src/Orleans.Sdk/build/Microsoft.Orleans.Sdk.targets.

Hosting

O tipo ClientBuilder foi substituído por um método de extensão UseOrleansClient em IHostBuilder. O tipo IHostBuilder vem do pacote NuGet Microsoft.Extensions.Hosting. Isso significa que você pode adicionar um cliente Orleans a um host existente sem precisar criar um contêiner de injeção de dependência separado. O cliente se conecta ao cluster durante a inicialização. Depois de IHost.StartAsync ter concluído, o cliente será conectado automaticamente. Os serviços adicionados ao IHostBuilder são iniciados na ordem de registro, portanto, chamar UseOrleansClient antes de ConfigureWebHostDefaults garantirá que o Orleans seja iniciado antes que ASP.NET Core seja iniciado, por exemplo, permitindo que você acesse o cliente de seu aplicativo ASP.NET Core imediatamente.

Se você quiser emular o comportamento anterior ClientBuilder, você poderá criar um HostBuilder separado e configurá-lo com um cliente do Orleans. O IHostBuilder pode ter um cliente do Orleans ou um silo do Orleans configurado. Todos os silos registram uma instância de IGrainFactory e IClusterClient que o aplicativo pode usar, portanto, configurar um cliente separadamente é desnecessário e não compatível.

alteração de assinatura OnActivateAsync e OnDeactivateAsync

O Orleans permite que as granularidades executem o código durante a ativação e a desativação. Isso pode ser usado para executar tarefas como o estado de leitura do armazenamento ou mensagens do ciclo de vida do log. No Orleans 7.0, a assinatura desses métodos de ciclo de vida mudou:

  • OnActivateAsync() agora aceita um parâmetro CancellationToken. Quando o CancellationToken for cancelado, o processo de ativação deverá ser abandonado.
  • OnDeactivateAsync() agora aceita um parâmetro DeactivationReason e um parâmetro CancellationToken. O DeactivationReason indica por que a ativação está sendo desativada. Espera-se que os desenvolvedores usem essas informações para fins de registro em log e diagnóstico. Quando o CancellationToken for cancelado, o processo de desativação deverá ser concluído imediatamente. Observe que, como qualquer host pode falhar a qualquer momento, não é recomendável confiar em OnDeactivateAsync para executar ações importantes, como manter o estado crítico.

Considere o seguinte exemplo de uma granularidade substituindo esses novos métodos:

public sealed class PingGrain : Grain, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(ILogger<PingGrain> logger) =>
        _logger = logger;

    public override Task OnActivateAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("OnActivateAsync()");
        return Task.CompletedTask;
    }

    public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
    {
        _logger.LogInformation("OnDeactivateAsync({Reason})", reason);
        return Task.CompletedTask;
    }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

Granularidades POCO e IGrainBase

As granularidades no Orleans não precisam mais herdar da classe base Grain nem de qualquer outra classe. Essa funcionalidade é conhecida como granularidades POCO. Para acessar métodos de extensão, tais como qualquer um dos seguintes:

Sua granularidade deve implementar IGrainBase ou herdar de Grain. Aqui está um exemplo de implementação IGrainBase em uma classe de granularidade:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    public PingGrain(IGrainContext context) => GrainContext = context;

    public IGrainContext GrainContext { get; }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

IGrainBase também define OnActivateAsync e OnDeactivateAsync com implementações padrão, permitindo que sua granularidade participe do ciclo de vida, se desejado:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(IGrainContext context, ILogger<PingGrain> logger)
    {
        _logger = logger;
        GrainContext = context;
    }

    public IGrainContext GrainContext { get; }

    public Task OnActivateAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("OnActivateAsync()");
        return Task.CompletedTask;
    }

    public Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
    {
        _logger.LogInformation("OnDeactivateAsync({Reason})", reason);
        return Task.CompletedTask;
    }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

Serialização

A mudança mais pesada no Orleans 7.0 é a introdução do serializador tolerante a versão. Essa alteração foi feita porque os aplicativos tendem a evoluir, o que criou uma armadilha significativa para os desenvolvedores, já que o serializador anterior não tolerava a adição de propriedades a tipos existentes. Por outro lado, o serializador era flexível, permitindo que os desenvolvedores representassem a maioria dos tipos .NET sem modificação, incluindo recursos como genéricos, polimorfismo e acompanhamento de referência. Uma substituição estava muito atrasada, mas os usuários ainda precisam de representação de alta fidelidade de seus tipos. Portanto, um serializador de substituição foi introduzido no Orleans 7.0, que dá suporte à declaração de alta fidelidade de tipos .NET e permite também que os tipos evoluam. O novo serializador é muito mais eficiente do que o serializador anterior, resultando em uma taxa de transferência de ponta a ponta até 170% maior.

Para obter mais informações, confira os seguintes artigos relacionados ao Orleans 7.0:

Identidades de granularidade

Cada granularidade tem uma identidade exclusiva que é composta pelo tipo da granularidade e sua chave. As versões anteriores do Orleans usavam um tipo composto para GrainIds para dar suporte a chaves de granularidade de:

Isso envolve alguma complexidade quando se trata de lidar com chaves de granularidade. As identidades de granularidade consistem em dois componentes: um tipo e uma chave. O componente de tipo consistia anteriormente em um código de tipo numérico, uma categoria e 3 bytes de informações de tipo genérico.

As identidades de granularidade agora assumem a forma type/key em que type e key são cadeias de caracteres. A interface de chave de granularidade mais comumente usada é a IGrainWithStringKey. Isso simplifica muito o funcionamento da identidade de granularidade e melhora o suporte para tipos genéricos de granularidade.

As interfaces de granularidade também são agora representadas usando um nome legível por humanos, em vez de uma combinação de um código hash e uma representação de cadeia de caracteres de qualquer parâmetro de tipo genérico.

O novo sistema é mais personalizável e essas personalizações podem ser controladas por atributos.

  • GrainTypeAttribute(String) em uma granularidade class especifica a parte Tipo de sua ID de granularidade.
  • DefaultGrainTypeAttribute(String) em uma granularidade interface especifica o Tipo da granularidade que IGrainFactory deve resolver por padrão ao obter uma referência de granularidade. Por exemplo, ao chamar IGrainFactory.GetGrain<IMyGrain>("my-key"), a fábrica de granularidade retornará uma referência à granularidade "my-type/my-key" se IMyGrain tiver o atributo acima mencionado especificado.
  • GrainInterfaceTypeAttribute(String) permite substituir o nome da interface. Especificar um nome explicitamente usando esse mecanismo permite renomear o tipo de interface sem interromper a compatibilidade com referências de granularidade existentes. Observe que sua interface também deve ter o AliasAttribute nesse caso, pois sua identidade pode ser serializada. Para mais informações sobre como especificar um alias de tipo, consulte a seção sobre serialização.

Conforme mencionado acima, substituir os nomes de classe e interface de granularidade padrão para seus tipos permite renomear os tipos subjacentes sem interromper a compatibilidade com implantações existentes.

Identidades de fluxo

Quando os fluxos do Orleans foram lançados, eles só podiam ser identificados usando um Guid. Isso era eficiente em termos de alocação de memória, mas era difícil para os usuários criarem identidades de fluxo significativas, muitas vezes exigindo alguma codificação ou indireção para determinar a identidade de fluxo apropriada para uma determinada finalidade.

No Orleans 7.0, os fluxos agora são identificados usando cadeias de caracteres. A classe Orleans.Runtime.StreamIdstruct contém três propriedades: um StreamId.Namespace, um StreamId.Key, e um StreamId.FullKey. Esses valores de propriedade são cadeias de caracteres UTF-8 codificadas. Por exemplo, StreamId.Create(String, String).

Substituição de SimpleMessageStreams por BroadcastChannel

SimpleMessageStreams (também chamado de SMS) foi removido na versão 7.0. O SMS tinha a mesma interface que Orleans.Providers.Streams.PersistentStreams, mas seu comportamento era muito diferente, pois se baseava em chamadas diretas de granularidade para granularidade. Para evitar confusão, o SMS foi removido e uma nova substituição chamada Orleans.BroadcastChannel foi introduzida.

BroadcastChannel só dá suporte a assinaturas implícitas e pode ser um substituto direto nesse caso. Se você precisar de assinaturas explícitas ou precisar usar a interface PersistentStream (por exemplo, você estava usando SMS em testes enquanto usava EventHub em produção), então MemoryStream será o melhor candidato para você.

BroadcastChannel terá os mesmos comportamentos que o SMS, enquanto MemoryStream se comportará como outros provedores de fluxo. Considere o seguinte exemplo de uso do Broadcast Channel:

// Configuration
builder.AddBroadcastChannel(
    "my-provider",
    options => options.FireAndForgetDelivery = false);

// Publishing
var grainKey = Guid.NewGuid().ToString("N");
var channelId = ChannelId.Create("some-namespace", grainKey);
var stream = provider.GetChannelWriter<int>(channelId);

await stream.Publish(1);
await stream.Publish(2);
await stream.Publish(3);

// Simple implicit subscriber example
[ImplicitChannelSubscription]
public sealed class SimpleSubscriberGrain : Grain, ISubscriberGrain, IOnBroadcastChannelSubscribed
{
    // Called when a subscription is added to the grain
    public Task OnSubscribed(IBroadcastChannelSubscription streamSubscription)
    {
        streamSubscription.Attach<int>(
          item => OnPublished(streamSubscription.ChannelId, item),
          ex => OnError(streamSubscription.ChannelId, ex));

        return Task.CompletedTask;

        // Called when an item is published to the channel
        static Task OnPublished(ChannelId id, int item)
        {
            // Do something
            return Task.CompletedTask;
        }

        // Called when an error occurs
        static Task OnError(ChannelId id, Exception ex)
        {
            // Do something
            return Task.CompletedTask;
        }
    }
}

A migração para MemoryStream será mais fácil, pois apenas a configuração precisa ser alterada. Considere a configuração MemoryStream a seguir:

builder.AddMemoryStreams<DefaultMemoryMessageBodySerializer>(
    "in-mem-provider",
    _ =>
    {
        // Number of pulling agent to start.
        // DO NOT CHANGE this value once deployed, if you do rolling deployment
        _.ConfigurePartitioning(partitionCount: 8);
    });

OpenTelemetry

O sistema de telemetria foi atualizado no Orleans 7.0 e o sistema anterior foi removido em favor de APIs .NET padronizadas, como métricas do .NET para métricas e ActivitySource para rastreamento.

Como parte disso, os pacotes Microsoft.Orleans.TelemetryConsumers.*existentes foram removidos. Estamos considerando introduzir um novo conjunto de pacotes para simplificar o processo de integração das métricas emitidas pelo Orleans para a solução de monitoramento de sua escolha. Como sempre, comentários e contribuições são bem-vindos.

A ferramenta dotnet-counters apresenta monitoramento de desempenho para monitoramento de integridade ad hoc e investigação de desempenho de primeiro nível. Para contadores do Orleans, a ferramenta dotnet-counters pode ser usada para monitorá-los:

dotnet counters monitor -n MyApp --counters Microsoft.Orleans

Da mesma forma, as métricas do OpenTelemetry podem adicionar os medidores Microsoft.Orleans, conforme mostrado no seguinte código:

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddPrometheusExporter()
        .AddMeter("Microsoft.Orleans"));

Para habilitar o rastreamento distribuído, configure o OpenTelemetry conforme mostrado no seguinte código:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService(serviceName: "ExampleService", serviceVersion: "1.0"));

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.Orleans.Runtime");
        tracing.AddSource("Microsoft.Orleans.Application");

        tracing.AddZipkinExporter(options =>
        {
            options.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
        });
    });

No código anterior, OpenTelemetry está configurado para monitorar:

  • Microsoft.Orleans.Runtime
  • Microsoft.Orleans.Application

Para propagar a atividade, chame AddActivityPropagation:

builder.Host.UseOrleans((_, clientBuilder) =>
{
    clientBuilder.AddActivityPropagation();
});

Refatorar recursos do pacote principal em pacotes separados

No Orleans 7.0, fizemos um esforço para fatorar extensões em pacotes separados que não dependem de Orleans.Core. Ou seja, Orleans.Streaming, Orleans.Reminderse Orleans.Transactions foram separados do núcleo. Isso significa que esses pacotes são totalmente pagos pelo que você usa e não há código no núcleo do Orleans dedicado a esses recursos. Isso reduz a superfície da API principal e o tamanho do assembly, simplifica o núcleo e melhora o desempenho. Em relação ao desempenho, as transações no Orleans exigiam anteriormente algum código que foi executado para cada método para coordenar possíveis transações. Desde então, isso foi movido para por método.

Essa é uma alteração interruptiva da compilação. Você pode ter um código existente que interage com lembretes ou fluxos chamando métodos definidos anteriormente na classe base Grain, mas que agora são métodos de extensão. Essas chamadas que não especificam this (por exemplo GetReminders) precisarão ser atualizadas para incluir this (por exemplo this.GetReminders()) porque os métodos de extensão devem ser qualificados. Haverá um erro de compilação se você não atualizar essas chamadas e a alteração de código necessária não pode ser óbvia se você não souber o que mudou.

Cliente de transação

O Orleans 7.0 introduz uma nova abstração para coordenar transações, Orleans.ITransactionClient. Anteriormente, as transações só podiam ser coordenadas por granularidades. Com ITransactionClient, que está disponível por meio de injeção de dependência, os clientes também podem coordenar transações sem precisar de uma granularidade intermediária. O exemplo a seguir retira créditos de uma conta e os deposita em outra dentro de uma única transação. Esse código pode ser chamado de dentro de uma granularidade ou de um cliente externo que recuperou o ITransactionClient do contêiner de injeção de dependência.

await transactionClient.RunTransaction(
  TransactionOption.Create,
  () => Task.WhenAll(from.Withdraw(100), to.Deposit(100)));

Para transações coordenadas pelo cliente, o cliente deve adicionar os serviços necessários durante o tempo de configuração:

clientBuilder.UseTransactions();

O exemplo BankAccount demonstra o uso de ITransactionClient. Para obter mais informações, consulte Transações do Orleans.

Reentrância da cadeia de chamadas

As granularidades são de thread único e processam solicitações uma a uma do início à conclusão por padrão. Em outras palavras, as granularidades não são reentrantes por padrão. Adicionar o ReentrantAttribute a uma classe de granularidade permite que várias solicitações sejam processadas simultaneamente, de forma intercalada, enquanto ainda estão sendo de thread único. Isso pode ser útil para granularidades que não contêm estado interno ou executam muitas operações assíncronas, como emitir chamadas HTTP ou gravar em um banco de dados. É necessário ter cuidado extra quando as solicitações puderem intercalar: é possível que o estado de uma granularidade observado antes de uma instrução await tenha mudado no momento que a operação assíncrona é concluída e o método retoma a execução.

Por exemplo, a granularidade a seguir representa um contador. Ele foi marcado como Reentrant, permitindo que várias chamadas intercalem. O método Increment() deve incrementar o contador interno e retornar o valor observado. No entanto, como o corpo do método Increment() observa o estado da granularidade antes de um ponto await e o atualiza posteriormente, é possível que várias execuções de intercalação de Increment() possam resultar em um _value menor do que o total de Increment() chamadas recebidas. Esse é um erro introduzido pelo uso inadequado da reentrância.

Remover o ReentrantAttribute é suficiente para corrigir o problema.

[Reentrant]
public sealed class CounterGrain : Grain, ICounterGrain
{
    int _value;
    
    /// <summary>
    /// Increments the grain's value and returns the previous value.
    /// </summary>
    public Task<int> Increment()
    {
        // Do not copy this code, it contains an error.
        var currentVal = _value;
        await Task.Delay(TimeSpan.FromMilliseconds(1_000));
        _value = currentVal + 1;
        return currentValue;
    }
}

Para evitar esses erros, as granularidades não são reentrantes por padrão. A desvantagem disso é a taxa de transferência reduzida para granularidades que executam operações assíncronas em sua implementação, já que outras solicitações não podem ser processadas enquanto a granularidade aguarda a conclusão de uma operação assíncrona. Para aliviar isso, o Orleans oferece várias opções para permitir a reentrância em determinados casos:

  • Para uma classe inteira: colocar o ReentrantAttribute na granularidade permite que qualquer solicitação para a granularidade intercale com qualquer outra solicitação.
  • Para um subconjunto de métodos: colocar o AlwaysInterleaveAttribute no método de interface de granularidade permite que as solicitações a esse método intercalem com qualquer outra solicitação e que as solicitações a esse método sejam intercaladas por qualquer outra solicitação.
  • Para um subconjunto de métodos: colocar o ReadOnlyAttribute no método de interface de granularidade permite que as solicitações a esse método intercalem com qualquer outra solicitação ReadOnly e que as solicitações a esse método sejam intercaladas por qualquer outra solicitação ReadOnly. Nesse sentido, é uma forma mais restrita de AlwaysInterleave.
  • Para qualquer solicitação em uma cadeia de chamadas: RequestContext.AllowCallChainReentrancy() e <xref:Orleans. Runtime.RequestContext.SuppressCallChainReentrancy?displayProperty=nameWithType permite aceitar e recusar que solicitações downstream retornem à granularidade. As chamadas retornam um valor que precisa ser descartado ao sair da solicitação. Portanto, o uso adequado é o seguinte:
public Task<int> OuterCall(IMyGrain other)
{
    // Allow call-chain reentrancy for this grain, for the duration of the method.
    using var _ = RequestContext.AllowCallChainReentrancy();
    await other.CallMeBack(this.AsReference<IMyGrain>());
}

public Task CallMeBack(IMyGrain grain)
{
    // Because OuterCall allowed reentrancy back into that grain, this method 
    // will be able to call grain.InnerCall() without deadlocking.
    await grain.InnerCall();
}

public Task InnerCall() => Task.CompletedTask;

A reentrância da cadeia de chamadas deve ser aceita por granularidade, por cadeia de chamadas. Por exemplo, considere dois grãos, granularidade A e granularidade B. Se a granularidade A habilitar a reentrância da cadeia de chamadas antes de chamar a granularidade B, a granularidade B poderá chamar de volta a granularidade A nessa chamada. No entanto, a granularidade A não poderá chamar de volta a granularidade B se a granularidade B também não tiver habilitado a reentrância da cadeia de chamadas. É por granularidade, por cadeia de chamada.

As granularidades também podem suprimir as informações de reentrância da cadeia de chamadas de fluir para baixo em uma cadeia de chamadas usando using var _ = RequestContext.SuppressCallChainReentrancy(). Isso impede a reentrada de chamadas subsequentes.

Scripts de migração ADO.NET

Para garantir a compatibilidade futura com o clustering, a persistência e os lembretes do Orleans que dependem de ADO.NET, você precisará do script de migração de SQL apropriado:

Selecione os arquivos para o banco de dados usado e aplique-os na ordem.