Entitetsfunktioner
Entitetsfunktioner definierar åtgärder för att läsa och uppdatera små delar av tillståndet, så kallade varaktiga entiteter. Precis som orkestreringsfunktioner är entitetsfunktioner funktioner med en särskild utlösartyp, entitetsutlösaren. Till skillnad från orkestreringsfunktioner hanterar entitetsfunktioner tillståndet för en entitet explicit i stället för att implicit representera tillstånd via kontrollflöde. Entiteter är ett sätt att skala ut program genom att distribuera arbetet mellan många entiteter, var och en med ett tillstånd av blygsam storlek.
Kommentar
Entitetsfunktioner och relaterade funktioner är endast tillgängliga i Durable Functions 2.0 och senare. De stöds för närvarande i .NET in-proc, .NET isolated worker, JavaScript och Python, men inte i PowerShell eller Java.
Viktigt!
Entitetsfunktioner stöds för närvarande inte i PowerShell och Java.
Allmänna begrepp
Entiteter beter sig lite som små tjänster som kommunicerar via meddelanden. Varje entitet har en unik identitet och ett internt tillstånd (om den finns). Som tjänster eller objekt utför entiteter åtgärder när de uppmanas att göra det. När en åtgärd körs kan den uppdatera entitetens interna tillstånd. Den kan också anropa externa tjänster och vänta på ett svar. Entiteter kommunicerar med andra entiteter, orkestreringar och klienter med hjälp av meddelanden som implicit skickas via tillförlitliga köer.
För att förhindra konflikter kommer alla åtgärder på en enda entitet garanterat att köras seriellt, det vill: en efter en.
Kommentar
När en entitet anropas bearbetas nyttolasten till slutförande och schemalägger sedan en ny körning för att aktivera när framtida indata tas emot. Därför kan entitetskörningsloggarna visa en extra körning efter varje entitetsanrop. detta förväntas.
Entitets-ID
Entiteter nås via en unik identifierare, entitets-ID :t. Ett entitets-ID är helt enkelt ett par strängar som unikt identifierar en entitetsinstans. Den består av en:
- Entitetsnamn, som är ett namn som identifierar typen av entitet. Ett exempel är "Counter". Det här namnet måste matcha namnet på entitetsfunktionen som implementerar entiteten. Det är inte känsligt för skiftläge.
- Entitetsnyckel, som är en sträng som unikt identifierar entiteten bland alla andra entiteter med samma namn. Ett exempel är ett GUID.
En entitetsfunktion Counter
kan till exempel användas för att hålla poäng i ett onlinespel. Varje instans av spelet har ett unikt entitets-ID, till exempel @Counter@Game1
och @Counter@Game2
. Alla åtgärder som riktar sig mot en viss entitet kräver att du anger ett entitets-ID som en parameter.
Entitetsåtgärder
Om du vill anropa en åtgärd på en entitet anger du följande:
- Entitets-ID för målentiteten.
- Åtgärdsnamn, som är en sträng som anger vilken åtgärd som ska utföras. Entiteten
Counter
kan till exempel ha stödadd
för ,get
ellerreset
åtgärder. - Åtgärdsindata, som är en valfri indataparameter för åtgärden. Add-åtgärden kan till exempel ta en heltalsmängd som indata.
- Schemalagd tid, vilket är en valfri parameter för att ange leveranstiden för åtgärden. En åtgärd kan till exempel schemaläggas på ett tillförlitligt sätt så att den körs flera dagar i framtiden.
Åtgärder kan returnera ett resultatvärde eller ett felresultat, till exempel ett JavaScript-fel eller ett .NET-undantag. Det här resultatet eller felet uppstår i orkestreringar som kallas åtgärden.
En entitetsåtgärd kan också skapa, läsa, uppdatera och ta bort tillståndet för entiteten. Tillståndet för entiteten bevaras alltid i lagringen.
Definiera entiteter
Du definierar entiteter med hjälp av en funktionsbaserad syntax, där entiteter representeras som funktioner och åtgärder uttryckligen skickas av programmet.
För närvarande finns det två distinkta API:er för att definiera entiteter i .NET:
När du använder en funktionsbaserad syntax representeras entiteter som funktioner och åtgärder uttryckligen skickas av programmet. Den här syntaxen fungerar bra för entiteter med enkelt tillstånd, få åtgärder eller en dynamisk uppsättning åtgärder som i programramverk. Den här syntaxen kan vara omständlig att underhålla eftersom den inte fångar upp typfel vid kompileringstillfället.
De specifika API:erna beror på om C#-funktionerna körs i en isolerad arbetsprocess (rekommenderas) eller i samma process som värden.
Följande kod är ett exempel på en enkel Counter
entitet som implementeras som en varaktig funktion. Den här funktionen definierar tre åtgärder, add
, reset
och , som var och get
en fungerar i ett heltalstillstånd.
[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;
}
}
Mer information om den funktionsbaserade syntaxen och hur du använder den finns i Funktionsbaserad syntax.
Varaktiga entiteter är tillgängliga i JavaScript från och med version 1.3.0 av durable-functions
npm-paketet. Följande kod är entiteten Counter
implementerad som en varaktig funktion skriven i JavaScript.
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;
}
});
Kommentar
Mer information om hur V2-modellen fungerar finns i utvecklarguiden för Azure Functions Python.
Följande kod är entiteten Counter
implementerad som en varaktig funktion skriven i Python.
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)
Åtkomstentiteter
Entiteter kan nås med enkelriktad eller dubbelriktad kommunikation. Följande terminologi skiljer de två kommunikationsformerna åt:
- Att anropa en entitet använder dubbelriktad kommunikation (tur och retur). Du skickar ett åtgärdsmeddelande till entiteten och väntar sedan på svarsmeddelandet innan du fortsätter. Svarsmeddelandet kan ge ett resultatvärde eller ett felresultat, till exempel ett JavaScript-fel eller ett .NET-undantag. Det här resultatet eller felet observeras sedan av anroparen.
- Signalering av en entitet använder enkelriktad kommunikation (brand och glöm). Du skickar ett åtgärdsmeddelande men väntar inte på ett svar. Meddelandet kommer garanterat att levereras så småningom, men avsändaren vet inte när och kan inte observera några resultatvärden eller fel.
Entiteter kan nås inifrån klientfunktioner, från orkestreringsfunktioner eller inifrån entitetsfunktioner. Alla former av kommunikation stöds inte av alla kontexter:
- Inifrån klienter kan du signalera entiteter och du kan läsa entitetstillståndet.
- Inifrån orkestreringar kan du signalera entiteter och du kan anropa entiteter.
- Inifrån entiteter kan du signalera entiteter.
Följande exempel illustrerar dessa olika sätt att komma åt entiteter.
Exempel: Klienten signalerar en entitet
Om du vill komma åt entiteter från en vanlig Azure-funktion, som även kallas för en klientfunktion, använder du entitetsklientbindningen. I följande exempel visas en köutlöst funktion som signalerar en entitet med hjälp av den här bindningen.
Kommentar
För enkelhetens skull visar följande exempel den löst inskrivna syntaxen för åtkomst till entiteter. I allmänhet rekommenderar vi att du kommer åt entiteter via gränssnitt eftersom det ger mer typkontroll.
[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)
Termen signal innebär att entitets-API-anropet är enkelriktad och asynkron. Det är inte möjligt för en klientfunktion att veta när entiteten har bearbetat åtgärden. Dessutom kan klientfunktionen inte observera några resultatvärden eller undantag.
Exempel: Klienten läser ett entitetstillstånd
Klientfunktioner kan också köra frågor mot tillståndet för en entitet, som du ser i följande exempel:
[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)
Frågor om entitetstillstånd skickas till Durable-spårningsarkivet och returnerar entitetens senast bevarade tillstånd. Det här tillståndet är alltid ett "bekräftat" tillstånd, det vill säga att det aldrig är ett tillfälligt mellanliggande tillstånd som antas i mitten av körningen av en åtgärd. Det är dock möjligt att det här tillståndet är inaktuellt jämfört med entitetens minnesinterna tillstånd. Endast orkestreringar kan läsa en entitets minnesinterna tillstånd enligt beskrivningen i följande avsnitt.
Exempel: Orkestreringssignaler och anropar en entitet
Orchestrator-funktioner kan komma åt entiteter med hjälp av API:er på orkestreringsutlösarbindningen. Följande exempelkod visar en orchestrator-funktion som anropar och signalerar en entitet 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");
});
Kommentar
JavaScript stöder för närvarande inte signalering av en entitet från en orkestrerare. Använd callEntity
i stället.
@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
Endast orkestreringar kan anropa entiteter och få ett svar, vilket kan vara antingen ett returvärde eller ett undantag. Klientfunktioner som använder klientbindningen kan bara signalera entiteter.
Kommentar
Att anropa en entitet från en orkestreringsfunktion liknar att anropa en aktivitetsfunktion från en orkestreringsfunktion. Den största skillnaden är att entitetsfunktioner är varaktiga objekt med en adress, vilket är entitets-ID. Entitetsfunktioner har stöd för att ange ett åtgärdsnamn. Aktivitetsfunktioner är å andra sidan tillståndslösa och har inte begreppet åtgärder.
Exempel: Entitet signalerar en entitet
En entitetsfunktion kan skicka signaler till andra entiteter, eller till och med till sig själv, medan den kör en åtgärd.
Vi kan till exempel ändra det tidigare entitetsexemplet Counter
så att det skickar en "milestone-reached"-signal till en övervakarentitet när räknaren når värdet 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;
Kommentar
Python stöder inte entitets-till-entitetssignaler ännu. Använd en orkestrerare för att signalera entiteter i stället.
Entitetssamordning
Det kan finnas tillfällen då du behöver samordna åtgärder mellan flera entiteter. I ett bankprogram kan du till exempel ha entiteter som representerar enskilda bankkonton. När du överför pengar från ett konto till ett annat måste du se till att källkontot har tillräckliga medel. Du måste också se till att uppdateringar av både käll- och målkontona görs på ett transaktionsmässigt konsekvent sätt.
Exempel: Överföra medel
I följande exempel överförs pengar mellan två kontoentiteter med hjälp av en orchestrator-funktion. Samordning av entitetsuppdateringar kräver att du använder LockAsync
metoden för att skapa ett kritiskt avsnitt i orkestreringen.
Kommentar
För enkelhetens skull återanvänder det här exemplet den Counter
entitet som definierats tidigare. I ett verkligt program skulle det vara bättre att definiera en mer detaljerad BankAccount
entitet.
// 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;
}
}
}
I .NET LockAsync
returnerar IDisposable
, vilket avslutar det kritiska avsnittet när det tas bort. Det här IDisposable
resultatet kan användas tillsammans med ett using
block för att få en syntaktisk representation av det kritiska avsnittet.
I föregående exempel överför en orchestrator-funktion medel från en källentitet till en målentitet. Metoden LockAsync
låste både käll- och målkontottiteterna. Den här låsningen säkerställde att ingen annan klient kunde fråga eller ändra tillståndet för något av kontona förrän orkestreringslogik avslutade det kritiska avsnittet i slutet av -instruktionen using
. Det här beteendet förhindrar möjligheten att övertrassera från källkontot.
Kommentar
När en orkestrering avslutas, antingen normalt eller med ett fel, avslutas alla pågående kritiska avsnitt implicit och alla lås frigörs.
Kritiskt avsnittsbeteende
Metoden LockAsync
skapar ett kritiskt avsnitt i en orkestrering. Dessa kritiska avsnitt hindrar andra orkestreringar från att göra överlappande ändringar i en angiven uppsättning entiteter. Internt skickar API:et LockAsync
"lås"-åtgärder till entiteterna och returnerar när det tar emot ett svarsmeddelande om "låsförvärvat" från var och en av dessa entiteter. Både lås och upplåsning är inbyggda åtgärder som stöds av alla entiteter.
Inga åtgärder från andra klienter tillåts på en entitet medan den är i ett låst tillstånd. Det här beteendet säkerställer att endast en orkestreringsinstans kan låsa en entitet i taget. Om en anropare försöker anropa en åtgärd på en entitet medan den är låst av en orkestrering placeras åtgärden i en väntande åtgärdskö. Inga väntande åtgärder bearbetas förrän efter att innehavsorkestreringen släpper låset.
Kommentar
Det här beteendet skiljer sig något från synkroniseringsprivilegier som används i de flesta programmeringsspråk, till exempel -instruktionen lock
i C#. I C# måste instruktionen lock
till exempel användas av alla trådar för att säkerställa korrekt synkronisering över flera trådar. Entiteter kräver dock inte att alla anropare uttryckligen låser en entitet. Om någon anropare låser en entitet blockeras alla andra åtgärder på den entiteten och placeras i kö bakom låset.
Lås på entiteter är varaktiga, så de bevaras även om körningsprocessen återanvänds. Lås sparas internt som en del av en entitets varaktiga tillstånd.
Till skillnad från transaktioner återställer kritiska avsnitt inte automatiskt ändringar när fel inträffar. I stället måste all felhantering, till exempel återställning eller återförsök, uttryckligen kodas, till exempel genom att fel eller undantag fångas upp. Det här designvalet är avsiktligt. Att automatiskt återställa alla effekter av en orkestrering är svårt eller omöjligt i allmänhet, eftersom orkestreringar kan köra aktiviteter och göra anrop till externa tjänster som inte kan återställas. Dessutom kan försök att återställa själva misslyckas och kräva ytterligare felhantering.
Regler för kritiska avsnitt
Till skillnad från lågnivålåsningspri primitiver i de flesta programmeringsspråk garanteras kritiska avsnitt att inte blockeras. För att förhindra dödlägen tillämpar vi följande begränsningar:
- Kritiska avsnitt kan inte kapslas.
- Kritiska avsnitt kan inte skapa understrängar.
- Kritiska avsnitt kan bara anropa entiteter som de har låst.
- Kritiska avsnitt kan inte anropa samma entitet med flera parallella anrop.
- Kritiska avsnitt kan bara signalera entiteter som de inte har låst.
Eventuella överträdelser av dessa regler orsakar ett körningsfel, till exempel LockingRulesViolationException
i .NET, som innehåller ett meddelande som förklarar vilken regel som bröts.
Jämförelse med virtuella aktörer
Många av de hållbara entitetsfunktionerna är inspirerade av aktörsmodellen. Om du redan är bekant med aktörer kanske du känner igen många av de begrepp som beskrivs i den här artikeln. Varaktiga entiteter liknar virtuella aktörer, eller korn, som populariseras av Orleans-projektet. Till exempel:
- Varaktiga entiteter kan adresseras via ett entitets-ID.
- Varaktiga entitetsåtgärder körs seriellt, en i taget, för att förhindra konkurrensförhållanden.
- Varaktiga entiteter skapas implicit när de anropas eller signaleras.
- Varaktiga entiteter tas tyst bort från minnet när åtgärder inte körs.
Det finns några viktiga skillnader som är värda att notera:
- Varaktiga entiteter prioriterar hållbarhet framför svarstid, så det kanske inte är lämpligt för program med strikta svarstidskrav.
- Varaktiga entiteter har inte inbyggda tidsgränser för meddelanden. I Orleans överskrider alla meddelanden tidsgränsen efter en konfigurerbar tid. Standardvärdet är 30 sekunder.
- Meddelanden som skickas mellan entiteter levereras tillförlitligt och i ordning. I Orleans stöds tillförlitlig eller ordnad leverans för innehåll som skickas via strömmar, men garanteras inte för alla meddelanden mellan korn.
- Mönster för begärandesvar i entiteter är begränsade till orkestreringar. Inifrån entiteter tillåts endast enkelriktade meddelanden (kallas även signalering), som i den ursprungliga aktörsmodellen, och till skillnad från korn i Orleans.
- Varaktiga entiteter är inte låsta. I Orleans kan dödlägen inträffa och inte lösas förrän tidsgränsen för meddelanden överskrids.
- Hållbara entiteter kan användas med hållbara orkestreringar och stöd för distribuerade låsmekanismer.