Funkce entit

Funkce entit definují operace pro čtení a aktualizaci malých částí stavu, označované jako odolné entity. Podobně jako funkce orchestrátoru jsou funkce entit se speciálním typem triggeru , triggerem entity. Na rozdíl od funkcí orchestrátoru funkce entit spravují stav entity explicitně, nikoli implicitně představující stav prostřednictvím toku řízení. Entity poskytují prostředky pro horizontální navýšení kapacity aplikací tím, že distribuují práci napříč mnoha entitami, z nichž každá má mírně velký stav.

Poznámka:

Funkce entit a související funkce jsou k dispozici pouze v Durable Functions 2.0 a vyšší. V současné době se podporují v .NET in-proc, izolovaném pracovním procesu .NET, JavaScriptu a Pythonu, ale ne v PowerShellu nebo Javě.

Důležité

Funkce entit se v současné době nepodporují v PowerShellu a Javě.

Obecné koncepty

Entity se chovají trochu jako malé služby, které komunikují prostřednictvím zpráv. Každá entita má jedinečnou identitu a interní stav (pokud existuje). Podobně jako služby nebo objekty provádějí entity operace po zobrazení výzvy. Když se operace spustí, může aktualizovat interní stav entity. Může také volat externí služby a čekat na odpověď. Entity komunikují s jinými entitami, orchestracemi a klienty pomocí zpráv, které se implicitně odesílají přes spolehlivé fronty.

Aby se zabránilo konfliktům, je zaručeno, že všechny operace na jedné entitě se budou spouštět sériově, tj. jeden po druhém.

Poznámka:

Při vyvolání entity zpracuje datovou část na dokončení a pak naplánuje nové spuštění, které se aktivuje, jakmile dorazí budoucí vstupy. V důsledku toho můžou protokoly spuštění entity po vyvolání každé entity zobrazit další spuštění. to je očekávané.

ID entity

Entity jsou přístupné prostřednictvím jedinečného identifikátoru , ID entity. ID entity je jednoduše dvojice řetězců, které jednoznačně identifikují instanci entity. Skládá se z:

  • Název entity, což je název, který identifikuje typ entity. Příkladem je "Counter" (Čítač). Tento název se musí shodovat s názvem funkce entity, která entitu implementuje. Nerozlišuje se na malá a velká písmena.
  • Klíč entity, což je řetězec, který jednoznačně identifikuje entitu mezi všemi ostatními entitami se stejným názvem. Příkladem je identifikátor GUID.

Counter Například funkce entity se může použít k udržení skóre v online hře. Každá instance hry má jedinečné ID entity, například @Counter@Game1 a @Counter@Game2. Všechny operace, které cílí na konkrétní entitu, vyžadují zadání ID entity jako parametru.

Operace entit

Pokud chcete vyvolat operaci u entity, zadejte:

  • ID entity cílové entity.
  • Název operace, což je řetězec, který určuje operaci, která se má provést. Entita Counter může například podporovat add, getnebo reset operace.
  • Vstup operace, což je volitelný vstupní parametr operace. Operace přidání může například jako vstup převzít celočíselnou hodnotu.
  • Naplánovaný čas, což je volitelný parametr pro zadání doby doručení operace. Například operaci je možné spolehlivě naplánovat tak, aby běžela v budoucnu několik dní.

Operace můžou vrátit výslednou hodnotu nebo chybový výsledek, například chybu JavaScriptu nebo výjimku .NET. K tomuto výsledku nebo chybě dochází v orchestracích, které volali operaci.

Operace entity může také vytvořit, číst, aktualizovat a odstranit stav entity. Stav entity je vždy trvale trvalý v úložišti.

Definování entit

Entity definujete pomocí syntaxe založené na funkcích, kde jsou entity reprezentovány jako funkce a operace jsou explicitně odeslány aplikací.

V současné době existují dvě různá rozhraní API pro definování entit v .NET:

Při použití syntaxe založené na funkcích jsou entity reprezentovány jako funkce a operace jsou explicitně odeslány aplikací. Tato syntaxe funguje dobře pro entity s jednoduchým stavem, několika operacemi nebo dynamickou sadou operací, jako jsou v aplikačních architekturách. Tato syntaxe nemůže být zdlouhavá, protože při kompilaci nezachytává chyby typu.

Konkrétní rozhraní API závisí na tom, jestli se funkce jazyka C# spouští v izolovaném pracovním procesu (doporučeno) nebo ve stejném procesu jako hostitel.

Následující kód je příkladem jednoduché Counter entity implementované jako odolná funkce. Tato funkce definuje tři operace, adda resetget, z nichž každá pracuje s celočíselnou stav.

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

Další informace o syntaxi založené na funkci a jejím použití najdete v tématu Syntaxe založená na funkci.

Odolné entity jsou dostupné v JavaScriptu od verze 1.3.0durable-functions balíčku npm. Následující kód je entita Counter implementovaná jako odolná funkce napsaná v JavaScriptu.

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

Poznámka:

Další podrobnosti o tom, jak model V2 funguje, najdete v příručce pro vývojáře v Pythonu pro Azure Functions.

Následující kód je entita Counter implementovaná jako odolná funkce napsaná v Pythonu.

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)

Přístup k entitě

Entity je možné získat přístup pomocí jednosměrné nebo obousměrné komunikace. Následující terminologie rozlišuje dvě formy komunikace:

  • Volání entity používá obousměrnou (zpáteční) komunikaci. Odešlete do entity zprávu operace a pak před pokračováním počkejte na zprávu s odpovědí. Zpráva odpovědi může poskytnout výslednou hodnotu nebo chybový výsledek, například chybu JavaScriptu nebo výjimku .NET. Tento výsledek nebo chyba pak volajícího pozoruje.
  • Signaling an entity uses one-way (fire and forget) communication. Odešlete zprávu operace, ale nečekáte na odpověď. I když je zaručeno, že se zpráva doručí nakonec, odesílatel neví, kdy a nemůže sledovat žádnou výslednou hodnotu nebo chyby.

Entity jsou přístupné z klientských funkcí, z funkcí orchestrátoru nebo z funkcí entity. Ne všechny formy komunikace jsou podporovány všemi kontexty:

  • V rámci klientů můžete signalizovat entity a číst stav entity.
  • V rámci orchestrací můžete signalizovat entity a můžete volat entity.
  • Z entit můžete signalizovat entity.

Následující příklady ilustrují různé způsoby přístupu k entitám.

Příklad: Klient signalizuje entitu

Pro přístup k entitě z běžné funkce Azure Functions, která se označuje také jako klientská funkce, použijte vazbu klienta entity. Následující příklad ukazuje funkci aktivovanou frontou, která signalizovala entitu pomocí této vazby.

Poznámka:

Pro zjednodušení následující příklady ukazují volně napsanou syntaxi pro přístup k entitě. Obecně doporučujeme přistupovat k entitám prostřednictvím rozhraní, protože poskytuje více kontroly typů.

[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)

Signál termínu znamená, že volání rozhraní API entity je jednosměrné a asynchronní. Klientskou funkci není možné zjistit, kdy entita operaci zpracovala. Funkce klienta také nemůže sledovat žádné výsledné hodnoty ani výjimky.

Příklad: Klient přečte stav entity.

Klientské funkce můžou také dotazovat stav entity, jak je znázorněno v následujícím příkladu:

[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)

Dotazy na stav entit se odesílají do úložiště durable tracking a vrací naposledy trvalý stav entity. Tento stav je vždy potvrzený stav, tj. nikdy se nejedná o dočasný přechodný stav, který se předpokládá uprostřed provádění operace. Je však možné, že tento stav je v porovnání se stavem v paměti entity zastaralý. Orchestrace můžou číst pouze stav v paměti entity, jak je popsáno v následující části.

Příklad: Signály orchestrace a volání entity

