概要
「冪等性(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
└─ 違反すると:リトライ時に二重送信・二重登録が起きる
両者は独立した概念。片方を満たしてももう片方は保証されない。