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
callback: The callback function that receives the state and a CancellationToken that is canceled when the timer is disposed or the grain deactivates.state: The state object passed to the callback.options: A GrainTimerCreationOptions instance that configures timer behavior.
To cancel the timer, dispose of it.
A timer stops triggering if the grain deactivates or when a fault occurs and its silo crashes.
GrainTimerCreationOptions
The GrainTimerCreationOptions structure provides the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
| DueTime | TimeSpan | Required | The amount of time to delay before invoking the callback. Use TimeSpan.Zero to start immediately, or Timeout.InfiniteTimeSpan to prevent the timer from starting. |
| Period | TimeSpan | Required | The time interval between invocations of the callback. Use Timeout.InfiniteTimeSpan to disable periodic signaling (one-shot timer). |
| Interleave | bool |
false |
When true, timer callbacks can interleave with other timers and grain calls. When false, callbacks are treated like grain calls and don't interleave (unless the grain is reentrant). |
| KeepAlive | bool |
false |
When true, timer callbacks extend the grain activation's lifetime, preventing idle collection. When false, timer callbacks don't prevent grain collection. |
Example: Creating a grain timer
public class MyGrain : Grain, IMyGrain
{
private IGrainTimer? _timer;
public override Task OnActivateAsync(CancellationToken cancellationToken)
{
// Create a timer that fires every 10 seconds, starting 5 seconds after activation
// RegisterGrainTimer is an extension method on IGrainBase (which Grain implements)
_timer = this.RegisterGrainTimer(
static (state, ct) => state.DoWorkAsync(ct),
this,
new GrainTimerCreationOptions
{
DueTime = TimeSpan.FromSeconds(5),
Period = TimeSpan.FromSeconds(10),
KeepAlive = true // Prevent grain collection while timer is active
});
return Task.CompletedTask;
}
private Task DoWorkAsync(CancellationToken cancellationToken)
{
// Timer callback work
return Task.CompletedTask;
}
}
Important considerations:
- When activation collection is enabled, executing a timer callback doesn't change the activation's state from idle to in-use by default. Set KeepAlive to
trueif you want timer callbacks to prevent deactivation. - The period passed to RegisterGrainTimer is the amount of time passing from the moment the Task returned by
callbackresolves to the moment the next invocation ofcallbackshould occur. This not only prevents successive calls tocallbackfrom overlapping but also means the timecallbacktakes to complete affects the frequency at whichcallbackis invoked. This is an important deviation from the semantics of System.Threading.Timer. - Each invocation of
callbackis 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 to
true. - You can update grain timers using the Change method on the returned IGrainTimer instance.
- Callbacks can keep the grain active, preventing collection if the timer period is relatively short. Enable this by setting KeepAlive to
true. - 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 the RegisterGrainTimer extension method.
Important
The RegisterTimer API is obsolete starting in Orleans 8.2. If you're upgrading to Orleans 8.0 or later, migrate to the new RegisterGrainTimer API. See the migration section for details.
To start a timer, use the Grain.RegisterTimer method, which returns an IDisposable reference:
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
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.RegisterTimeris the amount of time passing from the moment the Task returned byasyncCallbackresolves to the moment the next invocation ofasyncCallbackshould occur. This not only prevents successive calls toasyncCallbackfrom overlapping but also means the timeasyncCallbacktakes to complete affects the frequency at whichasyncCallbackis invoked. This is an important deviation from the semantics of System.Threading.Timer. - Each invocation of
asyncCallbackis delivered to an activation on a separate turn and never runs concurrently with other turns on the same activation. - Timer callbacks can interleave with other grain calls and timers.
Migrate from RegisterTimer to RegisterGrainTimer
If you're upgrading from Orleans 7.x to Orleans 8.x or later, you should migrate from the obsolete RegisterTimer API to the new RegisterGrainTimer API.
Key differences
| Aspect | RegisterTimer (Orleans 7.x) |
RegisterGrainTimer (Orleans 8.x+) |
|---|---|---|
| Interleaving | Callbacks interleave by default | Callbacks do not interleave by default |
| Return type | IDisposable | IGrainTimer |
| Callback signature | Func<object, Task> |
Func<TState, CancellationToken, Task> (receives CancellationToken) |
| State type | object (untyped) |
TState (strongly typed) |
| CancellationToken | Not supported | Supported via CancellationToken, canceled on disposal or deactivation |
| Updatable | No | Yes, via Change method |
| KeepAlive option | Not supported | Supported via KeepAlive, prevents grain collection |
| Call filters | Not subject to filters | Subject to grain call filters |
| Distributed tracing | Not visible | Visible in distributed tracing |
Migration example
Before (Orleans 7.x):
public class MyGrain : Grain, IMyGrain
{
private IDisposable? _timer;
public override Task OnActivateAsync()
{
_timer = RegisterTimer(
DoWorkAsync,
null,
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10));
return base.OnActivateAsync();
}
private Task DoWorkAsync(object state)
{
// Timer work - this interleaves with other calls
return Task.CompletedTask;
}
}
After (Orleans 8.x+):
public class MyGrainAfter : Grain, IMyGrain
{
private IGrainTimer? _timer;
public override Task OnActivateAsync(CancellationToken cancellationToken)
{
// Use this.RegisterGrainTimer() - an extension method on IGrainBase
_timer = this.RegisterGrainTimer(
static (state, ct) => state.DoWorkAsync(ct),
this,
new GrainTimerCreationOptions
{
DueTime = TimeSpan.FromSeconds(5),
Period = TimeSpan.FromSeconds(10),
Interleave = true // Set to true to match old behavior
});
return Task.CompletedTask;
}
private Task DoWorkAsync(CancellationToken cancellationToken)
{
// Timer work - check cancellationToken for graceful shutdown
return Task.CompletedTask;
}
}
Warning
The default interleaving behavior changed in Orleans 8.2. The old RegisterTimer API allowed timer callbacks to interleave with other grain calls by default. The new RegisterGrainTimer API does not interleave by default. If your grain logic depends on interleaving behavior, set Interleave to true to preserve the old behavior.
This section applies to Orleans 8.0 and later. Select the Orleans 8.0, Orleans 9.0, or Orleans 10.0 pivot to view migration guidance.
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:
public static async Task ConfigureAzureTableAsync(string[] args)
{
// TODO replace with your connection string
const string connectionString = "YOUR_CONNECTION_STRING_HERE";
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleans(siloBuilder =>
{
siloBuilder.UseAzureTableReminderService(connectionString);
});
using var host = builder.Build();
await host.RunAsync();
}
SQL:
public static async Task ConfigureAdoNetAsync(string[] args)
{
const string connectionString = "YOUR_CONNECTION_STRING_HERE";
const string invariant = "YOUR_INVARIANT";
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleans(siloBuilder =>
{
siloBuilder.UseAdoNetReminderService(options =>
{
options.ConnectionString = connectionString; // Redacted
options.Invariant = invariant;
});
});
using var host = builder.Build();
await host.RunAsync();
}
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:
public static async Task ConfigureInMemoryAsync(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleans(siloBuilder =>
{
siloBuilder.UseInMemoryReminderService();
});
using var host = builder.Build();
await host.RunAsync();
}
Redis:
public static async Task ConfigureRedisAsync(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleans(siloBuilder =>
{
siloBuilder.UseRedisReminderService(options =>
{
options.ConfigurationOptions = new ConfigurationOptions
{
EndPoints = { "localhost:6379" },
AbortOnConnectFail = false
};
});
});
using var host = builder.Build();
await host.RunAsync();
}
The RedisReminderTableOptions class provides the following configuration options:
| Property | Type | Description |
|---|---|---|
ConfigurationOptions |
ConfigurationOptions |
The StackExchange.Redis client configuration. Required. |
EntryExpiry |
TimeSpan? |
Optional expiration time for reminder entries. Only set this for ephemeral environments like testing. Default is null. |
CreateMultiplexer |
Func<RedisReminderTableOptions, Task<IConnectionMultiplexer>> |
Custom factory for creating the Redis connection multiplexer. |
Azure Cosmos DB:
Install the Microsoft.Orleans.Reminders.Cosmos NuGet package and configure with UseCosmosReminderService:
public static async Task ConfigureCosmosAsync(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleans(siloBuilder =>
{
siloBuilder.UseCosmosReminderService(options =>
{
options.ConfigureCosmosClient(
"https://myaccount.documents.azure.com:443/",
new DefaultAzureCredential());
options.DatabaseName = "Orleans";
options.ContainerName = "OrleansReminders";
options.IsResourceCreationEnabled = true;
});
});
using var host = builder.Build();
await host.RunAsync();
}
The CosmosReminderTableOptions class provides the following configuration options:
| Property | Type | Default | Description |
|---|---|---|---|
DatabaseName |
string |
"Orleans" |
The name of the Cosmos DB database. |
ContainerName |
string |
"OrleansReminders" |
The name of the container for reminder data. |
IsResourceCreationEnabled |
bool |
false |
When true, automatically creates the database and container if they don't exist. |
DatabaseThroughput |
int? |
null |
The provisioned throughput for the database. If null, uses serverless mode. |
ContainerThroughputProperties |
ThroughputProperties? |
null |
The throughput properties for the container. |
ClientOptions |
CosmosClientOptions |
new() |
The options passed to the Cosmos DB client. |
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.
.NET Aspire integration for reminders
When using .NET Aspire, you can configure Orleans reminders declaratively in your AppHost project. Aspire automatically injects the necessary configuration into your silo projects via environment variables.
Redis reminders with Aspire
AppHost project (Program.cs):
public static void RemindersRedisAppHost(string[] args)
{
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("redis");
var orleans = builder.AddOrleans("cluster")
.WithClustering(redis)
.WithReminders(redis);
builder.AddProject<Projects.Silo>("silo")
.WithReference(orleans)
.WaitFor(redis);
builder.Build().Run();
}
Silo project (Program.cs):
public static void RemindersRedisSilo(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.AddKeyedRedisClient("redis");
builder.UseOrleans();
builder.Build().Run();
}
Azure Table Storage reminders with Aspire
AppHost project (Program.cs):
public static void RemindersAzureTableAppHost(string[] args)
{
var builder = DistributedApplication.CreateBuilder(args);
var storage = builder.AddAzureStorage("storage")
.RunAsEmulator();
var reminders = storage.AddTables("reminders");
var orleans = builder.AddOrleans("cluster")
.WithClustering(reminders)
.WithReminders(reminders);
builder.AddProject<Projects.Silo>("silo")
.WithReference(orleans)
.WaitFor(storage);
builder.Build().Run();
}
Silo project (Program.cs):
public static void RemindersAzureTableSilo(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.AddKeyedAzureTableClient("reminders");
builder.UseOrleans();
builder.Build().Run();
}
Tip
During local development, Aspire automatically uses the Azurite emulator for Azure Storage. In production, configure a real Azure Storage account in your AppHost.
In-memory reminders for development with Aspire
For local development, you can use in-memory reminders that don't require external storage:
AppHost project (Program.cs):
public static void RemindersInMemoryAppHost(string[] args)
{
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("redis");
var orleans = builder.AddOrleans("cluster")
.WithClustering(redis)
.WithMemoryReminders();
builder.AddProject<Projects.Silo>("silo")
.WithReference(orleans)
.WaitFor(redis);
builder.Build().Run();
}
Silo project (Program.cs):
public static void RemindersInMemorySilo(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.UseOrleans();
builder.Build().Run();
}
Warning
In-memory reminders are lost when the silo restarts. Only use WithMemoryReminders() for local development and testing. For production, always use a persistent reminder storage provider like Redis, Azure Table Storage, or SQL.
Important
You must call the appropriate AddKeyed* method (such as AddKeyedRedisClient or AddKeyedAzureTableClient) to register the backing resource in the dependency injection container. Orleans providers look up resources by their keyed service nameāif you skip this step, Orleans won't be able to resolve the resource and will throw a dependency resolution error at runtime.
For more information about Orleans and .NET Aspire integration, see Orleans and .NET Aspire integration.
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
Pingis called, registers a reminder invoked every hour, starting immediately after registration. - The
Disposemethod cancels the reminder if it's registered.