0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Durable Functions で学ぶ「冪等性」と「決定論的」の違い

0
Posted at

概要

「冪等性(Idempotent)」と「決定論的(Deterministic)」は、分散システムの文脈でよく登場する2つの概念だ。
Azure Durable Functions では、Orchestrator には決定論的が、Activity には冪等性がそれぞれ要求される。
この記事では両概念の定義と違いを整理し、なぜ Durable Functions がこの設計になっているかを C# コードサンプルで解説する。


決定論的(Deterministic)とは

同じ入力を与えれば必ず同じ出力になる性質のことだ。
焦点は「入力と出力の関係」にある。副作用の有無は問わない。

// ✅ 決定論的:引数が同じなら結果は常に同じ
public int Add(int a, int b) => a + b;

// ❌ 非決定論的:実行タイミングによって結果が変わる
public DateTime GetNow() => DateTime.UtcNow;

// ❌ 非決定論的:呼び出しごとに値が変わる
public double GetRandom() => Random.Shared.NextDouble();

冪等性(Idempotent)とは

何回実行しても副作用の結果が同じになる性質だ。
焦点は「外部への影響」にある。戻り値が毎回同じかどうかは関係ない。

// ✅ 冪等:何回呼んでも DB の状態は同じ(上書き)
public async Task SetUserNameAsync(string userId, string name)
{
    await _db.Users.Upsert(userId, name);
}

// ❌ 冪等でない:呼ぶたびにレコードが増える
public async Task AddUserAsync(string name)
{
    await _db.Users.InsertAsync(new User { Name = name });
}

両者の違いを際立たせる例

// 🔵 決定論的だが冪等でない
// → 戻り値は常に "sent" だが、呼ぶたびに SMS が届く
public async Task<string> SendSmsAsync(string userId, string otp)
{
    await _smsClient.SendAsync(userId, otp);
    return "sent";
}

// 🟠 冪等だが決定論的でない
// → 副作用はゼロだが、呼び出すたびに異なるタイムスタンプを返す
public long GetTimestamp() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

この2例が示すように、両者は独立した概念だ。どちらか一方を満たしても、もう一方を満たすとは限らない。


Durable Functions における使い分け

要件 対象 理由
決定論的 Orchestrator replay 時に同じ結果を再現するため
冪等性 Activity 自動リトライ時に二重実行されても安全なため

Orchestrator が決定論的でなければならない理由

Durable Functions は中断・再開のたびに Orchestrator のコードを最初から再実行(replay) する仕組みになっている。
Azurite(ローカルストレージエミュレーター)や Azure Storage に保存した実行履歴を参照しながら「どこまで進んだか」を復元するため、毎回同じ結果が返ってこないと状態が壊れる。

Orchestrator の replay フロー

クライアント
  │
  ▼
[Orchestrator 起動]
  │
  ├─ Activity A を呼び出す(履歴に記録)
  │     ↓
  │   ネットワーク障害 → 中断
  │
  ▼
[Orchestrator replay 開始]
  │
  ├─ コードを先頭から再実行
  ├─ 「Activity A は履歴に完了済みで記録されている」→ スキップ
  └─ Activity B を次に呼び出す(再実行)

Orchestrator 内で禁止される操作

[FunctionName("MyOrchestrator")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    // ❌ 禁止:replay のたびに値が変わる
    var now = DateTime.UtcNow;

    // ✅ 正しい:Durable が管理するタイムスタンプを使う
    var now = context.CurrentUtcDateTime;

    // ❌ 禁止:replay のたびに異なる値になる
    var id = Guid.NewGuid();

    // ✅ 正しい:決定論的な GUID を生成する
    var id = context.NewGuid();

    // ❌ 禁止:外部 API への直接呼び出し(副作用 + 非決定論的)
    var result = await httpClient.GetAsync("https://example.com/api");

    // ✅ 正しい:Activity 経由で呼び出す
    var result = await context.CallActivityAsync<string>("FetchDataActivity", null);
}

Activity が冪等でなければならない理由

ネットワーク障害などで Activity が失敗した場合、Durable は自動でリトライする。
このとき「2回実行されても1回と同じ結果」にならないと、二重送信・二重登録が発生する。

⚠️ 冪等でない Activity の例(危険)

[FunctionName("SendEmailActivity")]
public static async Task SendEmailBad(
    [ActivityTrigger] string userId,
    ILogger log)
{
    // ❌ リトライのたびにメールが送信される
    await _emailClient.SendAsync(userId, "ご登録ありがとうございます");
}

✅ 冪等にする実装パターン

[FunctionName("SendEmailActivity")]
public static async Task SendEmailGood(
    [ActivityTrigger] SendEmailInput input,
    ILogger log)
{
    // ✅ 送信済みフラグを確認してから送信(重複排除)
    var alreadySent = await _db.EmailLogs
        .AnyAsync(e => e.IdempotencyKey == input.IdempotencyKey);

    if (alreadySent)
    {
        log.LogInformation("Already sent. Skipping. Key={Key}", input.IdempotencyKey);
        return;
    }

    await _emailClient.SendAsync(input.UserId, "ご登録ありがとうございます");
    await _db.EmailLogs.AddAsync(new EmailLog { IdempotencyKey = input.IdempotencyKey });
    await _db.SaveChangesAsync();
}

public record SendEmailInput(string UserId, string IdempotencyKey);

補足: IdempotencyKey は Orchestrator 側で context.NewGuid() を使って生成し、Activity に渡すのが定石だ。


よくあるミス:Orchestrator 内で DateTime.UtcNow を使う

[FunctionName("RegisterOrchestrator")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    // ❌ replay 時に「初回実行時と異なる時刻」が返り、ロジックが壊れる
    var now = DateTime.UtcNow;
    if (now.Hour >= 9 && now.Hour < 18)
    {
        await context.CallActivityAsync("BusinessHoursActivity", null);
    }
}
[FunctionName("RegisterOrchestrator")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    // ✅ Durable が管理する「オーケストレーション開始時刻」を使う
    var now = context.CurrentUtcDateTime;
    if (now.Hour >= 9 && now.Hour < 18)
    {
        await context.CallActivityAsync("BusinessHoursActivity", null);
    }
}

replay 中は context.CurrentUtcDateTime初回実行時の時刻を返すため、時刻依存のロジックが安全に動作する。


まとめ

決定論的 vs 冪等性

① 決定論的(Deterministic)
   └─ 焦点:入力と出力の関係
   └─ 「同じ入力 → 必ず同じ出力」
   └─ Durable Functions での対象:Orchestrator
   └─ 違反すると:replay 時に状態が壊れる

② 冪等性(Idempotent)
   └─ 焦点:外部への副作用
   └─ 「何回実行しても副作用は同じ」
   └─ Durable Functions での対象:Activity
   └─ 違反すると:リトライ時に二重送信・二重登録が起きる

両者は独立した概念。片方を満たしてももう片方は保証されない。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?