计时器和提醒

Orleans 运行时提供两种分别称为计时器和提醒的机制,使开发人员能够指定 grain 的定期行为。

计时器

计时器用于创建无需跨越多个激活(粒度的实例化)的定期粒度行为。 计时器与标准 .NET System.Threading.Timer 类相同。 此外,计时器在其运行的粒度激活内受到单线程执行保证的约束,并且它们的执行与其他请求交错,就好像计时器回调是标记了 AlwaysInterleaveAttribute 的粒度方法。

每个激活都可能有零个或多个与之关联的计时器。 运行时在激活与之关联的运行时上下文中执行每个计时器例程。

计时器使用情况

若要启动计时器,请使用 Grain.RegisterTimer 方法,该方法返回 IDisposable 引用:

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

若要取消计时器,请将其释放。

如果停用粒度或故障发生且接收器崩溃,计时器将停止触发。

重要注意事项:

  • 如果启用了激活集合,执行计时器回调后不会将激活的状态从空闲更改为正在使用中。 这意味着不能使用计时器来推迟对其他空闲激活的停用。
  • 传递给 Grain.RegisterTimer 的时间段是从 asyncCallback 返回的任务被解析的时刻到下一次调用 asyncCallback 的时刻所经过的时间量。 这不仅使得对 asyncCallback 的连续调用不可能重叠,而且还使得完成 asyncCallback 所花费的时间长度影响调用 asyncCallback 的频率。 这是与 System.Threading.Timer 语义的重要偏差。
  • asyncCallback 的每次调用都会传递为单独轮次的激活,并且永远不会与同一激活的其他轮次同时运行。 但是,asyncCallback 调用不会作为消息传递,因此不受消息交错语义的约束。 这意味着,asyncCallback 的调用行为就像粒度是重入的一样,并与其他粒度请求并发执行。 为了使用粒度的请求计划语义,可以调用粒度方法来执行本应在 asyncCallback 中完成的工作。 另一种替代方法是使用 AsyncLockSemaphoreSlimOrleans GitHub 问题 #2574 中提供了更详细的说明。

提醒

提醒与计时器类似,但有一些重要的区别:

  • 提醒是永久性的,除非明确取消,否则会在几乎所有情况(包括部分或完整群集重启)下继续触发。
  • 提醒“定义”写入存储。 每个特定事件及其特定时间则不然。 这会产生副作用,即如果群集在特定的提醒滴答声时关闭,它将被错过,并且只会在提醒的下次滴答声响起时发生。
  • 提醒与粒度(而不是任何特定激活)相关联。
  • 如果提醒的滴答声响起时,粒度没有与之相关的激活,则会创建该粒度。 如果激活处于空闲状态且已停用,则与同一粒度关联的提醒会在下次滴答声响起时重新激活此粒度。
  • 提醒传递通过消息发生,并且受到与所有其他粒度方法相同的交错语义的约束。
  • 提醒不应用于高频计时器(其周期应以分钟、小时或天为单位)。

配置

提醒持久有效,依赖于存储来发挥作用。 在提醒子系统正常运行之前,必须指定要使用的存储支持。 这是通过 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 接口并将 ITimerRegistryIReminderRegistry 注入粒度的构造函数中。

using Orleans.Runtime;
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.RegisterTimer(
            grainContext,
            asyncCallback: static async state =>
            {
                // Omitted for brevity...
                // Use state

                await Task.CompletedTask;
            },
            state: this,
            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);
        }
    }
}

前面的代码:

  • 定义实现 IGrainBaseIPingGrainIDisposable 的 POCO 粒度。
  • 注册每 10 秒调用一次的计时器,并在注册后 3 秒启动。
  • 调用 Ping 时,注册每小时调用一次的提醒,并在注册后立即开始。
  • 如果注册了提醒,Dispose 方法将取消该提醒。