Recordatorios y temporizadores

El runtime de Orleans proporciona dos mecanismos, denominados temporizadores y recordatorios, que permiten al desarrollador especificar un comportamiento periódico para granos.

Temporizadores

Los temporizadores se usan para crear un comportamiento de grano periódico que no es necesario para abarcar varias activaciones (instancias del grano). Un temporizador es idéntico a la clase estándar .NET de System.Threading.Timer. Además, los temporizadores están sujetos a garantías de ejecución de un solo subproceso dentro de la activación de grano en la que operan, y sus ejecuciones están intercaladas con otras solicitudes, como si la devolución de llamada del temporizador fuera un método de grano marcado con AlwaysInterleaveAttribute.

Cada activación puede tener cero o más temporizadores asociados. El tiempo de ejecución ejecuta cada rutina del temporizador dentro del contexto en tiempo de ejecución de la activación a la que está asociado.

Uso del temporizador

Para iniciar un temporizador, use el método Grain.RegisterTimer, que devuelve una referencia IDisposable:

protected IDisposable RegisterTimer(
    Func<object, Task> asyncCallback, // function invoked when the timer ticks
    object state,                     // object to pass to asyncCallback
    TimeSpan dueTime,                 // time to wait before the first timer tick
    TimeSpan period)                  // the period of the timer

Para cancelar el temporizador, se debe eliminarlo.

Un temporizador deja de desencadenarse si se desactiva el grano o cuando se produce un error y su silo se bloquea.

Consideraciones importantes:

  • Cuando se habilita la recopilación de activación, la ejecución de una devolución de llamada del temporizador no cambia el estado de la activación de inactiva a operativa. Esto significa que no se puede usar un temporizador para posponer la desactivación de activaciones que, en otros casos, estén inactivas.
  • El período pasado a Grain.RegisterTimer es la cantidad de tiempo que pasa desde el momento en que se resuelve la tarea devuelta por asyncCallback hasta el momento en que se debe producir la siguiente invocación de asyncCallback. Esto no solo hace imposible que las llamadas sucesivas a asyncCallback se superpongan, sino que también hace que el período de tiempo asyncCallback que se tarda en completarse afecte a la frecuencia a la que asyncCallback se invoca. Se trata de una desviación importante de la semántica de System.Threading.Timer.
  • Cada invocación de asyncCallback se entrega a una activación en un turno independiente y nunca se ejecuta simultáneamente con otros activando la misma activación. Sin embargo, las invocaciones de asyncCallback no se entregan como mensajes y, por tanto, no están sujetas a la semántica de intercalación de mensajes. Esto significa que las invocaciones de asyncCallback se comportan como si el grano se vuelve a entrar y se ejecuta simultáneamente con otras solicitudes de grano. Para usar la semántica de programación de solicitudes del grano, puede llamar a un método de grano para realizar el trabajo que habría realizado en asyncCallback. Otra alternativa es usar un AsyncLock o un SemaphoreSlim. Hay disponible una explicación más detallada en Problema de GitHub de n.º 2574Orleans.

Recordatorios

Los avisos son similares a los temporizadores, con algunas diferencias importantes:

  • Los recordatorios son persistentes y continúan desencadenando en casi todas las situaciones (incluidos los reinicios parciales o completos del clúster), a menos que se cancele explícitamente.
  • Los avisos "definiciones" se escriben en el almacenamiento. Sin embargo, cada aparición específica, con su hora específica, no es. Esto tiene el efecto secundario de que si el clúster está inactivo en el momento de un tic de recordatorio específico, se perderá y solo se producirá el siguiente tic del aviso.
  • Los recordatorios están asociados a un grano, no a ninguna activación específica.
  • Si un grano no tiene ninguna activación asociada cuando se marca un aviso, se crea el grano. Si una activación se vuelve inactiva y se desactiva, un recordatorio asociado al mismo grano reactiva el grano cuando se marca a continuación.
  • La entrega del recordatorio se produce a través del mensaje y está sujeta a la misma semántica intercalada que todos los demás métodos de grano.
  • Los recordatorios no deben usarse en temporizadores de alta frecuencia; su período debe medirse en minutos, horas o días.

