Funkcje jednostki
Funkcje jednostki definiują operacje odczytu i aktualizowania małych fragmentów stanu, nazywanych trwałymi jednostkami. Podobnie jak funkcje orkiestratora, funkcje jednostki to funkcje ze specjalnym typem wyzwalacza, wyzwalaczem jednostki. W przeciwieństwie do funkcji orkiestratora funkcje jednostki jawnie zarządzają stanem jednostki, a nie niejawnie reprezentują stan za pośrednictwem przepływu sterowania. Jednostki umożliwiają skalowanie aplikacji w poziomie przez dystrybucję pracy między wieloma jednostkami, z których każda ma skromny stan.
Uwaga
Funkcje jednostki i powiązane funkcje są dostępne tylko w rozszerzeniach Durable Functions 2.0 i nowszych. Są one obecnie obsługiwane w środowisku .NET in-proc, izolowanym procesem roboczym platformy .NET, językiem JavaScript i językiem Python, ale nie w programie PowerShell lub języku Java. Ponadto funkcje jednostek dla platformy .NET Isolated są obsługiwane w przypadku korzystania z dostawców stanu usługi Azure Storage lub Netherite, ale nie w przypadku korzystania z dostawcy stanu MSSQL.
Ważne
Funkcje jednostek nie są obecnie obsługiwane w programach PowerShell i Java.
Ogólne pojęcia
Jednostki zachowują się trochę jak małe usługi komunikujące się za pośrednictwem komunikatów. Każda jednostka ma unikatową tożsamość i stan wewnętrzny (jeśli istnieje). Podobnie jak w przypadku usług lub obiektów, jednostki wykonują operacje po wyświetleniu monitu. Po wykonaniu operacji może ona zaktualizować stan wewnętrzny jednostki. Może również wywoływać usługi zewnętrzne i czekać na odpowiedź. Jednostki komunikują się z innymi jednostkami, aranżacjami i klientami przy użyciu komunikatów, które są niejawnie wysyłane za pośrednictwem niezawodnych kolejek.
Aby zapobiec konfliktom, wszystkie operacje na jednej jednostce są gwarantowane do wykonania szeregowo, czyli jeden po drugim.
Uwaga
Po wywołaniu jednostki przetwarza ładunek do ukończenia, a następnie planuje nowe wykonanie, aby aktywować po nadejściu przyszłych danych wejściowych. W związku z tym dzienniki wykonywania jednostki mogą wyświetlać dodatkowe wykonanie po wywołaniu każdej jednostki; jest to oczekiwane.
Identyfikator encji
Dostęp do jednostek jest uzyskiwany za pośrednictwem unikatowego identyfikatora, identyfikatora jednostki. Identyfikator jednostki to po prostu para ciągów, które jednoznacznie identyfikują wystąpienie jednostki. Składa się z:
- Nazwa jednostki, która jest nazwą identyfikującą typ jednostki. Przykładem jest "Licznik". Ta nazwa musi być zgodna z nazwą funkcji jednostki, która implementuje jednostkę. Nie jest wrażliwa na wielkość liter.
- Klucz jednostki, który jest ciągiem, który jednoznacznie identyfikuje jednostkę między wszystkimi innymi jednostkami o tej samej nazwie. Przykładem jest identyfikator GUID.
Na przykład Counter
funkcja jednostki może służyć do utrzymywania wyników w grze online. Każde wystąpienie gry ma unikatowy identyfikator jednostki, taki jak @Counter@Game1
i @Counter@Game2
. Wszystkie operacje przeznaczone dla określonej jednostki wymagają określenia identyfikatora jednostki jako parametru.
Operacje jednostek
Aby wywołać operację w jednostce, określ następujące elementy:
- Identyfikator jednostki docelowej.
- Nazwa operacji, która jest ciągiem określającym operację do wykonania. Na przykład
Counter
jednostka może obsługiwaćadd
operacje ,get
lubreset
. - Dane wejściowe operacji, które są opcjonalnym parametrem wejściowym dla operacji. Na przykład operacja dodawania może przyjmować liczbę całkowitą jako dane wejściowe.
- Zaplanowany czas, który jest opcjonalnym parametrem służącym do określania czasu dostarczania operacji. Na przykład operacja może być niezawodnie zaplanowana na kilka dni w przyszłości.
Operacje mogą zwracać wartość wyniku lub wynik błędu, na przykład błąd JavaScript lub wyjątek platformy .NET. Ten wynik lub błąd występuje w aranżacjach, które nazwały operację.
Operacja jednostki może również tworzyć, odczytywać, aktualizować i usuwać stan jednostki. Stan jednostki jest zawsze trwały w magazynie.
Definiowanie jednostek
Jednostki są definiowane przy użyciu składni opartej na funkcji, gdzie jednostki są reprezentowane jako funkcje i operacje są jawnie wysyłane przez aplikację.
Obecnie istnieją dwa odrębne interfejsy API do definiowania jednostek na platformie .NET:
W przypadku korzystania ze składni opartej na funkcji jednostki są reprezentowane jako funkcje i operacje są jawnie wysyłane przez aplikację. Ta składnia działa dobrze w przypadku jednostek o prostym stanie, kilku operacjach lub dynamicznym zestawie operacji, takich jak w strukturach aplikacji. Ta składnia nie może być żmudna do utrzymania, ponieważ nie przechwytuje błędów typu w czasie kompilacji.
Określone interfejsy API zależą od tego, czy funkcje języka C# działają w izolowanym procesie roboczym (zalecanym) czy w tym samym procesie co host.
Poniższy kod jest przykładem prostej Counter
jednostki zaimplementowanej jako trwałej funkcji. Ta funkcja definiuje trzy operacje, add
, reset
i get
, z których każda działa w stanie liczby całkowitej.
[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;
}
}
Aby uzyskać więcej informacji na temat składni opartej na funkcji i sposobu jej używania, zobacz Składnia oparta na funkcjach.
Jednostki trwałe są dostępne w języku JavaScript, począwszy od wersji 1.3.0 durable-functions
pakietu npm. Poniższy kod to Counter
jednostka zaimplementowana jako trwała funkcja napisana w języku JavaScript.
Licznik/function.json
{
"bindings": [
{
"name": "context",
"type": "entityTrigger",
"direction": "in"
}
],
"disabled": false
}
Licznik/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;
}
});
Uwaga
Aby uzyskać więcej informacji na temat sposobu działania modelu w wersji 2, zapoznaj się z przewodnikiem dla deweloperów języka Python w usłudze Azure Functions.
Poniższy kod to Counter
jednostka zaimplementowana jako trwała funkcja napisana w języku 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)
Uzyskiwanie dostępu do jednostek
Dostęp do jednostek można uzyskać przy użyciu komunikacji jednokierunkowej lub dwukierunkowej. Następująca terminologia rozróżnia dwie formy komunikacji:
- Wywoływanie jednostki używa komunikacji dwukierunkowej (dwukierunkowej). Przed kontynuowaniem wysyłasz komunikat operacji do jednostki, a następnie czekasz na komunikat odpowiedzi. Komunikat odpowiedzi może dostarczyć wartość wyniku lub wynik błędu, taki jak błąd języka JavaScript lub wyjątek platformy .NET. Ten wynik lub błąd jest następnie obserwowany przez obiekt wywołujący.
- Sygnalizowanie jednostki używa komunikacji jednokierunkowej (fire and forget). Wysyłasz komunikat operacji, ale nie czekaj na odpowiedź. Chociaż komunikat jest gwarantowany do dostarczenia w końcu, nadawca nie wie, kiedy i nie może obserwować żadnych wartości wyników ani błędów.
Dostęp do jednostek można uzyskać z poziomu funkcji klienta, z funkcji orkiestratora lub z funkcji jednostki. Nie wszystkie formy komunikacji są obsługiwane przez wszystkie konteksty:
- Z poziomu klientów można sygnalizować jednostki i odczytywać stan jednostki.
- Z poziomu orkiestracji można sygnalizować jednostki i wywoływać jednostki.
- Z poziomu jednostek można sygnalizować jednostki.
Poniższe przykłady ilustrują te różne sposoby uzyskiwania dostępu do jednostek.
Przykład: klient sygnalizuje jednostkę
Aby uzyskać dostęp do jednostek ze zwykłej funkcji platformy Azure, która jest również nazywana funkcją klienta, użyj powiązania klienta jednostki. W poniższym przykładzie pokazano funkcję wyzwalaną przez kolejkę sygnalizując jednostkę przy użyciu tego powiązania.
Uwaga
Dla uproszczenia w poniższych przykładach przedstawiono luźno typizowane składnię uzyskiwania dostępu do jednostek. Ogólnie rzecz biorąc, zalecamy uzyskiwanie dostępu do jednostek za pośrednictwem interfejsów , ponieważ zapewnia on więcej sprawdzania typów.
[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)
Termin sygnał oznacza, że wywołanie interfejsu API jednostki jest jednokierunkowe i asynchroniczne. Nie jest możliwe, aby funkcja klienta wiedziała, kiedy jednostka przetworzyła operację. Ponadto funkcja klienta nie może obserwować żadnych wartości wyników ani wyjątków.
Przykład: Klient odczytuje stan jednostki
Funkcje klienta mogą również wykonywać zapytania dotyczące stanu jednostki, jak pokazano w poniższym przykładzie:
[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)
Zapytania o stan jednostki są wysyłane do magazynu śledzenia trwałej i zwracają ostatnio utrwalone stany jednostki. Ten stan jest zawsze stanem "zatwierdzonym", czyli nigdy nie jest tymczasowym stanem pośrednim przyjmowanym w trakcie wykonywania operacji. Istnieje jednak możliwość, że ten stan jest nieaktualny w porównaniu ze stanem w pamięci jednostki. Tylko aranżacje mogą odczytywać stan w pamięci jednostki, zgodnie z opisem w poniższej sekcji.
Przykład: orkiestracja sygnałów i wywoływanie jednostki
Funkcje programu Orchestrator mogą uzyskiwać dostęp do jednostek przy użyciu interfejsów API w powiązaniu wyzwalacza aranżacji. Poniższy przykładowy kod przedstawia funkcję orkiestratora wywołującą i sygnalizując Counter
jednostkę.
[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");
});
Uwaga
Język JavaScript nie obsługuje obecnie sygnalizowania jednostki z orkiestratora. Użycie w zamian parametru 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
Tylko aranżacje mogą wywoływać jednostki i otrzymywać odpowiedź, co może być wartością zwracaną lub wyjątkiem. Funkcje klienta korzystające z powiązania klienta mogą sygnalizować tylko jednostki.
Uwaga
Wywoływanie jednostki z funkcji orkiestratora jest podobne do wywoływania funkcji działania z funkcji orkiestratora. Główną różnicą jest to, że funkcje jednostek są trwałymi obiektami o adresie, który jest identyfikatorem jednostki. Funkcje jednostki obsługują określanie nazwy operacji. Z drugiej strony funkcje działania są bezstanowe i nie mają pojęcia operacji.
Przykład: jednostka sygnalizuje jednostkę
Funkcja jednostki może wysyłać sygnały do innych jednostek, a nawet sama, podczas wykonywania operacji.
Na przykład możemy zmodyfikować poprzedni Counter
przykład jednostki, tak aby wysyłał sygnał "osiągnięto punkt kontrolny" do jednostki monitora, gdy licznik osiągnie wartość 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;
Uwaga
Język Python nie obsługuje jeszcze sygnałów jednostki do jednostki. Zamiast tego użyj orkiestratora do sygnalizowania jednostek.
Koordynacja jednostek
Czasami konieczne może być koordynowanie operacji między wieloma jednostkami. Na przykład w aplikacji bankowej mogą istnieć jednostki reprezentujące indywidualne konta bankowe. Podczas przenoszenia funduszy z jednego konta do innego należy upewnić się, że konto źródłowe ma wystarczające środki. Należy również upewnić się, że aktualizacje kont źródłowych i docelowych są wykonywane w sposób spójny transakcyjnie.
Przykład: Transfer funduszy
Poniższy przykładowy kod przenosi środki między dwiema jednostkami kont przy użyciu funkcji orkiestratora. Koordynowanie aktualizacji jednostek wymaga użycia LockAsync
metody w celu utworzenia sekcji krytycznej w aranżacji.
Uwaga
Dla uproszczenia w tym przykładzie użyto wcześniej zdefiniowanej Counter
jednostki. W rzeczywistej aplikacji lepiej byłoby zdefiniować bardziej szczegółową BankAccount
jednostkę.
// 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;
}
}
}
Na platformie .NET LockAsync
zwraca wartość IDisposable
, która kończy sekcję krytyczną po usunięciu. Tego IDisposable
wyniku można użyć razem z blokiem using
, aby uzyskać składniową reprezentację sekcji krytycznej.
W poprzednim przykładzie funkcja orkiestratora przenosi fundusze z jednostki źródłowej do jednostki docelowej. Metoda LockAsync
zablokowała zarówno jednostki konta źródłowego, jak i docelowego. Ta blokada gwarantuje, że żaden inny klient nie może wykonywać zapytań ani modyfikować stanu konta, dopóki logika aranżacji nie zakończy sekcji krytycznej na końcu using
instrukcji. To zachowanie uniemożliwia przekroczenie rachunku w rachunku źródłowym.
Uwaga
Po zakończeniu aranżacji, zwykle lub z błędem, wszystkie sekcje krytyczne w toku są niejawnie zakończone i wszystkie blokady są zwalniane.
Zachowanie sekcji krytycznej
Metoda LockAsync
tworzy sekcję krytyczną w aranżacji. Te sekcje krytyczne uniemożliwiają innym orkiestracji wprowadzanie nakładających się zmian w określonym zestawie jednostek. LockAsync
Wewnętrznie interfejs API wysyła operacje "lock" do jednostek i zwraca, gdy otrzyma komunikat odpowiedzi "lock acquired" z każdej z tych samych jednostek. Zarówno blokada, jak i odblokowywanie to wbudowane operacje obsługiwane przez wszystkie jednostki.
W jednostce nie są dozwolone żadne operacje od innych klientów, gdy są w stanie zablokowanym. To zachowanie gwarantuje, że tylko jedno wystąpienie orkiestracji może zablokować jednostkę naraz. Jeśli obiekt wywołujący próbuje wywołać operację na jednostce, gdy jest ona zablokowana przez orkiestrację, operacja ta jest umieszczana w oczekującej kolejce operacji. Żadne oczekujące operacje nie są przetwarzane do momentu wydania blokady orkiestracji holdingu.
Uwaga
To zachowanie różni się nieco od typów pierwotnych synchronizacji używanych w większości języków programowania, takich jak lock
instrukcja w języku C#. Na przykład w języku C#instrukcja lock
musi być używana przez wszystkie wątki, aby zapewnić właściwą synchronizację w wielu wątkach. Jednostki nie wymagają jednak od wszystkich wywołujących jawnego blokowania jednostki. Jeśli dowolny obiekt wywołujący zablokuje jednostkę, wszystkie inne operacje na tej jednostce są blokowane i kolejkowane za tym blokadą.
Blokady na jednostkach są trwałe, więc utrzymują się nawet wtedy, gdy proces wykonywania jest poddawany recyklingu. Blokady są utrwalane wewnętrznie w ramach stanu trwałego jednostki.
W przeciwieństwie do transakcji sekcje krytyczne nie automatycznie cofają zmian w przypadku wystąpienia błędów. Zamiast tego każda obsługa błędów, taka jak wycofywanie lub ponawianie prób, musi być jawnie zakodowana, na przykład przez przechwycenie błędów lub wyjątków. Ten wybór projektu jest zamierzony. Automatyczne wycofywanie wszystkich efektów orkiestracji jest trudne lub niemożliwe w ogóle, ponieważ orkiestracje mogą uruchamiać działania i wykonywać wywołania usług zewnętrznych, których nie można wycofać. Ponadto próby wycofania mogą samodzielnie zakończyć się niepowodzeniem i wymagać dalszej obsługi błędów.
Reguły sekcji krytycznej
W przeciwieństwie do pierwotnych blokowania niskiego poziomu w większości języków programowania, sekcje krytyczne nie są gwarantowane do zakleszczenia. Aby zapobiec zakleszczeniom, wymuszamy następujące ograniczenia:
- Nie można zagnieżdżać sekcji krytycznych.
- Sekcje krytyczne nie mogą tworzyć podorchestracji.
- Sekcje krytyczne mogą wywoływać tylko zablokowane jednostki.
- Sekcje krytyczne nie mogą wywoływać tej samej jednostki przy użyciu wielu wywołań równoległych.
- Sekcje krytyczne mogą sygnalizować tylko jednostki, których nie zablokowano.
Wszelkie naruszenia tych reguł powodują błąd środowiska uruchomieniowego, na przykład LockingRulesViolationException
na platformie .NET, który zawiera komunikat wyjaśniający, która reguła została uszkodzona.
Porównanie z aktorami wirtualnymi
Wiele cech trwałych jednostek jest inspirowanych modelem aktora. Jeśli znasz już aktorów, możesz rozpoznać wiele pojęć opisanych w tym artykule. Trwałe jednostki są podobne do wirtualnych aktorów lub ziarna, które są spopularyzowane przez projekt Orlean. Na przykład:
- Jednostki trwałe są adresowane za pośrednictwem identyfikatora jednostki.
- Operacje jednostek trwałych są wykonywane szeregowo, pojedynczo, aby zapobiec warunkom wyścigu.
- Jednostki trwałe są tworzone niejawnie, gdy są wywoływane lub sygnalizowane.
- Jednostki trwałe są w trybie dyskretnym zwalniane z pamięci, gdy nie są wykonywane operacje.
Istnieją pewne ważne różnice, które warto zauważyć:
- Jednostki trwałe priorytetują trwałość w przypadku opóźnień i dlatego mogą nie być odpowiednie dla aplikacji ze ścisłymi wymaganiami dotyczącymi opóźnień.
- Jednostki trwałe nie mają wbudowanych limitów czasu dla komunikatów. W Orleanie wszystkie komunikaty upłynął po konfigurowalnym czasie. Wartość domyślna to 30 sekund.
- Komunikaty wysyłane między jednostkami są dostarczane niezawodnie i w kolejności. W Orleanie niezawodne lub uporządkowane dostarczanie jest obsługiwane w przypadku zawartości wysyłanej za pośrednictwem strumieni, ale nie jest gwarantowane dla wszystkich komunikatów między ziarnami.
- Wzorce żądań odpowiedzi w jednostkach są ograniczone do aranżacji. Z poziomu jednostek dozwolone jest tylko jednokierunkowe przesyłanie komunikatów (znane również jako sygnalizowanie), jak w oryginalnym modelu aktora i w przeciwieństwie do ziarna w Orleanie.
- Jednostki trwałe nie zakleszczą się. W Orleanie mogą wystąpić zakleszczenia i nie rozwiązywać, dopóki komunikaty nie upłynął limit czasu.
- Jednostki trwałe mogą być używane z trwałymi orkiestracjami i obsługiwać rozproszone mechanizmy blokowania.