エンティティ関数

エンティティ関数では、"持続エンティティ" と呼ばれる小さい状態の読み取りと更新のための操作が定義されています。 オーケストレーター関数と同様、エンティティ関数は特殊なトリガー型であるエンティティ トリガーを含む関数です。 オーケストレーター関数とは異なり、エンティティ関数では、制御フローを介して状態を暗黙的に表すのではなく、エンティティの状態を明示的に管理します。 エンティティは、それぞれが適度なサイズの状態を備えた多数のエンティティ全体に作業を分散することにより、アプリケーションをスケールアウトする手段を提供します。

Note

エンティティ関数と関連する機能は Durable Functions 2.0 以降でのみ使用できます。 現在、.NET in-proc、.NET 分離ワーカー、JavaScript、Python ではサポートされていますが、PowerShell、Java ではサポートされていません。

重要

現在、エンティティ関数は PowerShell と Java ではサポートされていません。

一般的な概念

エンティティは、メッセージを介して通信する小さなサービスと同じように動作します。 各エンティティは、一意の ID と内部状態 (存在する場合) を備えています。 サービスやオブジェクトと同様に、エンティティは、要求されたときに操作を実行します。 操作を実行すると、エンティティの内部状態が更新されることがあります。 また、外部サービスを呼び出して応答を待つこともあります。 エンティティは、リライアブル キューを介して暗黙的に送信されるメッセージを使用して、他のエンティティ、オーケストレーション、およびクライアントと通信します。

競合を防ぐために、1 つのエンティティに対するすべての操作は、直列に (つまり順番に) 実行されることが保証されます。

Note

呼び出されたエンティティにより、ペイロードが完了に処理された後、後で入力が到着したらアクティブになるように新しい実行がスケジュールされます。 その結果、エンティティの実行ログで、各エンティティの呼び出しの後に追加の実行が示されることがあります。これは想定されることです。

エンティティ ID

エンティティには、一意の識別子である "エンティティ ID" を介してアクセスします。 エンティティ ID は単に、エンティティのインスタンスを一意に示す文字列のペアです。 構成は次のとおりです。

  • エンティティ名: エンティティの種類を示す名前です。 たとえば、"Counter" などです。この名前は、エンティティを実装するエンティティ関数の名前と一致している必要があります。 大文字と小文字は区別されません。
  • エンティティ キー: 同じ名前の他のすべてのエンティティの間でそのエンティティを一意に示す文字列です。 たとえば、GUID などです。

たとえば、Counter エンティティ関数は、オンライン ゲームでスコアを保持するために使用される可能性があります。 ゲームの各インスタンスは、@Counter@Game1@Counter@Game2 などの一意のエンティティ ID を持ちます。 特定のエンティティを対象とするすべての操作では、エンティティ ID をパラメーターとして指定する必要があります。

エンティティの操作

エンティティで操作を呼び出すには、次を指定します。

  • ターゲット エンティティのエンティティ ID
  • 操作名: 実行する操作を指定する文字列です。 たとえば、Counter エンティティでは、addget、または reset 操作をサポートする場合があります。
  • 操作の入力: 操作のオプションの入力パラメーターです。 たとえば、add 操作では入力として整数値を受け取ることができます。
  • スケジュール時刻: 操作の配信時刻を指定するためのオプション パラメーターです。 たとえば、将来、数日間にわたって操作を実行するよう確実にスケジュールすることができます。

操作では、結果値またはエラー結果 (JavaScript エラーや .NET 例外など) を返すことができます。 この結果またはエラーは、操作を呼び出したオーケストレーションで発生します。

エンティティ操作では、エンティティの状態の作成、読み取り、更新、および削除を行うこともできます。 エンティティの状態は、常にストレージ内に持続的に保持されます。

エンティティの定義

エンティティは関数ベースの構文を使用して定義します。エンティティは関数として表され、操作はアプリケーションによって明示的にディスパッチされます。

現在、.NET でエンティティを定義するための 2 つの異なる API があります。

関数ベースの構文を使用する場合、エンティティは関数として表され、操作はアプリケーションによって明示的にディスパッチされます。 この構文は、単純な状態のエンティティ、操作が少ないエンティティ、または動的な操作のセット (アプリケーション フレームワーク内のような) を備えたエンティティに適しています。 この構文は、コンパイル時に型エラーがキャッチされないため、保守が煩雑になる可能性があります。

特定の API は、C# 関数が分離ワーカー プロセス (推奨) で実行されるか、ホストと同じプロセスで実行されるかによって異なります。

