タイマーとアラーム

Orleans ランタイムには、タイマーおよびアラームと呼ばれる 2 つのメカニズムが用意されており、開発者はグレインの定期的な動作を指定できます。

タイマー

タイマーは、複数のアクティブ化 (グレインのインスタンス化) にまたがる必要のないグレインの定期的な動作を作成するために使われます。 タイマーは、標準的な .NET System.Threading.Timer クラスと同じです。 さらに、タイマーは、それが動作するグレイン アクティブ化内でのシングル スレッド実行保証の対象になります。また、その実行は、タイマー コールバックが AlwaysInterleaveAttribute でマークされたグレイン メソッドであるかのように、他の要求とインターリーブされます。

各アクティブ化に、0 個以上のタイマーを関連付けることができます。 ランタイムにより、各タイマー ルーチンが、それと関連付けられているアクティブ化のランタイム コンテキスト内で実行されます。

タイマーの使用方法

タイマーを開始するには、IDisposable の参照を返す Grain.RegisterTimer メソッドを使います。

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 内で行っていた作業を実行します。 もう 1 つの方法は、AsyncLock または SemaphoreSlim を使うことです。 詳しくは、Orleans GitHub issue #2574 をご覧ください。

リマインダー

アラームはタイマーに似ていますが、いくつかの重要な違いがあります。

  • アラームは永続的であり、明示的に取り消されない限り、ほぼすべての状況 (部分的または完全なクラスターの再起動を含む) でトリガーされ続けます。
  • アラームの "定義" は、ストレージに書き込まれます。 ただし、特定の日時が指定された個々の特定の実行はそうではありません。 これにより、特定のアラーム ティックの時点でクラスターがダウンした場合、そのアラームは実行されず、アラームの次のティックのみが実行されるという副作用があります。
  • アラームは、特定のアクティブ化ではなく、グレインに関連付けられます。
  • アラームのティックの時点でグレインにアクティブ化が関連付けられていない場合は、グレインが作成されます。 アクティブ化がアイドル状態になり、非アクティブ化された場合、同じグレインに関連付けられているアラームにより、次のティックの時点で、グレインが再アクティブ化されます。
  • アラームの配信はメッセージを介して行われ、他のすべてのグレイン メソッドと同じインターリーブ セマンティクスの対象となります。
  • 高頻度のタイマーには、アラームを使わないでください。アラームの期間は、分、時間、または日の単位で測定する必要があります。

構成

アラームは永続的であり、機能するためにストレージに依存します。 アラーム サブシステムが機能する前に、使用するストレージ バッキングを指定する必要があります。 これは、Use{X}ReminderService 拡張メソッドを使ってアラーム プロバイダーの 1 つを構成することによって行います。X はプロバイダーの名前です (例: UseAzureTableReminderService)。

Azure Table の構成:

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

アラームを開始するには、IGrainReminder オブジェクトを返す Grain.RegisterOrUpdateReminder メソッドを使います。

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.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 が呼び出されると、1 時間ごとに呼び出されるアラームが登録され、登録後すぐに開始されます。
  • 登録されている場合、Dispose メソッドによりアラームが取り消されます。