Minuteries et rappels
Le runtime Orleans propose deux mécanismes, appelés « minuteurs » et « rappels », qui permettent au développeur de spécifier le comportement périodique des grains.
Minuteurs
Les minuteurs servent à créer un comportement de grain périodique qui n’a pas besoin de couvrir plusieurs activations (instanciations du grain). Un minuteur est identique à la classe .NET System.Threading.Timer standard. En outre, les minuteurs sont soumis à des garanties d’exécution à thread unique au sein de l’activation de grain sur laquelle ils opèrent et leurs exécutions sont entrelacées avec d’autres requêtes, comme si le rappel du minuteur était une méthode de grain marquée avec AlwaysInterleaveAttribute.
Chaque activation peut être associée à zéro ou plusieurs minuteurs. Le runtime exécute chaque routine de minuteur dans le contexte d’exécution de l’activation à laquelle il est associé.
Utilisation d’un minuteur
Pour démarrer un minuteur, utilisez la méthode RegisterGrainTimer
qui retourne une référence 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
Pour annuler le minuteur, supprimez-le.
Un minuteur cesse de se déclencher si le grain est désactivé ou en cas d’erreur entraînant le plantage de son silo.
Considérations importantes :
- Quand la collection d’activations est activée, l’exécution d’un rappel de minuteur ne fait pas passer l’activation de l’état « inactif » à l’état « en cours d’utilisation ». Cela signifie que vous ne pouvez pas utiliser un minuteur pour reporter la désactivation d’activations inactives.
- La période passée à
Grain.RegisterGrainTimer
correspond à la durée qui s’écoule entre le moment oùTask
retournée parcallback
est résolue et celui où l’appel suivant decallback
doit se produire. Il est donc impossible que des appels successifs àcallback
se chevauchent. De plus, le temps nécessaire à l’exécution decallback
affecte la fréquence à laquellecallback
est appelé. Ceci constitue une différence importante par rapport à la sémantique de System.Threading.Timer. - Chaque appel de
callback
est remis à une activation lors d’un tour séparé et ne s’exécute jamais en même temps que d’autres tours sur la même activation. Toutefois, les appels decallback
n’étant pas remis en tant que messages, ils ne sont pas soumis à la sémantique d’entrelacement des messages. Cela signifie que les appels decallback
se comportent comme si le grain était réentrant et s’exécutent en même temps que d’autres demandes de grain. Afin d’utiliser la sémantique de planification de requête du grain, vous pouvez appeler une méthode de grain pour effectuer le travail que vous auriez fait danscallback
. Une autre solution consiste à utiliserAsyncLock
ou SemaphoreSlim. Vous trouvez des explications plus détaillées dans le Orleansproblème #2574 sur GitHub.
Rappels
Les rappels sont similaires aux minuteurs, avec toutefois quelques différences notables :
- Les rappels sont persistants et continuent de se déclencher dans presque toutes les situations (y compris les redémarrages partiels ou complets du cluster), sauf annulation explicite.
- Les « définitions » de rappel sont écrites dans le stockage. Ce n’est cependant pas le cas de chaque occurrence spécifique, avec son heure spécifique. Un effet secondaire est que, si le cluster est hors service au moment d’un cycle de rappel spécifique, il n’est pas pris en compte et seul le cycle suivant du rappel se produit.
- Les rappels sont associés à un grain, et non à une activation spécifique.
- Si aucune activation n’est associée à un grain au début du cycle d’un rappel, le grain est créé. Si une activation devient inactive et est désactivée, un rappel associé au même grain réactive le grain lors du cycle suivant.
- La remise de rappel se produit via un message et est soumise à la même sémantique d’entrelacement que toutes les autres méthodes de grain.
- Les rappels ne doivent pas être utilisés pour les minuteurs à fréquence élevée ; leur période doit être mesurée en minutes, en heures ou en jours.
Configuration
Les rappels étant persistants, ils dépendent du stockage pour fonctionner. Vous devez spécifier le support de stockage à utiliser pour que le sous-système de rappel fonctionne. Pour cela, configurez l’un des fournisseurs de rappel avec les méthodes d’extension Use{X}ReminderService
, où X
est le nom du fournisseur. Par exemple : UseAzureTableReminderService.
Configuration d’Azure Table :
// 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 vous souhaitez simplement une implémentation substituable de rappels sans configurer un compte Azure ou une base de données SQL, voici comment obtenir une implémentation du système de rappel à des fins de développement uniquement :
var silo = new HostBuilder()
.UseOrleans(builder =>
{
builder.UseInMemoryReminderService();
})
.Build();
Important
Si vous disposez d’un cluster hétérogène, où les silos gèrent différents types de grains (implémentent des interfaces différentes), chaque silo doit ajouter la configuration pour les rappels, même si le silo lui-même ne gère aucun rappel.
Utilisation des rappels
Un grain qui utilise des rappels doit implémenter la méthode IRemindable.ReceiveReminder.
Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
Console.WriteLine("Thanks for reminding me-- I almost forgot!");
return Task.CompletedTask;
}
Pour démarrer un rappel, utilisez la méthode Grain.RegisterOrUpdateReminder qui retourne un objet IGrainReminder :
protected Task<IGrainReminder> RegisterOrUpdateReminder(
string reminderName,
TimeSpan dueTime,
TimeSpan period)
reminderName
: est une chaîne qui doit identifier de manière unique le rappel dans la portée du grain contextuel.dueTime
: spécifie le temps d’attente avant d’émettre le cycle du premier minuteur.period
: spécifie la période du minuteur.
Étant donné que les rappels survivent à la durée de vie d’une seule activation, ils doivent être explicitement annulés (au lieu d’être supprimés). Pour annuler un rappel, appelez Grain.UnregisterReminder :
protected Task UnregisterReminder(IGrainReminder reminder)
Le reminder
est l’objet handle retourné par Grain.RegisterOrUpdateReminder.
Il n’est pas garanti que les instances de IGrainReminder
soient valides au-delà de la durée de vie d’une activation. Si vous souhaitez identifier un rappel de manière persistante, utilisez une chaîne contenant le nom du rappel.
Si vous avez uniquement le nom du rappel et que vous avez besoin de l’instance correspondante de IGrainReminder
, appelez la méthode Grain.GetReminder :
protected Task<IGrainReminder> GetReminder(string reminderName)
Décider quelle option utiliser
Nous vous recommandons d’utiliser des minuteurs dans les circonstances suivantes :
- S’il n’est pas exigé (ou souhaitable) que le minuteur cesse de fonctionner lorsque l’activation est désactivée ou en cas de défaillance.
- La résolution du minuteur est faible (par exemple, exprimable de manière raisonnable en secondes ou minutes).
- Le rappel du minuteur peut être démarré à partir de Grain.OnActivateAsync() ou quand une méthode de grain est appelée.
Nous vous recommandons d’utiliser des rappels dans les circonstances suivantes :
- Le comportement périodique doit survivre à l’activation et à toute défaillance.
- La fréquence des tâches exécutées est faible (par exemple, exprimable de manière raisonnable en minutes, heures ou jours).
Combiner des minuteurs et des rappels
Vous pouvez envisager d’utiliser une combinaison de rappels et de minuteurs pour atteindre votre objectif. Par exemple, si vous avez besoin d’un minuteur avec une faible résolution qui doit survivre les activations, vous pouvez utiliser un rappel qui s’exécute toutes les cinq minutes et dont l’objectif est de réveiller un grain qui redémarre un minuteur local qui peut avoir été perdu en raison d’une désactivation.
Inscriptions aux grains POCO
Pour inscrire un minuteur ou un rappel avec un grain POCO, vous implémentez l’interface IGrainBase et injectez le ITimerRegistry ou IReminderRegistry dans le constructeur du grain.
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);
}
}
}
Le code précédent :
- Définit un grain POCO qui implémente IGrainBase,
IPingGrain
et IDisposable. - Inscrit un minuteur appelé toutes les 10 secondes et démarre 3 secondes après l’inscription.
- Lorsque
Ping
est appelé, inscrit un rappel qui est appelé toutes les heures et démarre immédiatement après l’inscription. - La méthode
Dispose
annule le rappel s’il est inscrit.