次のコードは、持続的関数として実装されているシンプルな Counter エンティティの例です。 この関数では 3 つの操作 addresetget が定義されていて、いずれも整数の状態に対して動作します。

[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
    switch (ctx.OperationName.ToLowerInvariant())
    {
        case "add":
            ctx.SetState(ctx.GetState<int>() + ctx.GetInput<int>());
            break;
        case "reset":
            ctx.SetState(0);
            break;
        case "get":
            ctx.Return(ctx.GetState<int>());
            break;
    }
}

関数ベースの構文と使用方法の詳細については、「関数ベースの構文」を参照してください。

durable-functions npm パッケージのバージョン 1.3.0 以降では、JavaScript で持続エンティティが利用できます。 以下のコードは、JavaScript で記述されている持続的関数として実装された Counter エンティティです。

Counter (function.json)

{
  "bindings": [
    {
      "name": "context",
      "type": "entityTrigger",
      "direction": "in"
    }
  ],
  "disabled": false
}

Counter (index.js)

const df = require("durable-functions");

module.exports = df.entity(function(context) {
    const currentValue = context.df.getState(() => 0);
    switch (context.df.operationName) {
        case "add":
            const amount = context.df.getInput();
            context.df.setState(currentValue + amount);
            break;
        case "reset":
            context.df.setState(0);
            break;
        case "get":
            context.df.return(currentValue);
            break;
    }
});

Note

v2 モデルの動作の詳細については、Azure Functions Python 開発者ガイドを参照してください。

次のコードは、Python で記述されている持続的関数として実装された Counter エンティティです。

import azure.functions as func
import azure.durable_functions as df

# Entity function called counter
@myApp.entity_trigger(context_name="context")
def Counter(context):
    current_value = context.get_state(lambda: 0)
    operation = context.operation_name
    if operation == "add":
        amount = context.get_input()
        current_value += amount
    elif operation == "reset":
        current_value = 0
    elif operation == "get":
        context.set_result(current_value)
    context.set_state(current_value)

エンティティへのアクセス

エンティティには、一方向または双方向の通信を使用してアクセスできます。 次の用語は、2 つの通信の形式を区別します。

  • エンティティの呼び出しでは、双方向 (ラウンドトリップ) 通信が使われます。 操作メッセージをエンティティに送信し、応答メッセージを待機してから続行します。 応答メッセージでは、結果値またはエラー結果 (JavaScript エラーや .NET 例外など) を提供できます。 この結果またはエラーは、呼び出し元によって確認されます。
  • エンティティのシグナル通知では、一方向 (ファイア アンド フォーゲット) 通信が使われます。 操作メッセージを送信しますが、応答を待ちません。 最終的にメッセージが配信されることは保証されますが、送信側は、それがいつかを認識せず、結果値やエラーを確認することもできません。

エンティティには、クライアント関数内、オーケストレーター関数内、またはエンティティ関数内からアクセスできます。 すべての形式の通信がすべてのコンテキストでサポートされるわけではありません。

  • クライアント内からは、エンティティにシグナル通知を出すことができ、エンティティの状態を読み取ることができます。
  • オーケストレーション内からは、エンティティにシグナル通知を出すことができ、エンティティを呼び出すことができます。
  • エンティティ内からは、エンティティにシグナル通知を出すことができます。

次の例では、エンティティにアクセスするこれらのさまざまな方法を示しています。

例:クライアントがエンティティにシグナル通知を出す

通常の Azure 関数 (クライアント関数とも呼ばれます) からエンティティにアクセスするには、エンティティ クライアントのバインドを使用します。 次の例では、このバインドを使用してエンティティにシグナル通知する、キューによってトリガーされた関数を示します。

Note

わかりやすくするために、次の例では、エンティティにアクセスするための緩やかに型指定された構文を示しています。 一般に、より多くの型チェックが提供されるので、インターフェイスを介してエンティティにアクセスすることをお勧めします。

[FunctionName("AddFromQueue")]
public static Task Run(
    [QueueTrigger("durable-function-trigger")] string input,
    [DurableClient] IDurableEntityClient client)
{
    // Entity operation input comes from the queue message content.
    var entityId = new EntityId(nameof(Counter), "myCounter");
    int amount = int.Parse(input);
    return client.SignalEntityAsync(entityId, "Add", amount);
}
const df = require("durable-functions");

module.exports = async function (context) {
    const client = df.getClient(context);
    const entityId = new df.EntityId("Counter", "myCounter");
    await client.signalEntity(entityId, "add", 1);
};
import azure.functions as func
import azure.durable_functions as df