Funkce orchestratoru mají přístup k entitám pomocí rozhraní API na vazbě triggeru orchestrace. Následující příklad kódu ukazuje funkci orchestrátoru volání a signalizaci Counter entity.

[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");
});

Poznámka:

JavaScript v současné době nepodporuje signalizaci entity z orchestrátoru. Místo toho použijte 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

Pouze orchestrace můžou volat entity a získat odpověď, což může být návratová hodnota nebo výjimka. Klientské funkce, které používají vazbu klienta, můžou signalizovat pouze entity.

Poznámka:

Volání entity z funkce orchestrátoru se podobá volání funkce aktivity z funkce orchestrátoru. Hlavním rozdílem je, že funkce entit jsou odolné objekty s adresou, což je ID entity. Funkce entit podporují zadání názvu operace. Na druhou stranu jsou funkce aktivit bezstavové a nemají koncept operací.

Příklad: Entita signalizuje entitu

Funkce entity může během provádění operace odesílat signály jiným entitám nebo dokonce sama sobě. Můžeme například upravit předchozí Counter příklad entity tak, aby po dosažení hodnoty 100 odesílala určité entitě monitorování signál o dosažení milníku.

   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;

Poznámka:

Python zatím nepodporuje signály entity-entit. Místo toho použijte orchestrátor pro signalizační entity.

Koordinace entit

Můžou nastat chvíle, kdy potřebujete koordinovat operace napříč více entitami. Například v bankovní aplikaci můžete mít entity, které představují jednotlivé bankovní účty. Při převodu finančních prostředků z jednoho účtu do druhého musíte zajistit, aby zdrojový účet měl dostatek finančních prostředků. Musíte také zajistit, aby se aktualizace zdrojového i cílového účtu prováděly konzistentně konzistentně.

Příklad: Převod finančních prostředků

Následující příklad kódu přenese prostředky mezi dvěma entitami účtu pomocí funkce orchestrátoru. Koordinace aktualizací entit vyžaduje použití LockAsync metody k vytvoření kritické části v orchestraci.

Poznámka:

Pro zjednodušení tento příklad znovu použije dříve definovanou entitu Counter . Ve skutečné aplikaci by bylo lepší definovat podrobnější BankAccount entitu.

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

V rozhraní .NET vrátí hodnotuIDisposable, LockAsync která ukončí kritickou část při vyřazení. Tento IDisposable výsledek lze použít společně s blokem using k získání syntaktické reprezentace kritické části.

V předchozím příkladu funkce orchestrátoru přenese prostředky ze zdrojové entity do cílové entity. Metoda LockAsync uzamkne entity zdrojového i cílového účtu. Toto uzamčení zajistilo, že žádný jiný klient nemůže dotazovat nebo upravit stav jednoho účtu, dokud logika orchestrace neukončí kritickou část na konci using příkazu. Toto chování brání možnosti přečernění ze zdrojového účtu.

Poznámka:

Když se orchestrace ukončí ( obvykle nebo s chybou), všechny probíhající kritické části se implicitně ukončí a všechny zámky se uvolní.

Chování kritického oddílu

Metoda LockAsync vytvoří kritický oddíl v orchestraci. Tyto kritické části brání ostatním orchestracím v provádění překrývajících se změn v zadané sadě entit. Rozhraní API interně LockAsync odesílá entity operace "lock" a vrací se, když obdrží zprávu s odpovědí "zámek získaný" z každé z těchto stejných entit. Zámky i odemknutí jsou integrované operace podporované všemi entitami.

V entitě nejsou povoleny žádné operace od jiných klientů, zatímco jsou v uzamčeném stavu. Toto chování zajišťuje, že entitu může najednou uzamknout pouze jedna instance orchestrace. Pokud se volající pokusí vyvolat operaci u entity v době, kdy je uzamčena orchestrací, je tato operace umístěna ve frontě čekající operace. Dokud neudržování orchestrace uvolní zámek, nezpracují se žádné čekající operace.

