Condividi tramite


Timer e promemoria

Il runtime Orleans fornisce due meccanismi, denominati timer e promemoria, che consentono allo sviluppatore di specificare il comportamento periodico per le granularità.

Timer

I timer vengono usati per creare un comportamento di granularità periodico che non è necessario per estendere più attivazioni (istanze della granularità). Un timer è identico alla classe System.Threading.Timer .NET standard. Inoltre, i timer sono soggetti a garanzie di esecuzione a thread singolo all'interno dell'attivazione granulare su cui operano e le relative esecuzioni vengono interfogliate con altre richieste, come se il callback del timer fosse un metodo di granularità contrassegnato con AlwaysInterleaveAttribute.

A ogni attivazione possono essere associati zero o più timer. Il runtime esegue ogni routine timer all'interno del contesto di runtime dell'attivazione a cui è associata.

Utilizzo del timer

Per avviare un timer, utilizzare il metodo RegisterGrainTimer, che restituisce un riferimento 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

Per annullare il timer, eliminarlo.

Un timer smette di attivare se la granularità viene disattivata o quando si verifica un errore e il relativo silo si arresta in modo anomalo.

Considerazioni importanti:

  • Quando la raccolta di attivazione è abilitata, l'esecuzione di un callback timer non modifica lo stato dell'attivazione da inattivo a in uso. Ciò significa che un timer non può essere usato per posticipare la disattivazione di attivazioni altrimenti inattive.
  • Il periodo passato a Grain.RegisterGrainTimer è la quantità di tempo che passa dal momento in cui Task restituita da callback viene risolta al momento in cui deve verificarsi la chiamata successiva di callback. Ciò non solo rende impossibile che le chiamate successive a callback si sovrappongano, ma fa sì che rendono anche in modo che il tempo di completamento di callback influisca sulla frequenza in cui callback viene richiamato. Si tratta di una deviazione importante dalla semantica di System.Threading.Timer.
  • Ogni chiamata di callback viene recapitata a un'attivazione su un turno separato e non viene mai eseguita simultaneamente con altre attivazioni della stessa attivazione. Tuttavia, le chiamate callback non vengono recapitate come messaggi e pertanto non sono soggette alla semantica di interleaving dei messaggi. Ciò significa che le chiamate di callback si comportano come se la granularità fosse rientrante e venisse eseguita simultaneamente con altre richieste di granularità. Per usare la semantica di pianificazione delle richieste di granularità, è possibile chiamare un metodo granulare per eseguire il lavoro svolto all'interno di callback. Un'altra alternativa consiste nell'usare un oggetto AsyncLock o SemaphoreSlim. Una spiegazione più dettagliata è disponibile nel Orleansproblema n. 2574 di GitHub.

Promemoria

I promemoria sono simili ai timer, con alcune differenze importanti:

  • I promemoria sono persistenti e continuano a essere attivati in quasi tutte le situazioni (inclusi i riavvii parziali o completi del cluster) a meno che non vengano annullati in modo esplicito.
  • I promemoria "definizioni" vengono scritti nella risorsa di archiviazione. Invece, per ogni occorrenza specifica, con il suo tempo specifico, non avviene lo stesso. Questo ha l'effetto collaterale che se il cluster è inattivo al momento di un tick di promemoria specifico, verrà perso e si verifica solo il segno di spunta successivo del promemoria.
  • I promemoria sono associati a una granularità, non a un'attivazione specifica.
  • Se a una granularità non è associata alcuna attivazione quando viene eseguito un promemoria, viene creata la granularità. Se un'attivazione diventa inattiva e viene disattivata, un promemoria associato alla stessa granularità riattiva la granularità quando viene selezionata successivamente.
  • Il recapito dei promemoria avviene tramite messaggio ed è soggetto alla stessa semantica di interleaving di tutti gli altri metodi granulari.
  • I promemoria non devono essere usati per timer ad alta frequenza. Il periodo deve essere misurato in minuti, ore o giorni.

Impostazione

