计时器和提醒

运行时 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接口,并在粒子的构造函数中注入ITimerRegistryIReminderRegistry

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);
        }
    }
}

前面的代码执行以下作:

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