# An HTTP-Triggered Function with a Durable Functions Client to set a value on a durable entity
@myApp.route(route="entitysetvalue")
@myApp.durable_client_input(client_name="client")
async def http_set(req: func.HttpRequest, client):
    logging.info('Python HTTP trigger function processing a request.')
    entityId = df.EntityId("Counter", "myCounter")
    await client.signal_entity(entityId, "add", 1)
    return func.HttpResponse("Done", status_code=200)

"シグナル通知" とは、エンティティの API 呼び出しが一方向で非同期であることを意味します。 クライアント関数は、エンティティが操作をいつ処理したかを認識できません。 また、クライアント関数は、結果の値または例外を確認することはできません。

例:クライアントがエンティティの状態を読み取る

次の例に示すように、クライアント関数では、エンティティの状態についてクエリを実行することもできます。

[FunctionName("QueryCounter")]
public static async Task<HttpResponseMessage> Run(
    [HttpTrigger(AuthorizationLevel.Function)] HttpRequestMessage req,
    [DurableClient] IDurableEntityClient client)
{
    var entityId = new EntityId(nameof(Counter), "myCounter");
    EntityStateResponse<JObject> stateResponse = await client.ReadEntityStateAsync<JObject>(entityId);
    return req.CreateResponse(HttpStatusCode.OK, stateResponse.EntityState);
}
const df = require("durable-functions");

module.exports = async function (context) {
    const client = df.getClient(context);
    const entityId = new df.EntityId("Counter", "myCounter");
    const stateResponse = await client.readEntityState(entityId);
    return stateResponse.entityState;
};
# An HTTP-Triggered Function with a Durable Functions Client to retrieve the state of a durable entity
@myApp.route(route="entityreadvalue")
@myApp.durable_client_input(client_name="client")
async def http_read(req: func.HttpRequest, client):
    entityId = df.EntityId("Counter", "myCounter")
    entity_state_result = await client.read_entity_state(entityId)
    entity_state = "No state found"
    if entity_state_result.entity_exists:
      entity_state = str(entity_state_result.entity_state)
    return func.HttpResponse(entity_state)

エンティティ状態のクエリは永続性追跡ストアに送信され、エンティティの最後に保存された状態が返されます。 この状態は、常に "コミット済み" 状態です。つまり、操作の実行中に想定される一時的な中間状態ではありません。 ただし、この状態は、エンティティのメモリ内の状態より古い可能性があります。 次のセクションで説明するように、エンティティのメモリ内の状態を読み取ることができるのはオーケストレーションだけです。

例:オーケストレーションがエンティティにシグナル通知を出し、エンティティを呼び出す

オーケストレーター関数では、オーケストレーション トリガー バインドに対する API を使用して、エンティティにアクセスできます。 次のコード例では、オーケストレーター関数による Counter エンティティの呼び出しとシグナル通知を示します。

[FunctionName("CounterOrchestration")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var entityId = new EntityId(nameof(Counter), "myCounter");

    // Two-way call to the entity which returns a value - awaits the response
    int currentValue = await context.CallEntityAsync<int>(entityId, "Get");
    if (currentValue < 10)
    {
        // One-way signal to the entity which updates the value - does not await a response
        context.SignalEntity(entityId, "Add", 1);
    }
}
const df = require("durable-functions");

module.exports = df.orchestrator(function*(context){
    const entityId = new df.EntityId("Counter", "myCounter");

    // Two-way call to the entity which returns a value - awaits the response
    currentValue = yield context.df.callEntity(entityId, "get");
});

Note

JavaScript では現在、オーケストレーターからのエンティティのシグナル通知はサポートされていません。 代わりに callEntity を使用してください

@myApp.orchestration_trigger(context_name="context")
def orchestrator(context: df.DurableOrchestrationContext):
    entityId = df.EntityId("Counter", "myCounter")
    context.signal_entity(entityId, "add", 3)
    logging.info("signaled entity")
    state = yield context.call_entity(entityId, "get")
    return state

エンティティを呼び出して応答 (戻り値または例外) を取得できるのは、オーケストレーションだけです。 クライアント バインドを使用するクライアント関数は、エンティティに対するシグナル通知だけが可能です。

Note

オーケストレーター関数からのエンティティの呼び出しは、オーケストレーター関数からのアクティビティ関数の呼び出しと似ています。 主な違いは、エンティティ関数は、アドレス (エンティティ ID) を持つ持続オブジェクトであることです。 エンティティ関数は、操作名の指定がサポートされています。 それに対し、アクティビティ関数はステートレスであり、操作の概念はありません。

