Compartilhar via


Desenvolver um grão

Antes de escrever o código para implementar uma classe de grão, crie um projeto de Biblioteca de Classes direcionado ao .NET Standard, ao .NET Core (preferencial) ou ao .NET Framework 4.6.1 ou superior (se não for possível usar o .NET Standard ou o .NET Core devido a dependências). As interfaces e as classes de grão podem ser definidas no mesmo projeto da Biblioteca de Classes ou em dois projetos diferentes para separar melhor as interfaces da implementação. Em ambos os casos, os projetos precisam referenciar os pacotes NuGet Microsoft.Orleans.Core.Abstractions e Microsoft.Orleans.CodeGenerator.MSBuild.

Para obter instruções mais completas, veja a seção Configuração do projeto do Tutorial 1 – Noções básicas do Orleans.

Interfaces e classes de grão

Os grãos interagem entre si e são chamados de fora invocando métodos declarados como parte das respectivas interfaces. Uma classe de grão implementa uma ou mais interfaces de grão declaradas anteriormente. Todos os métodos de uma interface de grão devem retornar um Task para métodos void e um Task<TResult> ou um ValueTask<TResult> para métodos que retornam valores do tipo T.

Veja o seguinte trecho do exemplo do Serviço de Presença do Orleans versão 1.5:

public interface IPlayerGrain : IGrainWithGuidKey
{
    Task<IGameGrain> GetCurrentGame();
    Task JoinGame(IGameGrain game);
    Task LeaveGame(IGameGrain game);
}

public class PlayerGrain : Grain, IPlayerGrain
{
    private IGameGrain _currentGame;

    // Game the player is currently in. May be null.
    public Task<IGameGrain> GetCurrentGame()
    {
       return Task.FromResult(_currentGame);
    }

    // Game grain calls this method to notify that the player has joined the game.
    public Task JoinGame(IGameGrain game)
    {
       _currentGame = game;

       Console.WriteLine(
           $"Player {GetPrimaryKey()} joined game {game.GetPrimaryKey()}");

       return Task.CompletedTask;
    }

   // Game grain calls this method to notify that the player has left the game.
   public Task LeaveGame(IGameGrain game)
   {
       _currentGame = null;

       Console.WriteLine(
           $"Player {GetPrimaryKey()} left game {game.GetPrimaryKey()}");

       return Task.CompletedTask;
   }
}

Valores de retorno de métodos de grão

Um método de grão que retorna um valor do tipo T é definido em uma interface de grão como se retornasse um Task<T>. Para métodos de grão não marcados com a palavra-chave async, quando o valor retornado está disponível, ele geralmente é retornado por meio da seguinte instrução:

public Task<SomeType> GrainMethod1()
{
    return Task.FromResult(GetSomeType());
}

Um método de grão que não retorna nenhum valor, ou seja, efetivamente um método nulo, é definido em uma interface de grão como se retornasse um Task. O Task retornado indica a execução assíncrona e a conclusão do método. Para métodos de grão não marcados com a palavra-chave async, quando a execução de um método "nulo" é concluída, ele precisa retornar o valor especial de Task.CompletedTask:

public Task GrainMethod2()
{
    return Task.CompletedTask;
}

Um método de grão marcado como async retorna o valor diretamente:

public async Task<SomeType> GrainMethod3()
{
    return await GetSomeTypeAsync();
}

Um método de grão void marcado como async que não retorna nenhum valor é simplesmente retornado ao final da execução:

public async Task GrainMethod4()
{
    return;
}

Se um método de grão recebe o valor de retorno de outra chamada de método assíncrono, para um grão ou não, e não precisa realizar o tratamento de erros dessa chamada, ele pode simplesmente retornar o Task recebido dessa chamada assíncrona:

public Task<SomeType> GrainMethod5()
{
    Task<SomeType> task = CallToAnotherGrain();

    return task;
}

Da mesma forma, um método de grão void pode retornar um Task que é retornado a ele por outra chamada em vez de aguardá-lo.