I promemoria, essendo persistenti, si basano sull'archiviazione per funzionare. È necessario specificare quale risorsa di archiviazione usare prima delle funzioni del sottosistema di promemoria. Questa operazione viene eseguita configurando uno dei provider di promemoria tramite metodi di estensione Use{X}ReminderService, dove X è il nome del provider, ad esempio UseAzureTableReminderService.

Configurazione tabella di 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 si vuole solo che un'implementazione segnaposto dei promemoria funzioni senza dover configurare un account di Azure o un database SQL, si ottiene un'implementazione di solo sviluppo del sistema di promemoria:

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

Importante

Se si dispone di un cluster eterogeneo, in cui i silo gestiscono tipi di grain diversi (implementano interfacce diverse), ogni silo deve aggiungere la configurazione per i promemoria, anche se il silo stesso non gestisce alcun promemoria.

Utilizzo dei promemoria

Una granularità che utilizza i promemoria deve implementare il metodo IRemindable.ReceiveReminder.

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

Per avviare un promemoria, utilizzare il metodo Grain.RegisterOrUpdateReminder, che restituisce un oggetto IGrainReminder:

protected Task<IGrainReminder> RegisterOrUpdateReminder(
    string reminderName,
    TimeSpan dueTime,
    TimeSpan period)
  • reminderName: è una stringa che deve identificare in modo univoco il promemoria nell'ambito della granularità contestuale.
  • dueTime: specifica una quantità di tempo di attesa prima di emettere il tick del primo timer.
  • period: specifica il periodo del timer.

Poiché i promemoria sopravvivono alla durata di qualsiasi singola attivazione, devono essere annullati in modo esplicito (anziché essere eliminati). Annullare un promemoria chiamando Grain.UnregisterReminder:

protected Task UnregisterReminder(IGrainReminder reminder)

reminder è l'oggetto handle restituito da Grain.RegisterOrUpdateReminder.

Le istanze di IGrainReminder non sono sicuramente valide oltre la durata di un'attivazione. Se si vuole identificare un promemoria in modo permanente, usare una stringa contenente il nome del promemoria.

Se si dispone solo del nome del promemoria e si necessita dell'istanza corrispondente di IGrainReminder, chiamare il metodo Grain.GetReminder:

protected Task<IGrainReminder> GetReminder(string reminderName)

Decidere quale usare

È consigliabile usare dei timer nelle circostanze seguenti:

  • Se non importa (o è auspicabile) che il timer smetta di funzionare quando l'attivazione viene disattivata o si verificano errori.
  • La risoluzione del timer è piccola (ad esempio, ragionevolmente esprimibili in secondi o minuti).
  • Il callback del timer può essere avviato da Grain.OnActivateAsync() o quando viene richiamato un metodo granulare.

È consigliabile usare i promemoria nelle circostanze seguenti:

  • Quando il comportamento periodico deve sopravvivere all'attivazione e a eventuali errori.
  • Esecuzione di attività poco frequenti (ad esempio, ragionevolmente esprimibili in minuti, ore o giorni).

Combinare timer e promemoria

È possibile prendere in considerazione l'uso di una combinazione di promemoria e timer per raggiungere l'obiettivo. Ad esempio, se è necessario un timer con una piccola risoluzione che deve sopravvivere tra le attivazioni, è possibile usare un promemoria che viene eseguito ogni cinque minuti, il cui scopo è quello di riattivare una granularità che riavvia un timer locale che potrebbe essere stato perso a causa della disattivazione.

Registrazioni di granularità POCO

Per registrare un timer o un promemoria con una granularità POCO, implementare l'interfaccia IGrainBase e inserire ITimerRegistry o IReminderRegistry nel costruttore della granularità.

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);
        }
    }
}

Il codice precedente:

  • Definisce una granularità POCO che implementa IGrainBase, IPingGrain e IDisposable.
  • Registra un timer richiamato ogni 10 secondi e inizia 3 secondi dopo la registrazione.
  • Quando Ping viene chiamato, registra un promemoria richiamato ogni ora e inizia immediatamente dopo la registrazione.
  • Il metodo Dispose annulla il promemoria se è registrato.