Poznámka:

Toto chování se mírně liší od primitiv synchronizace používaných ve většině programovacích jazyků, jako lock je například příkaz v jazyce C#. Například v jazyce C# musí příkaz lock používat všechna vlákna, aby se zajistila správná synchronizace mezi více vlákny. Entity ale nevyžadují, aby všichni volající explicitně zamkli entitu. Pokud některý volající zamkne entitu, všechny ostatní operace s danou entitou se zablokují a zařadí do fronty.

Zámky na entitách jsou odolné, takže se zachovají i v případě, že se proces provádění recykluje. Zámky se interně uchovávají jako součást odolného stavu entity.

Na rozdíl od transakcí se kritické části automaticky nevrací změny, když dojde k chybám. Místo toho musí být jakékoli zpracování chyb, například vrácení zpět nebo opakování, explicitně zakódované, například zachycením chyb nebo výjimek. Tato volba návrhu je úmyslná. Automatické vrácení všech efektů orchestrace je obecně obtížné nebo nemožné, protože orchestrace můžou spouštět aktivity a volat externí služby, které se nedají vrátit zpět. Pokusy o vrácení zpět se mohou takélhat a vyžadovat další zpracování chyb.

Pravidla kritického oddílu

Na rozdíl od primitiv uzamčení nízké úrovně ve většině programovacích jazyků jsou důležité části zaručeny, že vzájemné zablokování nebude. Abychom zabránili zablokování, vynucujeme následující omezení:

  • Kritické oddíly nelze vnořit.
  • Kritické oddíly nemůžou vytvářet suborchestrace.
  • Kritické oddíly můžou volat pouze entity, které jsou uzamčené.
  • Kritické oddíly nemůžou volat stejnou entitu pomocí více paralelních volání.
  • Kritické části můžou signalizovat pouze entity, které nebyly uzamčeny.

Jakákoli porušení těchto pravidel způsobí chybu za běhu, například LockingRulesViolationException v .NET, která obsahuje zprávu, která vysvětluje, jaké pravidlo bylo přerušeno.

Porovnání s virtuálními aktéry

Mnohé z funkcí trvalých entit jsou inspirované modelem objektu actor. Pokud už znáte aktéry, možná poznáte řadu konceptů popsaných v tomto článku. Odolné entity se podobají virtuálním hercům nebo zrnům, které jsou populární projektem Orleans. Příklad:

  • Odolné entity jsou adresovatelné prostřednictvím ID entity.
  • Operace trvalých entit se spouštějí sériově po jednom, aby se zabránilo podmínkám časování.
  • Odolné entity se vytvářejí implicitně, když se volají nebo signalizují.
  • Odolné entity jsou při provádění operací bezobslužně uvolněny z paměti.

Je několik důležitých rozdílů, které stojí za zmínku:

  • Odolné entity upřednostňují odolnost před latencí, takže nemusí být vhodné pro aplikace s přísnými požadavky na latenci.
  • Odolné entity nemají předdefinované časové limity pro zprávy. V Orleans vyprší časový limit všech zpráv po konfigurovatelném čase. Výchozí hodnota je 30 sekund.
  • Zprávy odeslané mezi entitami jsou spolehlivě a v pořadí. Ve společnosti Orleans je pro obsah odesílaný prostřednictvím datových proudů podporován spolehlivý nebo objednaný doručování, ale není zaručeno pro všechny zprávy mezi zrnky.
  • Vzory odpovědí na požadavky v entitách jsou omezené na orchestrace. Z entit je povoleno pouze jednosměrné zasílání zpráv (označované také jako signalizace), stejně jako v původním modelu herce, a na rozdíl od zrn v Orleans.
  • Odolné entity nemají vzájemné zablokování. V Orleans může dojít k zablokování a nevyřešují se, dokud nedojde k vypršení časového limitu zpráv.
  • Odolné entity lze použít s trvalými orchestracemi a podporovat distribuované mechanismy uzamčení.

Další kroky