运行时 Orleans 提供两种机制(计时器和提醒),可以指定颗粒的周期性行为。
计时器
使用 计时器 创建不需要跨多个激活(粒度实例化)的定期粒度行为。 计时器与标准 .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
的时间段是从callback
返回的Task
解决的那一刻,到下一次调用callback
应发生的那一刻所经过的时间量。 这不仅可以防止连续调用callback
重叠,还意味着callback
完成所需的时间会影响callback
的调用频率。 这是与 System.Threading.Timer 语义的重要偏差。 - 每次调用
callback
都会在单独的回合内传递给一个激活,并且永远不会与同一激活上的其他回合同时运行。 - 默认情况下,回调不会交错。 可以通过在
GrainTimerCreationOptions
上将Interleave
设置为true
来启用交错。 - 可以使用返回的
IGrainTimer
实例上的Change(TimeSpan, TimeSpan)
方法来更新粒度计时器。 - 回调可以保持粒度处于活动状态,如果计时器周期相对较短,则阻止收集。 通过在
GrainTimerCreationOptions
上将KeepAlive
设置为true
来启用此功能。 - 回调可以接收一个
CancellationToken
,当计时器被释放或粒度开始停用时,该CancellationToken
会被取消。 - 回调可以释放触发它们的粒度计时器。
- 回调受粒度调用筛选器的约束。
- 启用分布式跟踪时,回调在分布式跟踪中可见。
- POCO 粒(没有从
Grain
继承的粒类)可以使用RegisterGrainTimer
扩展方法注册粒计时器。
提醒
提醒与计时器类似,但有一些重要的区别:
- 提醒是永久性的,除非明确取消,否则会在几乎所有情况(包括部分或完整群集重启)下继续触发。
- 提醒“定义”写入存储。 但是,不会存储每个具体事件及其发生的具体时间。 这会产生副作用:如果群集在特定的提醒时刻到来时关闭,它将被错过,并且只有下一个提醒时刻会发生。
- 提醒与粒度(而不是任何特定激活)相关联。
- 如果某个粒度在提醒计时周期时没有与之关联的激活, Orleans 则创建粒度激活。 如果激活处于空闲状态且已停用,则与同一粒度关联的提醒会在下次滴答声响起时重新激活此粒度。
- 提醒传递通过消息发生,并且受到与所有其他粒度方法相同的交错语义的约束。
- 不应对高频率计时器使用提醒;其时间段应以分钟、小时或天为单位。
配置
由于提醒是永久性的,因此它们依赖于存储来运行。 在提醒子系统才能正常运行之前,必须指定要使用的存储后备。 为此,请通过 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();
重要说明
如果你有一个异构群集,其中接收器处理不同的 grain 类型(实现不同的接口),则每个接收器都必须添加提醒配置,即使接收器本身不处理任何提醒。
提醒使用情况
使用提醒的粒子必须实现该方法 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() 或者在调用粒度方法时启动计时器回调。
建议在以下情况下使用提醒:
- 当周期性行为需要保持活跃和承受任何失败时。
- 执行不常进行的任务(例如,可以用分钟、小时或天来合理表达)。
结合使用计时器和提醒
可以考虑结合使用提醒和计时器来实现目标。 例如,如果需要具有小分辨率并跨激活存活的计时器,可以使用每隔五分钟运行一次的提醒。 其目的是唤醒一个粒度,这个粒度可能因停用而丢失,并重新启动本地计时器。
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 Grain。 - 注册一个计时器,在注册后 3 秒开始,每隔 10 秒调用一次。
- 当调用
Ping
时,会注册一个提醒,该提醒每小时触发一次,并在注册后立即开始。 - 如果注册了提醒,
Dispose
方法将取消该提醒。