Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The Orleans runtime provides two mechanisms, timers and reminders, that enable you to specify periodic behavior for grains.
Timers
Use timers to create periodic grain behavior that isn't required to span multiple activations (instantiations of the grain). A timer is identical to the standard .NET System.Threading.Timer class. Additionally, timers are subject to single-threaded execution guarantees within the grain activation they operate on.
Each activation can have zero or more timers associated with it. The runtime executes each timer routine within the runtime context of its associated activation.
Timer usage
To start a timer, use the RegisterGrainTimer
method, which returns an IGrainTimer reference:
protected IGrainTimer RegisterGrainTimer<TState>(
Func<TState, CancellationToken, Task> callback, // function invoked when the timer ticks
TState state, // object to pass to callback
GrainTimerCreationOptions options) // timer creation options
To cancel the timer, dispose of it.
A timer stops triggering if the grain deactivates or when a fault occurs and its silo crashes.
Important considerations:
- When activation collection is enabled, executing a timer callback doesn't change the activation's state from idle to in-use. This means you can't use a timer to postpone the deactivation of otherwise idle activations.
- The period passed to
Grain.RegisterGrainTimer
is the amount of time passing from the moment theTask
returned bycallback
resolves to the moment the next invocation ofcallback
should occur. This not only prevents successive calls tocallback
from overlapping but also means the timecallback
takes to complete affects the frequency at whichcallback
is invoked. This is an important deviation from the semantics of System.Threading.Timer. - Each invocation of
callback
is delivered to an activation on a separate turn and never runs concurrently with other turns on the same activation. - Callbacks don't interleave by default. You can enable interleaving by setting
Interleave
totrue
onGrainTimerCreationOptions
. - You can update grain timers using the
Change(TimeSpan, TimeSpan)
method on the returnedIGrainTimer
instance. - Callbacks can keep the grain active, preventing collection if the timer period is relatively short. Enable this by setting
KeepAlive
totrue
onGrainTimerCreationOptions
. - Callbacks can receive a
CancellationToken
that is canceled when the timer is disposed or the grain starts to deactivate. - Callbacks can dispose of the grain timer that fired them.
- Callbacks are subject to grain call filters.
- Callbacks are visible in distributed tracing when distributed tracing is enabled.
- POCO grains (grain classes that don't inherit from
Grain
) can register grain timers using theRegisterGrainTimer
extension method.
Reminders
Reminders are similar to timers, with a few important differences:
- Reminders are persistent and continue to trigger in almost all situations (including partial or full cluster restarts) unless explicitly canceled.
- Reminder "definitions" are written to storage. However, each specific occurrence with its specific time isn't stored. This has the side effect that if the cluster is down when a specific reminder tick is due, it will be missed, and only the next tick of the reminder occurs.
- Reminders are associated with a grain, not any specific activation.
- If a grain has no activation associated with it when a reminder ticks, Orleans creates the grain activation. If an activation becomes idle and is deactivated, a reminder associated with the same grain reactivates the grain when it ticks next.
- Reminder delivery occurs via message and is subject to the same interleaving semantics as all other grain methods.
- You shouldn't use reminders for high-frequency timers; their period should be measured in minutes, hours, or days.
Configuration
Since reminders are persistent, they rely on storage to function. You must specify which storage backing to use before the reminder subsystem can function. Do this by configuring one of the reminder providers via Use{X}ReminderService
extension methods, where X
is the name of the provider (for example, UseAzureTableReminderService).
Azure Table configuration:
// 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();
If you just want a placeholder implementation of reminders to work without needing to set up an Azure account or SQL database, this provides a development-only implementation of the reminder system:
var silo = new HostBuilder()
.UseOrleans(builder =>
{
builder.UseInMemoryReminderService();
})
.Build();
Important
If you have a heterogenous cluster, where the silos handle different grain types (implement different interfaces), every silo must add the configuration for Reminders, even if the silo itself doesn't handle any reminders.
Reminder usage
A grain using reminders must implement the IRemindable.ReceiveReminder method.
Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
Console.WriteLine("Thanks for reminding me-- I almost forgot!");
return Task.CompletedTask;
}
To start a reminder, use the Grain.RegisterOrUpdateReminder method, which returns an IGrainReminder object:
protected Task<IGrainReminder> RegisterOrUpdateReminder(
string reminderName,
TimeSpan dueTime,
TimeSpan period)
reminderName
: is a string that must uniquely identify the reminder within the scope of the contextual grain.dueTime
: specifies a quantity of time to wait before issuing the first-timer tick.period
: specifies the period of the timer.
Since reminders survive the lifetime of any single activation, you must explicitly cancel them (as opposed to disposing of them). Cancel a reminder by calling Grain.UnregisterReminder:
protected Task UnregisterReminder(IGrainReminder reminder)
The reminder
is the handle object returned by Grain.RegisterOrUpdateReminder.
Instances of IGrainReminder
aren't guaranteed to be valid beyond the lifespan of an activation. If you wish to identify a reminder persistently, use a string containing the reminder's name.
If you only have the reminder's name and need the corresponding IGrainReminder
instance, call the Grain.GetReminder method:
protected Task<IGrainReminder> GetReminder(string reminderName)
Decide which to use
We recommend using timers in the following circumstances:
- If it doesn't matter (or is desirable) that the timer stops functioning when the activation deactivates or failures occur.
- The timer's resolution is small (e.g., reasonably expressible in seconds or minutes).
- You can start the timer callback from Grain.OnActivateAsync() or when a grain method is invoked.
We recommend using reminders in the following circumstances:
- When the periodic behavior needs to survive activation and any failures.
- Performing infrequent tasks (e.g., reasonably expressible in minutes, hours, or days).
Combine timers and reminders
You might consider using a combination of reminders and timers to accomplish your goal. For example, if you need a timer with a small resolution that must survive across activations, you can use a reminder running every five minutes. Its purpose would be to wake up a grain that restarts a local timer possibly lost due to deactivation.
POCO grain registrations
To register a timer or reminder with a POCO grain, implement the IGrainBase interface and inject ITimerRegistry or IReminderRegistry into the grain's constructor.
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);
}
}
}
The preceding code does the following:
- Defines a POCO grain implementing IGrainBase,
IPingGrain
, and IDisposable. - Registers a timer invoked every 10 seconds, starting 3 seconds after registration.
- When
Ping
is called, registers a reminder invoked every hour, starting immediately after registration. - The
Dispose
method cancels the reminder if it's registered.