public Task GrainMethod6()
{
    Task task = CallToAsyncAPI();
    return task;
}

ValueTask<T> pode ser usado em vez de Task<T>.

Referência de grão

Uma referência de grão é um objeto proxy que implementa a mesma interface de grão que a classe de grão correspondente. Ela encapsula a identidade lógica (tipo e chave exclusiva) do grão de destino. Uma referência de grão é usada para fazer chamadas ao grão de destino. Cada referência de grão destina-se a um único grão (uma única instância da classe de grão), mas é possível criar diversas referências independentes para o mesmo grão.

Como uma referência de grão representa a identidade lógica do grão de destino, ela é independente da localização física do grão e permanece válida mesmo após uma reinicialização completa do sistema. Os desenvolvedores podem usar referências de grão como qualquer outro objeto .NET. Ela pode ser transmitida para um método, usada como um valor de retorno de método etc., e pode até mesmo ser salva em um armazenamento persistente.

Uma referência de grão pode ser obtida transmitindo a identidade de um grão para o método IGrainFactory.GetGrain<TGrainInterface>(Type, Guid), em que T é a interface do grão e key é a chave exclusiva do grão dentro do tipo.

Veja a seguir exemplos de como obter uma referência de grão da interface IPlayerGrain definida acima.

Dentro de uma classe de grão:

IPlayerGrain player = GrainFactory.GetGrain<IPlayerGrain>(playerId);

No código do cliente Orleans.

IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);

Invocação do método de grão

O modelo de programação do Orleans é baseado em programação assíncrona. Usando a referência de grão do exemplo anterior, veja a seguinte explicação sobre como executar uma invocação de método de grão:

// Invoking a grain method asynchronously
Task joinGameTask = player.JoinGame(this);

// The await keyword effectively makes the remainder of the
// method execute asynchronously at a later point
// (upon completion of the Task being awaited) without blocking the thread.
await joinGameTask;

// The next line will execute later, after joinGameTask has completed.
players.Add(playerId);

É possível juntar dois ou mais Tasks e essa operação cria um Task que é resolvido quando todos os Tasks constituintes são concluídos. Esse é um padrão útil quando um grão precisa iniciar diversos cálculos e aguardar a conclusão de todos eles antes de continuar. Por exemplo, um grão de front-end que gera uma página da Web feita de muitas partes pode fazer diversas chamadas de back-end, uma para cada parte, e receber um Task para cada resultado. Em seguida, o grão aguardaria a junção de todos esses Tasks. Quando a junção de Task é resolvida, os Tasks individuais são concluídos e todos os dados necessários para formatar a página da Web são recebidos.

Exemplo:

List<Task> tasks = new List<Task>();
Message notification = CreateNewMessage(text);

foreach (ISubscriber subscriber in subscribers)
{
    tasks.Add(subscriber.Notify(notification));
}

// WhenAll joins a collection of tasks, and returns a joined
// Task that will be resolved when all of the individual notification Tasks are resolved.
Task joinedTask = Task.WhenAll(tasks);

await joinedTask;

// Execution of the rest of the method will continue
// asynchronously after joinedTask is resolve.

Métodos virtuais

Uma classe de grão pode substituir os métodos virtuais OnActivateAsync e OnDeactivateAsync, que são invocados pelo runtime do Orleans na ativação e desativação de cada grão da classe. Isso fornece ao código do grão a chance de executar operações adicionais de inicialização e limpeza. Uma exceção lançada por OnActivateAsync causa uma falha no processo de ativação. Enquanto o OnActivateAsync, quando substituído, é sempre chamado como parte do processo de ativação de grão, não há garantia de que OnDeactivateAsync seja chamado em todas as situações, por exemplo, em caso de falha do servidor ou de outro evento anormal. Devido a isso, os aplicativos não devem depender de OnDeactivateAsync para realizar operações críticas, como a persistência de alterações de estado. Eles devem usá-lo somente para operações de melhor esforço.