Timer und Erinnerungen
Die Orleans-Laufzeit stellt zwei Mechanismen bereit, die als Zeitgeber und Erinnerungen bezeichnet werden, mit denen der*die Entwickler ein periodisches Verhalten für Grains festlegen kann.
Timer
Zeitgeber werden verwendet, um ein periodisches Verhalten für Grains zu erstellen, das nicht mehrere Aktivierungen (Instanziierungen des Grains) umfassen muss. Ein Zeitgeber ist identisch mit der Standard-System.Threading.Timer-.NET-Klasse. Darüber hinaus gelten für Timer Ausführungsgarantien für Einzelthreads innerhalb ihrer Grainaktivierung, und ihre Ausführung wird mit anderen Anforderungen so kombiniert, als wäre der Timerrückruf eine Grainmethode, die mit AlwaysInterleaveAttribute markiert ist.
Jeder Aktivierung können null oder mehr Zeitgeber zugeordnet sein. Die Laufzeit führt jede Zeitgeberroutine innerhalb des Laufzeitkontexts der Aktivierung aus, der sie zugeordnet ist.
Verwendung des Zeitgebers
Verwenden Sie zum Starten eines Zeitgebers die RegisterGrainTimer
-Methode, die einen IDisposable-Verweis zurückgibt:
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
Um den Zeitgeber abzubrechen, müssen Sie ihn löschen.
Ein Zeitgeber wird nicht mehr ausgelöst, wenn das Grain deaktiviert wird oder wenn ein Fehler auftritt und sein Silo abstürzt.
Wichtige Überlegungen:
- Wenn die Aktivierungssammlung aktiviert ist, ändert die Ausführung eines Zeitgeberrückrufs den Status der Aktivierung von „Leerlauf“ zu „In Verwendung“. Dies bedeutet, dass ein Zeitgeber nicht verwendet werden kann, um die Deaktivierung von Aktivierungen zu verschieben, die ansonsten im Leerlauf sind.
- Der an
Grain.RegisterGrainTimer
übergebene Zeitraum ist die Zeitspanne vom Zeitpunkt der Auflösung der voncallback
zurückgegebenenTask
bis zum Zeitpunkt des nächsten Aufrufs voncallback
. Dies macht es nicht nur unmöglich, dass sich aufeinanderfolgende Aufrufe voncallback
überschneiden, sondern sorgt auch dafür, dass die Dauer der Ausführung voncallback
die Häufigkeit des Aufrufs voncallback
beeinflusst. Dies ist eine wichtige Abweichung von der Semantik der System.Threading.Timer. - Jeder Aufruf von
callback
wird einer Aktivierung in einer separaten Phase übermittelt und läuft nie gleichzeitig mit anderen Phasen derselben Aktivierung ab.callback
-Aufrufe werden jedoch nicht als Nachrichten übermittelt und unterliegen daher keiner Nachrichten-Interleavingsemantik. Das bedeutet, dass Aufrufe voncallback
sich so verhalten, als ob der Grain eintrittsinvariant wäre und gleichzeitig mit anderen Grain-Anforderungen ausgeführt wird. Um die Planungssemantik der Anforderung des Grains zu nutzen, können Sie eine Grainmethode aufrufen, um die Arbeit auszuführen, die Sie incallback
erledigt hätten. Eine weitere Alternative ist die Verwendung einesAsyncLock
oder eines SemaphoreSlim. Eine ausführlichere Erläuterung finden Sie im Orleans-GitHub-Issue #2574.
Reminders
Erinnerungen ähneln Zeitgebern, mit einigen wichtigen Unterschieden:
- Erinnerungen sind beständig und werden in fast allen Situationen ausgelöst (einschließlich teilweiser oder vollständiger Clusterneustarts), sofern sie nicht ausdrücklich abgebrochen werden.
- „Erinnerungsdefinitionen“ werden in den Speicher geschrieben. Dies ist bei bestimmten Vorkommen mit spezifischen Zeiten jedoch nicht der Fall. Dies hat den Nebeneffekt, dass, wenn der Cluster zum Zeitpunkt einer bestimmten Erinnerungsmeldung nicht verfügbar ist, diese verpasst wird und nur die nächste Erinnerungsmeldung ausgeführt wird.
- Erinnerungen sind mit einem Grain verknüpft, nicht mit einer bestimmten Aktivierung.
- Wenn einem Grain zum Zeitpunkt der Erinnerung keine Aktivierung zugeordnet ist, wird das Grain erstellt. Wenn eine Aktivierung im Leerlauf ist und deaktiviert wird, reaktiviert eine Erinnerung, die mit demselben Grain verknüpft ist, das Grain, wenn sie das nächste Mal ausgelöst wird.
- Die Erinnerungsübermittlung erfolgt über eine Nachricht und unterliegt der gleichen Interleavingsemantik wie alle anderen Grainmethoden.
- Erinnerungen sollten nicht für Zeitgeber mit hoher Häufigkeit verwendet werden – ihr Zeitraum sollte in Minuten, Stunden oder Tagen gemessen werden.
Konfiguration
Da Erinnerungen beständig sind, müssen sie gespeichert werden, um zu funktionieren. Sie müssen angeben, welche Speicherunterstützung verwendet werden soll, bevor das Erinnerungssubsystem funktionieren kann. Dazu wird einer der Erinnerungsanbieter über Use{X}ReminderService
-Erweiterungsmethoden konfiguriert, wobei X
der Name des Anbieters ist (z. B. UseAzureTableReminderService).
Azure Table-Konfiguration:
// 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();
Wenn Sie nur eine Platzhalterimplementierung von Erinnerungen benötigen, ohne ein Azure-Konto oder eine SQL-Datenbank einrichten zu müssen, dann erhalten Sie eine reine Entwicklungsimplementierung des Erinnerungssystems:
var silo = new HostBuilder()
.UseOrleans(builder =>
{
builder.UseInMemoryReminderService();
})
.Build();
Wichtig
Wenn Sie über einen heterogenen Cluster verfügen, in dem die Silos unterschiedliche Getreidetypen verarbeiten (unterschiedliche Schnittstellen implementieren), muss jeder Silo die Konfiguration für Erinnerungen hinzufügen, auch wenn der Silo selbst keine Erinnerungen verarbeitet.
Verwendung von Erinnerungen
Ein Grain, das Erinnerungen verwendet, muss die IRemindable.ReceiveReminder-Methode implementieren.
Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
Console.WriteLine("Thanks for reminding me-- I almost forgot!");
return Task.CompletedTask;
}
Verwenden Sie zum Starten einer Erinnerung die Grain.RegisterOrUpdateReminder-Methode, die ein IGrainReminder-Objekt zurückgibt:
protected Task<IGrainReminder> RegisterOrUpdateReminder(
string reminderName,
TimeSpan dueTime,
TimeSpan period)
reminderName
: Eine Zeichenfolge, die die Erinnerung innerhalb des Kontextgrains eindeutig identifizieren muss.dueTime
: Gibt eine Zeitmenge an, die gewartet werden soll, bevor der erste Zeitgeber ausgelöst werden soll.period
: Gibt den Zeitraum des Zeitgebers an.
Da Erinnerungen die Lebensdauer einer einzelnen Aktivierung überdauern, müssen sie explizit abgebrochen werden (und nicht entfernt) werden. Sie brechen eine Erinnerung ab, indem Sie Grain.UnregisterReminder anrufen:
protected Task UnregisterReminder(IGrainReminder reminder)
Das reminder
ist das Handleobjekt, das von Grain.RegisterOrUpdateReminder zurückgegeben wird.
Bei Instanzen von IGrainReminder
ist nicht garantiert, dass sie über die Lebensdauer einer Aktivierung hinaus gültig sind. Wenn Sie eine Erinnerung dauerhaft kennzeichnen möchten, verwenden Sie eine Zeichenfolge, die den Namen der Erinnerung enthält.
Wenn Sie nur den Namen der Erinnerung haben und die entsprechende Instanz von IGrainReminder
benötigen, rufen Sie die Methode Grain.GetReminder auf:
protected Task<IGrainReminder> GetReminder(string reminderName)
Entscheidung über die Verwendung
Es wird empfohlen, Zeitgeber unter folgenden Umständen zu verwenden:
- Wenn es keine Rolle spielt (oder erwünscht ist), dass der Zeitgeber nicht weiter funktioniert, wenn die Aktivierung deaktiviert wird oder Fehler auftreten
- Die Auflösung des Zeitgebers ist klein (z. B. sinnvoll in Sekunden oder Minuten ausdrückbar)
- Der Zeitgeberrückruf kann von Grain.OnActivateAsync() aus gestartet werden oder wenn eine Grainmethode aufgerufen wird.
Es wird empfohlen, Erinnerungen unter folgenden Umständen zu verwenden:
- Wenn das periodische Verhalten die Aktivierung und eventuelle Ausfälle überstehen muss.
- Bei der Durchführung unregelmäßiger Aufgaben (die z. B. im Abstand von Minuten, Stunden oder Tagen ausgeführt werden)
Kombinieren von Zeitgebern und Erinnerungen
Sie können eine Kombination aus Erinnerungen und Zeitgebern verwenden, um Ihr Ziel zu erreichen. Wenn Sie beispielsweise einen Zeitgeber mit einer kleinen Auflösung benötigen, der mehrere Aktivierungen überdauern muss, können Sie eine Erinnerung verwenden, die alle fünf Minuten ausgeführt wird und deren Zweck es ist, einen Grain zu aktivieren, der einen lokalen Zeitgeber neu startet, der aufgrund einer Deaktivierung verloren gegangen sein kann.
POCO-Grainregistrierungen
Um einen Timer oder eine Erinnerung mit einem POCO-Grain zu registrieren, implementieren Sie die IGrainBase-Schnittstelle und fügen das ITimerRegistry oder das IReminderRegistry in den Konstruktor des Grains ein.
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);
}
}
}
Der vorangehende Code:
- Definiert ein POCO-Grain, das IGrainBase,
IPingGrain
und IDisposable implementiert - Registriert einen Timer, der alle zehn Sekunden aufgerufen wird, und startet drei Sekunden nach der Registrierung
- Wenn
Ping
aufgerufen wird, registriert er eine Erinnerung, die jede Stunde aufgerufen wird und unmittelbar nach der Registrierung beginnt - Die
Dispose
-Methode bricht die Erinnerung ab, wenn sie registriert ist