Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Entitätsfunktionen definieren Vorgänge, die kleine Teile des Zustands lesen und aktualisieren, die als dauerhafte Entitäten bezeichnet werden. Wie Orchestratorfunktionen verwenden Entitätsfunktionen einen speziellen Triggertyp, der als Entitätstrigger bezeichnet wird. Im Gegensatz zu Orchestratorfunktionen verwalten Entitätsfunktionen den Entitätsstatus explizit, anstatt den Zustand über den Steuerungsfluss darzustellen. Entitäten helfen Ihnen, Anwendungen zu skalieren, indem sie die Arbeit auf viele Entitäten verteilen, von denen jede einen überschaubaren Zustand aufweist.
Hinweis
Entitätsfunktionen und zugehörige Features sind in Durable Functions 2.0 und höher verfügbar. Sie werden in .NET In-Proc, .NET isolierter Worker, JavaScript und Python unterstützt, jedoch nicht in PowerShell oder Java.
Entitäten definieren Vorgänge, die kleine Teile des Zustands lesen und aktualisieren, die als dauerhafte Entitäten bezeichnet werden. Im Gegensatz zu Orchestratoren verwalten Entitäten den Zustand explizit, anstatt den Zustand über den Steuerungsfluss darzustellen. Entitäten helfen Ihnen, Anwendungen zu skalieren, indem sie die Arbeit auf viele Entitäten verteilen, von denen jede einen überschaubaren Zustand aufweist.
Hinweis
Dauerhafte Entitäten werden in den .NET und Python Durable Task SDKs unterstützt. Das Java SDK unterstützt keine Entitäten.
Allgemeine Konzepte
Entitäten fungieren wie kleine Dienste, die mithilfe von Nachrichten kommunizieren. Jede Entität verfügt über eine eindeutige Identität und bei Bedarf einen internen Zustand. Entitäten führen Operationen aus, wenn sie dazu aufgefordert werden. Ein Vorgang kann den Status aktualisieren, externe Dienste aufrufen oder auf eine Antwort warten. Entitäten kommunizieren mit anderen Entitäten, Orchestrierungen und Clients über Nachrichten, die die Laufzeitumgebung über zuverlässige Warteschlangen sendet.
Um Konflikte zu verhindern, führt eine einzelne Entität vorgänge fortlaufend aus, und zwar nacheinander.
Hinweis
Wenn Sie eine Entität aufrufen, verarbeitet sie die Nutzlast bis zum Abschluss und plant dann eine neue Ausführung, die beim Eintreffen neuer Eingaben aktiviert wird. Daher können ihre Entitätsausführungsprotokolle nach jedem Aufruf eine zusätzliche Ausführung anzeigen. Das wird erwartet.
Entitäts-ID
Verwenden Sie die Entitäts-ID , um auf eine Entität zuzugreifen. Eine Entitäts-ID ist ein Zeichenfolgenpaar, das eine Entitätsinstanz eindeutig identifiziert. Sie besteht aus:
-
Entitätsname, der den Entitätstyp identifiziert. Beispiel:
Counter. Dieser Name entspricht dem Namen der Entitätsfunktion, die die Entität implementiert. Die Groß-/Kleinschreibung wird nicht beachtet. - Entitätsschlüssel, der die Entität unter allen anderen Entitäten mit demselben Namen eindeutig identifiziert. Beispielsweise eine GUID (Globally Unique Identifier).
Beispielsweise kann eine Counter Entitätsfunktion verwendet werden, um den Punktestand in einem Onlinespiel zu halten. Jede Instanz des Spiels verfügt über eine eindeutige Entitäts-ID, wie z. B. @Counter@Game1 und @Counter@Game2. Geben Sie die Entitäts-ID an, um eine Entität als Ziel festzulegen.
Entitätsvorgänge
Um einen Vorgang für eine Entität aufzurufen, geben Sie Folgendes an:
- Entitäts-ID der Zielentität.
-
Vorgangsname, bei dem es sich um eine Zeichenfolge handelt, die den auszuführenden Vorgang angibt. Die
Counter-Entität könnte zum Beispieladd,getoderresetVorgänge unterstützen. -
Vorgangseingabe, bei der es sich um einen optionalen Parameter für den Vorgang handelt. Beispielsweise nimmt der
addVorgang eine ganze Zahl als Eingabe an. - Geplante Zeit, bei der es sich um einen optionalen Parameter handelt, um die Lieferzeit des Vorgangs anzugeben. Planen Sie beispielsweise einen Vorgang, der mehrere Tage später ausgeführt werden soll.
Vorgänge können einen Ergebniswert oder ein Fehlerergebnis zurückgeben, z. B. einen JavaScript-Fehler oder eine .NET Ausnahme. Die Orchestrierung, die den Aufruf vornimmt, empfängt das Ergebnis oder den Fehler.
Vorgänge können einen Ergebniswert oder ein Fehlerergebnis zurückgeben, z. B. eine .NET Ausnahme oder Python Ausnahme. Der Aufrufer empfängt das Ergebnis oder den Fehler.
Entitätsvorgänge können auch den Zustand der Entität erstellen, lesen, aktualisieren und löschen. Die Laufzeit behält immer den Entitätsstatus im Speicher bei.
Definieren von Entitäten
Verwenden Sie eine von zwei APIs, um Entitäten in .NET zu definieren:
Funktionsbasierte Syntax: In funktionsbasierter Syntax schreiben Sie jede Entität als Funktion und Verteilervorgänge in Ihrer App. Diese Syntax eignet sich gut für Entitäten mit einfachem Zustand, wenigen Vorgängen oder einer dynamischen Gruppe von Vorgängen, z. B. in App-Frameworks. Es kann jedoch mühsam sein, zu pflegen, da es zur Kompilierungszeit keine Typfehler abfangen kann.
Klassenbasierte Syntax: In klassenbasierter Syntax .NET Klassen und Methoden modellieren Entitäten und Vorgänge. Diese Syntax erleichtert das Lesen von Code und ermöglicht ihnen das Aufrufen von Vorgängen auf sichere Weise. Es ist eine dünne Ebene über der funktionsbasierten Syntax, sodass Sie beide Varianten in derselben App kombinieren können.
Die verwendeten APIs hängen davon ab, wo Ihre C#-Funktionen ausgeführt werden. Es wird ein isolierter Arbeitsprozess empfohlen, Sie können aber auch im Hostprozess ausgeführt werden.
In-Process-Funktionsbasiertes Beispiel:
Dieses Beispiel zeigt eine einfache Counter Entität, die als dauerhafte Funktion implementiert wurde. Es definiert drei Vorgänge – add, reset und get –, die einen ganzzahligen Zustand verwenden.
[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;
}
}
Weitere Informationen finden Sie unter Funktionsbasierte Syntax.
In-Process-Klassenbasiertes Beispiel:
Dieses Beispiel zeigt die gleiche Counter Entität, die mithilfe von Klassen und Methoden implementiert wird.
[JsonObject(MemberSerialization.OptIn)]
public class Counter
{
[JsonProperty("value")]
public int CurrentValue { get; set; }
public void Add(int amount) => this.CurrentValue += amount;
public void Reset() => this.CurrentValue = 0;
public int Get() => this.CurrentValue;
[FunctionName(nameof(Counter))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
=> ctx.DispatchAsync<Counter>();
}
Diese Entität speichert den Zustand in einem Counter Objekt, das den aktuellen Zählerwert enthält. Durable Functions serialisiert und deserialisiert dieses Objekt mithilfe der Bibliothek Json.NET.
Weitere Informationen finden Sie unter Definieren von Entitätsklassen.
Funktionsbasiertes Beispiel für isolierten Arbeitsprozess:
Das folgende Beispiel zeigt eine funktionsbasierte Counter Entität in einem isolierten Arbeitsprozess. Es unterstützt add, reset, , getund delete.
[Function(nameof(Counter))]
public static Task Counter([EntityTrigger] TaskEntityDispatcher dispatcher)
{
return dispatcher.DispatchAsync(operation =>
{
if (operation.State.GetState(typeof(int)) is null)
{
operation.State.SetState(0);
}
switch (operation.Name.ToLowerInvariant())
{
case "add":
int state = operation.State.GetState<int>();
state += operation.GetInput<int>();
operation.State.SetState(state);
return new(state);
case "reset":
operation.State.SetState(0);
break;
case "get":
return new(operation.State.GetState<int>());
case "delete":
operation.State.SetState(null);
break;
}
return default;
});
}
Klassenbasiertes Beispiel für isolierten Arbeitsprozess:
Das folgende Beispiel zeigt die Implementierung der Counter Entität mithilfe von Klassen und Methoden.
public class Counter : TaskEntity<int>
{
readonly ILogger logger;
public Counter(ILogger<Counter> logger)
{
this.logger = logger;
}
public void Add(int amount) => this.State += amount;
public void Reset() => this.State = 0;
public int Get() => this.State;
[Function(nameof(Counter))]
public Task RunEntityAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
{
return dispatcher.DispatchAsync(this);
}
}
Das Durable Task SDK für .NET unterstützt das Definieren von Entitäten mithilfe einer klassenbasierten Syntax. Sie können die TaskEntity<TState> Basisklasse implementieren, um Ihre Entität zu definieren.
Das folgende Beispiel zeigt eine Counter Entität, die mit dem Durable Task SDK implementiert wurde:
using Microsoft.DurableTask.Entities;
public class Counter : TaskEntity<int>
{
public void Add(int amount) => this.State += amount;
public void Reset() => this.State = 0;
public int Get() => this.State;
}
Um die Entität beim Worker zu registrieren:
builder.Services.AddDurableTaskWorker()
.AddTasks(registry =>
{
registry.AddEntity<Counter>();
})
.UseDurableTaskScheduler(connectionString);
Um eine Entität von einem Orchestrator zu signalisieren oder aufzurufen:
public class EntityOrchestration : TaskOrchestrator<string, int>
{
public override async Task<int> RunAsync(TaskOrchestrationContext context, string entityKey)
{
var entityId = new EntityInstanceId(nameof(Counter), entityKey);
// Signal the entity (fire-and-forget)
await context.Entities.SignalEntityAsync(entityId, nameof(Counter.Add), 1);
// Call the entity and wait for response
int currentValue = await context.Entities.CallEntityAsync<int>(entityId, nameof(Counter.Get));
return currentValue;
}
}
Zugreifen auf Entitäten
Zugreifen auf Entitäten mithilfe einer unidirektionale oder bidirektionale Kommunikation:
- Beim Aufrufen einer Entität wird die bidirektionale Kommunikation (Roundtrip) verwendet. Senden Sie eine Vorgangsnachricht an die Entität, und warten Sie dann auf die Antwortnachricht, bevor Sie fortfahren. Die Antwortnachricht stellt einen Ergebniswert oder einen Fehler bereit (z. B. einen JavaScript-Fehler oder eine .NET Ausnahme).
- Beim Signalisieren wird für eine Entität die unidirektionale Kommunikation (Fire and Forget) verwendet. Senden Sie eine Vorgangsnachricht, warten Sie jedoch nicht auf eine Antwort. Die Laufzeit garantiert die Übermittlung, aber der Absender kann keine Ergebniswerte oder Fehler beobachten.
Zugreifen auf Entitäten von Clientfunktionen, Orchestratorfunktionen oder Entitätsfunktionen. Nicht jeder Kontext unterstützt beide Kommunikationstypen:
- Clientfunktionen unterstützen signalisierende Entitäten und das Lesen des Entitätsstatus.
- Orchestrator-Funktionen unterstützen Signalisierungs- und Aufrufen von Entitäten.
- Entitätsfunktionen unterstützen Signalentitäten.
Zugreifen auf Entitäten mithilfe einer unidirektionale oder bidirektionale Kommunikation:
- Beim Aufrufen einer Entität wird die bidirektionale Kommunikation (Roundtrip) verwendet. Senden Sie eine Vorgangsnachricht an die Entität, und warten Sie dann auf die Antwortnachricht, bevor Sie fortfahren. Die Antwortnachricht stellt einen Ergebniswert oder einen Fehler bereit.
- Beim Signalisieren wird für eine Entität die unidirektionale Kommunikation (Fire and Forget) verwendet. Senden Sie eine Vorgangsnachricht, warten Sie jedoch nicht auf eine Antwort. Die Laufzeit garantiert die Übermittlung, aber der Absender kann keine Ergebniswerte oder Fehler beobachten.
Greifen Sie von Clients oder Orchestratoren auf Entitäten zu. Nicht jeder Kontext unterstützt beide Kommunikationstypen:
- Clients unterstützen Signalisierungsentitäten und Lese-Entitätsstatus.
- Orchestrators unterstützen Signalisierungs- und Anrufkomponenten.
Die folgenden Beispiele zeigen, wie auf Entitäten zugegriffen wird.
Beispiel: Client signalisiert eine Entität
Verwenden Sie zum Zugreifen auf Entitäten aus einer normalen Azure-Funktion, die auch als Clientfunktion bezeichnet wird, die entity-Clientbindung. Im Beispiel unten wird eine per Warteschlange ausgelöste Funktion zur Signalisierung einer Entität unter Verwendung dieser Bindung veranschaulicht.
Hinweis
Der Einfachheit halber wird in den folgenden Beispielen die lose typisierte Syntax für den Entitätszugriff verwendet. Im Allgemeinen greifen Sie über Schnittstellen auf Entitäten zu , da sie eine größere Typüberprüfung ermöglichen.
In Bearbeitung:
In Bearbeitung:
[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);
}
Isolierter Arbeitsprozess:
[Function("AddFromQueue")]
public static Task Run(
[QueueTrigger("durable-function-trigger")] string input,
[DurableClient] DurableTaskClient client)
{
// Entity operation input comes from the queue message content.
var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
int amount = int.Parse(input);
return client.Entities.SignalEntityAsync(entityId, "Add", amount);
}
Der Begriff Signal bedeutet, dass der Aufruf der Entitäts-API unidirektional und asynchron ist. Eine Clientfunktion kann nicht wissen, wann die Entität den Vorgang verarbeitet. Die Clientfunktion kann keine Ergebniswerte oder Ausnahmen beobachten.
Verwenden Sie für den Zugriff auf Entitäten von einem Client das DurableTaskClient, um den Entitätsstatus zu signalisieren oder zu lesen.
// Signal an entity
var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
await client.Entities.SignalEntityAsync(entityId, nameof(Counter.Add), 1);
Das Signal bedeutet, dass der Aufruf der Entitäts-API unidirektional und asynchron ist. Ein Client kann nicht wissen, wann die Entität den Vorgang verarbeitet.
Beispiel: Client liest einen Entitätsstatus
Abfragen eines Entitätsstatus aus einer Clientfunktion:
Prozessintern:
[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);
}
Isolierter Arbeitsprozess:
[Function("QueryCounter")]
public static async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function)] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
EntityMetadata<int>? entity = await client.Entities.GetEntityAsync<int>(entityId);
if (entity is null)
{
return req.CreateResponse(HttpStatusCode.NotFound);
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(entity);
return response;
}
Clients können den Status einer Entität abfragen:
var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
EntityMetadata<int>? entity = await client.Entities.GetEntityAsync<int>(entityId);
if (entity != null)
{
Console.WriteLine($"Current value: {entity.State}");
}
Entitätsstatusabfragen werden an den dauerhaften Nachverfolgungsspeicher gesendet und geben den zuletzt beibehaltenen Zustand der Entität zurück. Dieser Zustand ist immer ein „verbindlicher“ Zustand, d. h. es ist niemals ein temporärer Übergangszustand während der Vorgangsausführung. Dieser Zustand kann jedoch veraltet sein, verglichen mit dem In-Memory-Zustand der Entität. Der In-Memory-Zustand einer Entität kann nur von Orchestrierungen gelesen werden, wie im folgenden Abschnitt beschrieben.
Beispiel: Orchestrierungssignale und Aufrufe einer Entität
Orchestratorfunktionen können mithilfe von APIs für die Orchestrierungstriggerbindung auf Entitäten zugreifen. Der folgende Beispielcode zeigt eine Orchestratorfunktion, die eine Counter Entität aufruft und signalisiert.
Prozessintern:
[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);
}
}
Isolierter Arbeitsprozess:
[Function("CounterOrchestration")]
public static async Task Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
// Two-way call to the entity which returns a value - awaits the response
int currentValue = await context.Entities.CallEntityAsync<int>(entityId, "Get");
if (currentValue < 10)
{
// One-way signal to the entity which updates the value - does not await a response
await context.Entities.SignalEntityAsync(entityId, "Add", 1);
}
}
Nur Orchestrierungen können Entitäten aufrufen und erhalten eine Antwort, bei der es sich um einen Rückgabewert oder eine Ausnahme handelt. Clientfunktionen, die die Clientbindung verwenden, können nur Entitäten signalisieren.
Orchestrators können mithilfe der Entitäts-API des Kontexts auf Entitäten zugreifen:
public class CounterOrchestration : TaskOrchestrator<string, int>
{
public override async Task<int> RunAsync(TaskOrchestrationContext context, string entityKey)
{
var entityId = new EntityInstanceId(nameof(Counter), entityKey);
// Two-way call to the entity which returns a value - awaits the response
int currentValue = await context.Entities.CallEntityAsync<int>(entityId, nameof(Counter.Get));
if (currentValue < 10)
{
// One-way signal to the entity - does not await a response
await context.Entities.SignalEntityAsync(entityId, nameof(Counter.Add), 1);
}
return currentValue;
}
}
Nur Orchestratoren können Entitäten aufrufen und eine Antwort erhalten, bei der es sich um einen Rückgabewert oder eine Ausnahme handelt. Clients können nur Entitäten signalisieren.
Hinweis
Das Aufrufen einer Entität aus einem Orchestrator ähnelt dem Aufrufen einer Aktivität. Der Hauptunterschied besteht darin, dass Entitäten dauerhafte Objekte mit einer Adresse (der Entitäts-ID) sind und die Angabe eines Vorgangsnamens unterstützen. Aktivitäten sind zustandslos und haben kein Betriebskonzept.
Beispiel: Eine Einheit signalisiert eine andere Einheit
Eine Entitätsfunktion kann während der Vorgangsausführung Signale an andere Entitäten und sogar an sich selbst senden.
Ändern Sie z. B. das vorherige Counter Entitätsbeispiel so, dass ein Signal "Meilenstein erreicht" an eine Monitorentität gesendet wird, wenn der Zähler 100 erreicht.
Prozessintern:
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;
Isolierter Arbeitsprozess:
case "add":
var currentValue = operation.State.GetState<int>();
var amount = operation.GetInput<int>();
if (currentValue < 100 && currentValue + amount >= 100)
{
operation.Context.SignalEntity(new EntityInstanceId("MonitorEntity", ""), "milestone-reached", operation.Context.EntityInstanceId);
}
operation.State.SetState(currentValue + amount);
break;
Koordination von Entitäten
Manchmal müssen Sie Vorgänge über mehrere Entitäten hinweg koordinieren. In einer Bankanwendung können Entitäten z. B. einzelne Bankkonten darstellen. Wenn Sie Guthaben von einem Konto auf ein anderes übertragen, müssen Sie sicherstellen, dass das Quellkonto über genügend Guthaben verfügt. Sie müssen auch beide Konten als einen konsistenten Vorgang aktualisieren.
Beispiel: Überweisungsguthaben (C#)
Der folgende Beispielcode führt eine Überweisung zwischen zwei Kontoentitäten über eine Orchestratorfunktion durch. Verwenden Sie die LockAsync-Methode, um Entitätsaktualisierungen zu koordinieren, indem Sie einen kritischen Abschnitt in der Orchestrierung erstellen.
Hinweis
Aus Gründen der Einfachheit verwendet dieses Beispiel die Counter zuvor definierte Entität wieder. In einer echten Anwendung ist es besser, eine detailliertere BankAccount Entität zu definieren.
// 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;
}
}
}
In .NET gibt LockAsyncIDisposable zurück. Durch das Löschen wird der kritische Abschnitt beendet. Verwenden Sie ihn mit einem using Block, um den kritischen Abschnitt darzustellen.
Im obigen Beispiel wurde mithilfe einer Orchestratorfunktion eine Überweisung von einer Quellentität zu einer Zielentität getätigt. Die LockAsync Methode hat sowohl die Quell- als auch die Zielkontoentitäten gesperrt. Durch diese Sperrung wurde sichergestellt, dass kein anderer Client den Status eines kontos abfragen oder ändern konnte, bis die Orchestrierungslogik den kritischen Abschnitt am Ende der using Anweisung beendet hat. Dieses Vorgehen verhindert Überziehungen auf dem Quellkonto.
Hinweis
Wenn eine Orchestrierung entweder normal oder mit einem Fehler endet, enden alle kritischen Abschnitte in Bearbeitung implizit, und das System gibt alle Sperren frei.
Verhalten des kritischen Abschnitts
Die LockAsync Methode erstellt einen kritischen Abschnitt in einer Orchestrierung. Diese kritischen Abschnitte sorgen dafür, dass andere Orchestrierungen keine sich überschneidenden Änderungen an einem angegebenen Satz von Entitäten vornehmen können. Intern sendet die LockAsync API "Sperrvorgänge" an die Entitäten und kehrt zurück, nachdem sie von jeder Entität eine "Sperre erworben"-Antwort erhalten hat. „Sperren“ und „Entsperren“ sind integrierte Vorgänge, die von allen Entitäten unterstützt werden.
Andere Clients können vorgänge für eine Entität nicht ausführen, während sie gesperrt ist. Dadurch wird sichergestellt, dass immer nur eine Orchestrierungsinstanz eine Entität sperren kann. Wenn ein Aufrufer versucht, einen Vorgang für eine Entität aufzurufen, die durch eine Orchestrierung gesperrt wurde, wird der Vorgang in einer Warteschlange für ausstehende Vorgänge platziert. Ausstehende Vorgänge werden erst verarbeitet, nachdem die Sperre dieser Orchestrierung aufgehoben wurde.
Hinweis
Dieses Verhalten unterscheidet sich geringfügig von Synchronisierungsgrundtypen, die in den meisten Programmiersprachen verwendet werden, z. B. die lock Anweisung in C#. In C# müssen beispielsweise alle Threads die lock Anweisung verwenden, um eine ordnungsgemäße Synchronisierung sicherzustellen. Entitäten müssen dagegen nicht von allen Aufrufern explizit gesperrt werden. Wenn ein Aufrufer eine Entität sperrt, werden alle anderen Vorgänge für diese Entität blockiert und in einer Warteschlange hinter dieser Sperre platziert.
Sperren für Entitäten sind dauerhaft und bleiben auch bestehen, wenn der ausführende Prozess recycelt wird. Das System speichert Sperren als Teil des dauerhaften Zustands einer Entität.
Im Gegensatz zu Transaktionen wird für kritische Abschnitte bei Fehlern kein automatischer Änderungsrollback durchgeführt. Stattdessen sollten Sie eine Fehlerbehandlung implementieren, beispielsweise durch Rollback oder Wiederholung, indem Sie Fehler oder Ausnahmen abfangen. Dies ist eine bewusste Entscheidung. Ein automatischer Rollback aller Auswirkungen einer Orchestrierung ist im Allgemeinen schwierig bis unmöglich, da Orchestrierungen unter Umständen Aktivitäten ausführen und externe Dienste aufrufen, für die kein Rollback möglich ist. Darüber hinaus kann es vorkommen, dass auch Rollbackversuche nicht erfolgreich sind, was eine weitere Fehlerbehandlung erforderlich macht.
Regeln für kritische Abschnitte
Im Gegensatz zu Sperrprimitiven auf niedriger Ebene in den meisten Programmiersprachen ist bei kritischen Abschnitten garantiert, dass sie nicht in einen Deadlock geraten. Zur Verhinderung von Deadlocks werden folgende Einschränkungen erzwungen:
- Kritische Abschnitte können nicht geschachtelt werden.
- Für kritische Abschnitte können keine Unterorchestrierungen erstellt werden.
- Kritische Abschnitte können nur die Entitäten aufrufen, die sie sperren.
- Kritische Abschnitte können nicht mit mehreren parallelen Aufrufen dieselbe Entität aufrufen.
- Kritische Abschnitte können nur Entitäten außerhalb der Sperrmenge signalisieren.
Verstöße gegen diese Regeln verursachen einen Laufzeitfehler, z. B. LockingRulesViolationException in .NET, was eine Meldung enthält, in der erläutert wird, welche Regel unterbrochen wurde.
Vergleich mit virtuellen Akteuren
Dauerhafte Entitäten verwenden viele Ideen aus dem Akteurmodell. Wenn Sie mit Schauspielern vertraut sind, erkennen Sie möglicherweise mehrere Konzepte in diesem Artikel. Dauerhafte Entitäten ähneln virtuellen Akteuren, auch Korn genannt, aus dem Orleans-Projekt. Beispiel:
- Sie adressieren dauerhafte Entitäten mithilfe einer Entitäts-ID.
- Dauerhafte Entitätsvorgänge werden fortlaufend ausgeführt, um Rennbedingungen zu verhindern.
- Das Aufrufen oder Signalisieren einer Entität erzeugt sie implizit.
- Die Laufzeit entlädt Entitäten aus dem Arbeitsspeicher, wenn sie keine Vorgänge ausführen.
Zu den wichtigsten Unterschieden gehören:
- Dauerhafte Entitäten priorisieren haltbarkeit gegenüber Latenz, sodass sie anwendungen möglicherweise nicht mit strengen Latenzanforderungen erfüllen.
- Dauerhafte Entitäten geben keine Timeoutnachrichten an. Orleans gibt Nachrichten nach einem konfigurierbaren Zeitraum aus (standardmäßig 30 Sekunden).
- Entitäten liefern Nachrichten zuverlässig und in der Reihenfolge. Orleans unterstützt eine zuverlässige, geordnete Zustellung von Stream-Nachrichten, garantiert dies jedoch nicht für alle Nachrichten zwischen Grains.
- Orchestrierungen sind der einzige Ort, an dem Sie das Request-Response-Muster mit Entitäten verwenden können. Verwenden Sie innerhalb einer Entität unidirektionale Nachrichten (Signalisierung), z. B. im ursprünglichen Akteurmodell.
- Bei dauerhaften Entitäten treten keine Deadlocks auf. Orleans kann zu Deadlocks führen, und Deadlocks bleiben so lange bestehen, bis die Nachrichten ablaufen.
- Dauerhafte Entitäten arbeiten mit dauerhaften Orchestrierungen und unterstützen verteilte Sperrmechanismen.