计时器和提醒
Orleans 运行时提供两种分别称为计时器和提醒的机制,使开发人员能够指定 grain 的定期行为。
计时器
计时器用于创建无需跨越多个激活(粒度的实例化)的定期粒度行为。 计时器与标准 .NET System.Threading.Timer 类相同。 此外,计时器在其操作的粒度激活中受到单线程执行保证。
每个激活都可能有零个或多个与之关联的计时器。 运行时在激活与之关联的运行时上下文中执行每个计时器例程。
计时器使用情况
若要启动计时器,请使用 RegisterGrainTimer
方法,该方法返回 IGrainTimer 引用:
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
若要取消计时器,请将其释放。
如果停用粒度或故障发生且接收器崩溃,计时器将停止触发。
重要注意事项:
- 如果启用了激活集合,执行计时器回调后不会将激活的状态从空闲更改为正在使用中。 这意味着不能使用计时器来推迟对其他空闲激活的停用。
- 传递给
Grain.RegisterGrainTimer
的时间段是从Task
返回的callback
被解析的时刻到下一次调用callback
的时刻所经过的时间量。 这不仅使得对callback
的连续调用不可能重叠,而且还使得完成callback
所花费的时间长度影响调用callback
的频率。 这是与 System.Threading.Timer 语义的重要偏差。 callback
的每次调用都会传递为单独轮次的激活,并且永远不会与同一激活的其他轮次同时运行。- 默认状态下,回调不会进行交错。 可以通过在 GrainTimerCreationOptions 上将“交错”设置为 true 来启用交错。
- 可以使用返回的 IGrainTimer 实例上的 Change(TimeSpan, TimeSpan) 方法来更新粒度计时器。
- 回调可以保持粒度处于活动状态,如果计时器周期相对较短,则会阻止收集粒度。 可以通过在 GrainTimerCreationOptions 上将 KeepAlive 设置为 true 来启用此功能。
- 回调可以接收 CancellationToken,当计时器被释放或粒度开始停用时,CancellationToken 会被取消。
- 回调可以释放触发它们的粒度计时器。
- 回调受粒度调用筛选器的约束。
- 启用分布式跟踪时,回调在分布式跟踪中可见。
- POCO 粒度(不继承自 Grain 的粒度类)可以使用 RegisterGrainTimer 扩展方法注册粒度计时器。
提醒
提醒与计时器类似,但有一些重要的区别:
- 提醒是永久性的,除非明确取消,否则会在几乎所有情况(包括部分或完整群集重启)下继续触发。
- 提醒“定义”写入存储。 每个特定事件及其特定时间则不然。 这会产生副作用,即如果群集在特定的提醒滴答声时关闭,它将被错过,并且只会在提醒的下次滴答声响起时发生。
- 提醒与粒度(而不是任何特定激活)相关联。
- 如果提醒的滴答声响起时,粒度没有与之相关的激活,则会创建该粒度。 如果激活处于空闲状态且已停用,则与同一粒度关联的提醒会在下次滴答声响起时重新激活此粒度。
- 提醒传递通过消息发生,并且受到与所有其他粒度方法相同的交错语义的约束。
- 提醒不应用于高频计时器(其周期应以分钟、小时或天为单位)。
配置
提醒持久有效,依赖于存储来发挥作用。 在提醒子系统正常运行之前,必须指定要使用的存储支持。 这是通过 Use{X}ReminderService
扩展方法配置其中一个提醒提供程序来完成的,其中 X
是提供程序的名称,例如 UseAzureTableReminderService。
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();
如果你只是想要提醒的占位符实现正常工作,而不设置 Azure 帐户或 SQL 数据库,那么这将为你提供提醒系统的仅开发实现:
var silo = new HostBuilder()
.UseOrleans(builder =>
{
builder.UseInMemoryReminderService();
})
.Build();
重要
如果有异类群集,其中孤岛处理不同的粒度类型(实现不同的接口),每个孤岛都必须添加提醒的配置,即使孤岛本身不处理任何提醒。
提醒使用情况
使用提醒的粒度必须实现 IRemindable.ReceiveReminder 方法。
Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
Console.WriteLine("Thanks for reminding me-- I almost forgot!");
return Task.CompletedTask;
}
若要启动提醒,请使用 Grain.RegisterOrUpdateReminder 返回,该方法返回 IGrainReminder 对象:
protected Task<IGrainReminder> RegisterOrUpdateReminder(
string reminderName,
TimeSpan dueTime,
TimeSpan period)
reminderName
:是一个字符串,必须唯一标识上下文粒度范围内的提醒。dueTime
:指定在发出第一声计时器滴答之前要等待的时间量。period
:指定计时器的时间段。
由于提醒在任何单个激活的生命周期内有效,因此必须明确取消它们(而不是将其释放)。 通过调用 Grain.UnregisterReminder 取消提醒:
protected Task UnregisterReminder(IGrainReminder reminder)
reminder
是由 Grain.RegisterOrUpdateReminder 返回的句柄对象。
不能保证 IGrainReminder
的实例在超出激活的生命周期之后仍然有效。 如果要以持久的方式标识提醒,请使用包含提醒名称的字符串。
如果只有提醒的名称并且需要相应的 IGrainReminder
实例,请调用 Grain.GetReminder 方法:
protected Task<IGrainReminder> GetReminder(string reminderName)
确定使用哪一个
我们建议在以下情况下使用定时器:
- 如果激活被停用或发生故障时,计时器停止运行不重要(或可取)。
- 计时器的分辨率很小(例如,可合理地以秒或分钟表示)。
- 计时器回调可以从 Grain.OnActivateAsync() 或调用粒度方法时启动。
我们建议在以下情况下使用提醒:
- 当定期行为需要在激活和任何故障中存在时。
- 执行不常有的任务(例如,可合理地以分钟、小时或天来表示)。
结合使用计时器和提醒
可以考虑结合使用提醒和计时器来实现目标。 例如,如果需要一个分辨率较小的计时器(需要在不同的激活中有效),则可以使用每 5 分钟运行一次的提醒,其目的是唤醒重启由于停用而可能丢失的本地计时器的粒度。
POCO 粒度注册
要向 POCO 粒度注册计时器或提醒,请实现 IGrainBase 接口并将 ITimerRegistry 或 IReminderRegistry 注入粒度的构造函数中。
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);
}
}
}
前面的代码:
- 定义实现 IGrainBase、
IPingGrain
、IDisposable 的 POCO 粒度。 - 注册每 10 秒调用一次的计时器,并在注册后 3 秒启动。
- 调用
Ping
时,注册每小时调用一次的提醒,并在注册后立即开始。 - 如果注册了提醒,
Dispose
方法将取消该提醒。