Compartilhar via


Lembretes e temporizadores

O runtime do Orleans fornece dois mecanismos, chamados de temporizadores e lembretes, que permitem que o desenvolvedor especifique o comportamento periódico para a granularidade.

Temporizadores

Os temporizadores são usados para criar um comportamento periódico da granularidade que não é necessário para abranger várias ativações (instanciações da granularidade). Um temporizador é idêntico à classe .NET System.Threading.Timer padrão. Além disso, os temporizadores estão sujeitos a garantias de execução de thread único dentro da ativação de granularidade em que ele opera e sua execução será intercalada com outras solicitações, como se o retorno de chamada do temporizador fosse um método de granularidade marcado com AlwaysInterleaveAttribute.

Cada ativação pode ter nenhum ou mais temporizadores associados. O runtime executa cada rotina de temporizador no contexto de runtime da ativação à qual está associado.

Uso do temporizador

Para iniciar um temporizador, use o método RegisterGrainTimer, que retorna uma referência IDisposable:

protected IDisposable RegisterGrainTimer(
    Func<object, Task> callback,        // function invoked when the timer ticks
    object state,                       // object to pass to callback
    GrainTimerCreationOptions options)  // timer creation options

Para cancelar o temporizador, descarte-o.

Um temporizador deixará de ser disparado, se a granularidade for desativada ou quando ocorrer uma falha e o silo falhar.

Considerações importantes:

  • Quando a coleta de ativação está habilitada, a execução de um retorno de chamada do temporizador não altera o estado da ativação de ocioso para em uso. Isso significa que um temporizador não pode ser usado para adiar a desativação de ativações ociosas.
  • O período passado para Grain.RegisterGrainTimer é a quantidade de tempo que passa desde o momento em que o Task retornado por callback é resolvido até o momento em que a próxima invocação de callback deve ocorrer. Isso não só impossibilita a sobreposição de chamadas sucessivas para callback, como também faz com que o tempo que callback leva para ser concluído afete a frequência em que callback é invocado. Esse é um desvio importante da semântica de System.Threading.Timer.
  • Cada invocação de callback é entregue a uma ativação em uma rodada diferente e nunca será executada simultaneamente com outras rodadas na mesma ativação. No entanto, as invocações de callback não são entregues como mensagens e, portanto, não estão sujeitas à semântica de intercalação de mensagens. Isso significa que as invocações de callback se comportarão como se a granulação fosse reentrante e serão executadas simultaneamente com outras solicitações de detalhamento. Para usar a semântica de agendamento solicitado da granularidade, você poderá chamar um método granular para executar o trabalho que seria feito em callback. Outra alternativa é usar um AsyncLock ou umSemaphoreSlim. Uma explicação mais detalhada está disponível no Problema do GitHub Orleans #2574.

Lembretes

Os lembretes são semelhantes aos temporizadores, com algumas diferenças importantes:

  • Os lembretes são persistentes e continuarão sendo disparados em quase todas as situações (incluindo reinicializações parciais ou completas do cluster), a menos que sejam cancelados explicitamente.
  • As "definições" de lembrete são gravadas no armazenamento. No entanto, cada ocorrência específica, com o tempo específico, não é. Isso tem o efeito colateral de que, se o cluster estiver inoperante no momento de um tique de lembrete específico, ele será perdido e apenas o próximo tique de lembrete ocorrerá.
  • Os lembretes são associados à granularidade, não a qualquer ativação específica.
  • Se a granularidade não tiver uma ativação associada no momento do tique de lembrete, a granularidade será criada. Se uma ativação ficar ociosa e for desativada, um lembrete associado à mesma granularidade reativará a granularidade no momento do próximo tique.
  • A entrega de lembretes ocorre por meio de mensagem e está sujeita à mesma semântica de intercalação que todos os outros métodos de grãos.
  • Os lembretes não devem ser usados para temporizadores de alta frequência. O período dos lembretes deve ser medido em minutos, horas ou dias.

Configuração

Como são persistentes, os lembretes dependem do armazenamento para funcionar. Você deve especificar qual suporte de armazenamento deve ser usado para que o subsistema de lembretes funcione. Você pode fazer isso configurando um dos provedores de lembrete usando os métodos de extensão Use{X}ReminderService, em que X é o nome do provedor, por exemplo, UseAzureTableReminderService.

Configuração da Tabela do Azure:

// TODO replace with your connection string
const string connectionString = "YOUR_CONNECTION_STRING_HERE";
var silo = new HostBuilder()
    .UseOrleans(builder =>
    {
        builder.UseAzureTableReminderService(connectionString)
    })
    .Build();

SQL:

const string connectionString = "YOUR_CONNECTION_STRING_HERE";
const string invariant = "YOUR_INVARIANT";
var silo = new HostBuilder()
    .UseOrleans(builder =>
    {
        builder.UseAdoNetReminderService(options =>
        {
            options.ConnectionString = connectionString; // Redacted
            options.Invariant = invariant;
        });
    })
    .Build();

Se você deseja apenas que uma implementação de lembretes de espaço reservado funcione, sem precisar configurar uma conta do Azure ou um banco de dados SQL, isso fornecerá uma implementação somente de desenvolvimento do sistema de lembretes:

var silo = new HostBuilder()
    .UseOrleans(builder =>
    {
        builder.UseInMemoryReminderService();
    })
    .Build();

Importante

Se você tiver um cluster heterogêneo, em que os silos manipulam diferentes tipos de grãos (implementam diferentes interfaces), cada silo deverá adicionar a configuração para Lembretes, mesmo que o silo em si não manipule nenhum lembrete.

Uso de lembrete

A granularidade que usa lembretes deve implementar o método IRemindable.ReceiveReminder.

Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
    Console.WriteLine("Thanks for reminding me-- I almost forgot!");
    return Task.CompletedTask;
}

Para iniciar um lembrete, use o método Grain.RegisterOrUpdateReminder, que retorna um objeto IGrainReminder:

protected Task<IGrainReminder> RegisterOrUpdateReminder(
    string reminderName,
    TimeSpan dueTime,
    TimeSpan period)
  • reminderName: é uma cadeia de caracteres que deve identificar exclusivamente o lembrete no escopo da granularidade contextual.
  • dueTime: especifica o tempo de espera para emitir o tique do primeiro temporizador.
  • period: especifica o período do temporizador.

Como os lembretes sobrevivem pelo tempo de vida de qualquer ativação única, eles devem ser cancelados explicitamente (em vez de serem descartados). Cancele um lembrete chamando Grain.UnregisterReminder:

protected Task UnregisterReminder(IGrainReminder reminder)

O objeto reminder é o objeto identificador retornado por Grain.RegisterOrUpdateReminder.

Não há garantia de que as instâncias de IGrainReminder sejam válidas após o tempo de vida de uma ativação. Se você quiser identificar um lembrete de uma maneira que persista, use uma cadeia de caracteres que contenha o nome do lembrete.

Se você tiver apenas o nome do lembrete e precisar da instância correspondente de IGrainReminder, chame o método Grain.GetReminder:

protected Task<IGrainReminder> GetReminder(string reminderName)

Decida qual deve ser usado

Recomendamos que você use os temporizadores nas seguintes circunstâncias:

  • Se for indiferente (ou desejável) que o temporizador deixe de funcionar quando a ativação for desativada ou ocorrerem falhas.
  • A resolução do temporizador é pequena (por exemplo, bastante significativa em segundos ou minutos).
  • O retorno de chamada do temporizador pode ser iniciado em Grain.OnActivateAsync() ou quando um método de granularidade é invocado.

Recomendamos que você use os lembretes nas seguintes circunstâncias:

  • Quando o comportamento periódico precisar sobreviver à ativação e a falhas.
  • Ao executar tarefas esporádicas (por exemplo, bastante significativas em minutos, horas ou dias).

Combinar temporizadores e lembretes

Você pode usar uma combinação de lembretes e temporizadores para atingir sua meta. Por exemplo, se você precisar de um temporizador com uma pequena resolução que precise sobreviver em todas as ativações, você pode usar um lembrete executado a cada cinco minutos, cujo objetivo é ativar a granularidade que reinicia um temporizador local que pode ter sido perdido devido à desativação.

Registros de grãos POCO

Para registrar um temporizador ou lembrete com um grão POCO, implemente a interface IGrainBase e injete o ITimerRegistry ou o IReminderRegistry no construtor do grão.

using Orleans.Timers;

namespace Timers;

public sealed class PingGrain : IGrainBase, IPingGrain, IDisposable
{
    private const string ReminderName = "ExampleReminder";

    private readonly IReminderRegistry _reminderRegistry;

    private IGrainReminder? _reminder;

    public  IGrainContext GrainContext { get; }

    public PingGrain(
        ITimerRegistry timerRegistry,
        IReminderRegistry reminderRegistry,
        IGrainContext grainContext)
    {
        // Register timer
        timerRegistry.RegisterGrainTimer(
            grainContext,
            callback: static async (state, cancellationToken) =>
            {
                // Omitted for brevity...
                // Use state

                await Task.CompletedTask;
            },
            state: this,
            options: new GrainTimerCreationOptions
            {
                DueTime = TimeSpan.FromSeconds(3),
                Period = TimeSpan.FromSeconds(10)
            });

        _reminderRegistry = reminderRegistry;

        GrainContext = grainContext;
    }

    public async Task Ping()
    {
        _reminder = await _reminderRegistry.RegisterOrUpdateReminder(
            callingGrainId: GrainContext.GrainId,
            reminderName: ReminderName,
            dueTime: TimeSpan.Zero,
            period: TimeSpan.FromHours(1));
    }

    void IDisposable.Dispose()
    {
        if (_reminder is not null)
        {
            _reminderRegistry.UnregisterReminder(
                GrainContext.GrainId, _reminder);
        }
    }
}

O código anterior:

  • Define um grão POCO que implementa IGrainBase, IPingGrain e IDisposable.
  • Registra um temporizador que é invocado a cada 10 segundos e inicia 3 segundos após o registro.
  • Quando Ping é chamado, registra um lembrete que é invocado a cada hora e começa imediatamente após o registro.
  • O método Dispose cancelará o lembrete se ele estiver registrado.