例:エンティティがエンティティにシグナル通知を出す

エンティティ関数は、操作の実行中に他のエンティティ (またはそれ自体) にシグナルを送信することができます。 たとえば、前の Counter エンティティの例を変更して、カウンターが値 100 に達したときに "マイルストーン到達" シグナルを一部の監視エンティティに送信することができます。

   case "add":
        var currentValue = ctx.GetState<int>();
        var amount = ctx.GetInput<int>();
        if (currentValue < 100 && currentValue + amount >= 100)
        {
            ctx.SignalEntity(new EntityId("MonitorEntity", ""), "milestone-reached", ctx.EntityKey);
        }

        ctx.SetState(currentValue + amount);
        break;
    case "add":
        const amount = context.df.getInput();
        if (currentValue < 100 && currentValue + amount >= 100) {
            const entityId = new df.EntityId("MonitorEntity", "");
            context.df.signalEntity(entityId, "milestone-reached", context.df.instanceId);
        }
        context.df.setState(currentValue + amount);
        break;

Note

Python では、エンティティ間の通知はまだサポートされていません。 エンティティの通知には、代わりにオーケストレーターを使用してください。

エンティティの調整

複数のエンティティ間で操作を調整することが必要になる場合があります。 たとえば、銀行のアプリケーションでは、個々の銀行口座を表すエンティティがある場合があります。 口座間で送金を行う場合は、送金元口座に十分な資金があることを確認する必要があります。 さらに、送金元口座と送金先口座の両方に対する更新がトランザクション的に一貫した方法で実行されることを確認する必要もあります。

例: 資金の送金

次のコード例では、オーケストレーター関数を使用して、2 つの口座エンティティ間で送金を行います。 エンティティの更新を調整するには、LockAsync メソッドを使用して、オーケストレーション内に クリティカル セクション を作成する必要があります。

Note

わかりやすくするため、この例では、前に定義した Counter エンティティを再利用します。 実際のアプリケーションでは、より詳細な BankAccount エンティティを定義することをお勧めします。

// This is a method called by an orchestrator function
public static async Task<bool> TransferFundsAsync(
    string sourceId,
    string destinationId,
    int transferAmount,
    IDurableOrchestrationContext context)
{
    var sourceEntity = new EntityId(nameof(Counter), sourceId);
    var destinationEntity = new EntityId(nameof(Counter), destinationId);

    // Create a critical section to avoid race conditions.
    // No operations can be performed on either the source or
    // destination accounts until the locks are released.
    using (await context.LockAsync(sourceEntity, destinationEntity))
    {
        ICounter sourceProxy = 
            context.CreateEntityProxy<ICounter>(sourceEntity);
        ICounter destinationProxy =
            context.CreateEntityProxy<ICounter>(destinationEntity);

        int sourceBalance = await sourceProxy.Get();

        if (sourceBalance >= transferAmount)
        {
            await sourceProxy.Add(-transferAmount);
            await destinationProxy.Add(transferAmount);

            // the transfer succeeded
            return true;
        }
        else
        {
            // the transfer failed due to insufficient funds
            return false;
        }
    }
}

.NET の LockAsync では、破棄されるとクリティカル セクションが終了される IDisposable が返されます。 この IDisposable の結果を using ブロックと共に使用して、クリティカル セクションの構文表現を取得できます。

前の例では、オーケストレーター関数を使用して、送金元エンティティから送金先エンティティに送金を行いました。 LockAsync メソッドにより、送金元と送金先の両方の口座エンティティがロックされました。 このロックにより、オーケストレーションのロジックが using ステートメントの最後でクリティカル セクションを終了するまで、他のクライアントによってどちらの口座の状態も照会または変更されないことが保証されました。 この動作により、送金元口座からの過剰な送金の可能性を防ぐことができます。

Note

オーケストレーションが (正常に、またはエラーで) 終了すると、進行中のすべてのクリティカル セクションが暗黙的に終了し、すべてのロックが解除されます。

クリティカル セクションの動作

LockAsync メソッドによって、オーケストレーション内にクリティカル セクションが作成されます。 これらのクリティカル セクションにより、指定されているエンティティのセットに対して他のオーケストレーションにより重複する変更が行われることを防止できます。 内部的には、LockAsync API ではエンティティに "ロック" 操作が送信され、同じこれらの各エンティティから "ロック取得済み" 応答メッセージを受信するとその API は制御を返します。 ロックとロック解除はどちらも、すべてのエンティティでサポートされている組み込み操作です。