Configuración

Los avisos, siendo persistentes, dependen del almacenamiento para funcionar. Debe especificar la copia de seguridad de almacenamiento que se usará antes de las funciones del subsistema de recordatorio. Para ello, configure uno de los proveedores de recordatorios a través de métodos de extensión Use{X}ReminderService, donde X es el nombre del proveedor, por ejemplo, UseAzureTableReminderService.

Configuración de tabla de 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();

Si solo desea que una implementación de marcador de posición de recordatorios funcione sin necesidad de configurar una cuenta de Azure o una base de datos SQL, esto le proporciona una implementación solo de desarrollo del sistema de recordatorio:

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

Uso de recordatorios

Un grano que use recordatorios debe implementar el 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 un recordatorio, use el método Grain.RegisterOrUpdateReminder, que devuelve un objeto IGrainReminder:

protected Task<IGrainReminder> RegisterOrUpdateReminder(
    string reminderName,
    TimeSpan dueTime,
    TimeSpan period)
  • reminderName: es una cadena que debe identificar de forma única el aviso dentro del ámbito del grano contextual.
  • dueTime: especifica una cantidad de tiempo de espera antes de emitir el tic del primer temporizador.
  • period: especifica el período del temporizador.

Dado que los recordatorios sobreviven a la duración de cualquier activación única, hay que cancelarlos explícitamente (en vez de eliminarlos). Para cancelar un aviso se puede llamar a Grain.UnregisterReminder:

protected Task UnregisterReminder(IGrainReminder reminder)

reminder es el objeto manipulador devuelto por Grain.RegisterOrUpdateReminder.

No se garantiza que las instancias de IGrainReminder sean válidas más allá de la duración de una activación. Si desea identificar un recordatorio de forma que persista, use una cadena que contenga el nombre del recordatorio.

Si solo tiene el nombre del aviso y necesita la instancia correspondiente de IGrainReminder, llame al método Grain.GetReminder:

protected Task<IGrainReminder> GetReminder(string reminderName)

Decida cuál utilizar

Se recomienda usar temporizadores en las siguientes circunstancias:

  • Si no importa (o es deseable) que el temporizador deje de funcionar cuando se desactive la activación o se produzcan errores.
  • La resolución del temporizador es pequeña (por ejemplo, se puede expresar razonablemente en segundos o minutos).
  • La devolución de llamada del temporizador se puede iniciar desde Grain.OnActivateAsync() o cuando se invoca un método específico.

Se recomienda usar recordatorios en las siguientes circunstancias:

  • Cuando el comportamiento periódico necesita sobrevivir a la activación y a los errores.
  • Cuando se realizan tareas poco frecuentes (por ejemplo, las que se pueden expresar razonablemente en minutos, horas o días).

Combinar recordatorios y temporizadores

Es posible que considere la posibilidad de usar una combinación de recordatorios y temporizadores para lograr su objetivo. Por ejemplo, si necesita un temporizador con una resolución pequeña que necesite sobrevivir entre activaciones, puede usar un recordatorio que se ejecute cada cinco minutos, cuyo propósito es reactivar un grano que reinicie un temporizador local que pueda haberse perdido debido a la desactivación.

Registros de grano POCO

Para registrar un temporizador o recordatorio con un grano POCO, implemente la interfaz IGrainBase e inserte ITimerRegistry o IReminderRegistry en el constructor del grano.

using Orleans.Runtime;
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.RegisterTimer(
            grainContext,
            asyncCallback: static async state =>
            {
                // Omitted for brevity...
                // Use state

                await Task.CompletedTask;
            },
            state: this,
            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);
        }
    }
}

El código anterior:

  • Define un grano POCO que implementa IGrainBase, IPingGrainy IDisposable.
  • Registra un temporizador que se invoca cada 10 segundos y comienza 3 segundos después del registro.
  • Cuando se llama a Ping, registra un recordatorio que se invoca cada hora y comienza inmediatamente después del registro.
  • El método Dispose cancela el aviso si está registrado.