Share via


計時器和提醒

Orleans 執行階段提供兩種機制,稱為計時器和提醒,可讓開發人員指定精細度的定期行為。

計時器

計時器可用來建立不需要跨多個啟用 (精細度具現化) 的定期精細度行為。 計時器與標準 .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 所傳回 Task 進行解析,到下一次叫用 asyncCallback 應該發生的傳遞時間量。 這不只讓後續呼叫 asyncCallback 重疊變得不可能,也讓 asyncCallback 完成所需的時間長度會影響叫用 asyncCallback 的頻率。 這是與 System.Threading.Timer 語意的重要偏差。
  • asyncCallback 的每個叫用都會在不同的回合傳遞至啟用,而且永遠不會與相同啟用上的其他回合同時執行。 不過,asyncCallback 叫用不會以訊息的形式傳遞,因此不會受限於訊息交錯語意。 這表示 asyncCallback 的叫用其行為方式會如同精細度重複執行,並且會與其他精細度要求同時執行。 若要使用精細度的要求排程語意,您可以呼叫精細度方法來執行您在 asyncCallback 內完成的工作。 另一個替代方法是使用 AsyncLockSemaphoreSlim。 如需更詳細的說明,請參閱 Orleans 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)

reminderGrain.RegisterOrUpdateReminder 所傳回的控制代碼物件。

IGrainReminder 的執行個體不保證在超過啟用的生命週期後仍然有效。 如果您想要以持續的方式識別提醒,請使用包含提醒名稱的字串。

如果您只有提醒的名稱,而且需要 IGrainReminder 的對應執行個體,請呼叫 Grain.GetReminder 方法:

protected Task<IGrainReminder> GetReminder(string reminderName)

決定要使用哪一個

建議您在下列情況下使用計時器:

  • 如果計時器不重要 (或不需要) 時,則在啟用已停用或發生失敗時,計時器會停止運作。
  • 計時器解析度很小 (例如,以秒或分鐘合理表達)。
  • 可以從 Grain.OnActivateAsync() 或在叫用精細度方法時,啟動計時器回呼。

建議您在下列情況下使用提醒:

  • 當定期行為需要在啟用和任何失敗下存留時。
  • 執行不頻繁的工作 (例如,以分鐘、小時或天合理表達)。

合併計時器和提醒

您可以考慮使用提醒和計時器的組合來完成您的目標。 例如,如果您需要一個必須跨啟用存留的小型解析度計時器,您可以使用每隔五分鐘執行一次的提醒,其目的是要喚醒精細度,重新啟動可能由於停用而遺失的本機計時器。

POCO 精細度註冊

若要向 POCO 精細度註冊計時器或提醒,您可以實作 IGrainBase 介面,並將 IReminderRegistryITimerRegistry 插入至精細度的建構函式中。

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 方法會取消提醒。