ロック状態にあるエンティティに対して、他のクライアントからの操作は許可されません。 この動作により、一度に 1 つのオーケストレーション インスタンスだけがエンティティをロックできるようになります。 オーケストレーションによってロックされているエンティティに対して呼び出し元が操作を呼び出そうとすると、その操作は保留中操作キューに配置されます。 保持しているオーケストレーションがロックを解放するまで、保留中の操作は処理されません。

Note

この動作は、C# の lock ステートメントなど、ほとんどのプログラミング言語で使用される同期プリミティブとは若干異なります。 たとえば、C# では、複数のスレッド間で同期が確実に行われるようにするには、すべてのスレッドで lock ステートメントを使用する必要があります。 一方、エンティティでは、すべての呼び出し元が明示的にエンティティをロックする必要はありません。 いずれかの呼び出し元によってエンティティがロックされた場合、そのエンティティに対する他のすべての操作はブロックされ、キューでそのロックの後に配置されます。

エンティティのロックは持続的であるため、実行プロセスがリサイクルされた場合でも永続化されます。 ロックは、エンティティの持続状態の一部として内部的に保持されます。

トランザクションとは異なり、クリティカル セクションでは、エラーが発生した場合に変更が自動的にロール バックされません。 代わりに、たとえばエラーまたは例外をキャッチすることにより、エラー処理 (ロールバックや再試行など) を明示的にコーディングする必要があります。 この設計の選択は意図的なものです。 一般に、オーケストレーションのすべての効果を自動的にロールバックすることは困難であるか不可能です。これは、オーケストレーションによってアクティビティが実行され、ロールバックできない外部サービスへの呼び出しが行われることがあるためです。 また、ロールバックの試み自体が失敗し、さらにエラー処理が必要になる場合があります。

クリティカル セクションのルール

ほとんどのプログラミング言語の低レベルのロック プリミティブとは異なり、クリティカル セクションは、デッドロックが発生しないことが保証されます。 デッドロックを防ぐために、次の制限が適用されます。

  • クリティカル セクションを入れ子にすることはできません。
  • クリティカル セクションでサブオーケストレーションを作成することはできません。
  • クリティカル セクションでは、自分がロックしているエンティティのみを呼び出すことができます。
  • クリティカル セクションでは、複数の並列呼び出しを使用して、同じエンティティを呼び出すことはできません。
  • クリティカル セクションでは、自分がロックしていないエンティティのみにシグナルを送信できます。

これらのルールに違反すると、どのルールに違反していたかを説明するメッセージが含まれる実行時エラー (.NET の LockingRulesViolationException など) が発生します。

仮想アクターとの比較

持続エンティティの多くの機能は、アクター モデルに基づいています。 アクターについて既によくわかっている場合は、この記事で説明されている多くの概念を理解できます。 持続エンティティは、Orleans プロジェクトによって普及した仮想アクターまたはグレインに似ています。 次に例を示します。

  • 持続エンティティは、エンティティ ID を使用してアドレス指定可能です。
  • 持続エンティティの操作は、競合状態を避けるため、一度に 1 つずつ順番に実行されます。
  • 持続エンティティは、呼び出されるか、シグナル通知を受け取ると、暗黙的に作成されます。
  • 実行する操作がなくなると、持続エンティティはメモリからサイレントにアンロードされます。

いくつか注目すべき重要な違いがあります。

  • 持続エンティティは、待ち時間より持続性の方が優先されるので、待ち時間の要件が厳しいアプリケーションには適していない可能性があります。
  • 持続エンティティには、メッセージの組み込みのタイムアウトがありません。 Orleans では、構成可能な時間後にすべてのメッセージがタイムアウトします。 既定値は 30 秒です。
  • エンティティ間で送信されるメッセージは、信頼性の高い方法で順番に配信されます。 Orleans では、ストリームを介して送信されるコンテンツの信頼性の高い配信または順次配信がサポートされますが、グレイン間のすべてのメッセージについては保証されません。
  • エンティティでの要求 - 応答パターンは、オーケストレーションに制限されます。 エンティティ内からは、元のアクター モデルのように、そして Orleans のグレインとは異なり、一方向のメッセージ (シグナル通知とも呼ばれます) のみが許可されます。
  • 持続エンティティではデッドロックは発生しません。 Orleans では、デッドロックが発生する可能性があり、メッセージがタイムアウトするまで解決されません。
  • 持続エンティティは、持続オーケストレーションと使用でき、分散ロック メカニズムがサポートされます。

次のステップ