实体函数定义了读取和更新小段状态的操作,称为持久实体。 与业务流程协调程序函数一样,实体函数使用一种特殊的触发器,称为 实体触发器。 与业务流程协调程序函数不同,实体函数显式管理实体状态,而不是通过控制流表示状态。
实体通过在多个拥有适度状态的实体之间分布工作,帮助实现应用的横向扩展。
实体定义读取和更新小段状态的操作,称为 持久实体。 与业务流程协调程序不同,实体显式管理状态,而不是通过控制流表示状态。 实体通过在多个拥有适度状态的实体之间分布工作,帮助实现应用的横向扩展。
对持久实体的支持
实体函数和相关功能可在 Durable Functions 2.0 及更高版本中使用。
| 编程语言 |
对持久实体的支持 |
| 独立 .NET |
✅ |
| .NET 进程内 |
✅ |
| Java |
✅ |
| Python |
✅ |
| JavaScript |
✅ |
| PowerShell |
❌ |
| Durable Task SDK |
对持久实体的支持 |
| 独立 .NET |
✅ |
| .NET 进程内 |
✅ |
| Java |
✅ |
| Python |
✅ |
| JavaScript |
✅ |
| Go(beta 版本) |
✅ |
| PowerShell |
❌ |
持久实体的一般概念
实体通过消息进行通信,类似于小型服务。 每个实体都有唯一的标识,并且如果需要,还具有内部状态。 当提示出现时,实体执行操作。 一个操作可能会更新状态、调用外部服务或等待响应。 实体通过运行时通过可靠队列发送的消息与其他实体、业务流程和客户端进行通信。
为了防止冲突,单个实体顺序地逐个运行操作。
注释
当调用一个实体时,它会完成对有效负载的处理,然后安排一个新的执行流程,该流程会在有新的输入数据到达时启动。 因此,实体执行日志可能会在每次调用后显示额外的执行。 这是预期的。
实体标识符
使用 实体 ID 访问实体。 实体 ID 是一对唯一标识实体实例的字符串。 它由以下部分组成:
- 标识实体类型的实体名称。 例如,
Counter。 此名称与实现实体的实体函数的名称匹配。 它不区分大小写。
-
实体键,用于在具有相同名称的所有实体中唯一标识该实体。 例如,GUID。
例如, Counter 实体函数可用于在在线游戏中保留分数。 游戏的每个实例都有一个唯一的实体 ID,例如 @Counter@Game1 和 @Counter@Game2。 若要以实体为目标,请指定其实体 ID。
实体操作
若要对实体调用一个操作,请指定:
- 目标实体的实体 ID。
-
操作名称,它是一个用于指定要执行操作的字符串。 例如,
Counter 实体可以支持 add、get 或 reset 操作。
-
操作输入,这是操作的可选参数。 例如,该
add 操作采用整数作为输入。
-
计划时间,这是一个可选参数,用于指定操作的交付时间。 例如,安排一个操作,让其在几天后执行。
操作可以返回结果值或错误结果,例如 JavaScript 错误或 .NET 异常。 调用编排接收结果或错误。
操作可以返回结果值或错误结果,比如 .NET 异常或 Python 异常。 调用方收到结果或错误。
实体操作还可以创建、读取、更新和删除实体的状态。 运行时始终在存储中保留实体状态。
定义实体
使用两个 API 中的一个定义.NET中的实体:
基于函数的语法:在基于函数的语法中,将每个实体编写为函数并在应用中调度作。 此语法适用于具有简单状态、少量操作或动态操作集的实体,例如在应用框架中。 但是,维护可能很繁琐,因为它在编译时不会捕获类型错误。
基于类的语法:在基于类的语法中,.NET类和方法建模实体和操作。 此语法使代码更易于读取,并允许以类型安全的方式调用作。 它是基于函数的语法之上的薄层,因此可以在同一应用中混合这两种变体。
使用的 API 取决于 C# 函数的运行位置。 建议 使用隔离的工作进程 ,但也可以在主机进程中运行。
基于函数的独立工作进程示例
以下示例显示了隔离工作进程中基于函数的 Counter 实体。 它支持add、reset和getdelete。
[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;
});
}
基于类的孤立工作进程示例
以下示例展示了如何使用类和方法实现 Counter 实体。
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);
}
}
进程中基于函数的示例
此示例演示一个简单的 Counter 实体,作为持久函数来实现。 它定义了三个操作add、reset和get,这些操作使用整数状态。
[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;
}
}
有关详细信息,请参阅 基于函数的语法。
基于类的进程示例
此示例显示了使用类和方法实现的相同 Counter 实体。
[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>();
}
此实体将状态存储在保存当前计数器值的对象中 Counter 。 Durable Functions使用 Json.NET 库序列化和反序列化此对象。
有关详细信息,请参阅 定义实体类。
JavaScript 从 npm 包1.3.0durable-functions版本起支持持久实体。 示例展示了如何在 JavaScript 中将 Counter 实体实现为持久函数。
Counter/function.json
{
"bindings": [
{
"name": "context",
"type": "entityTrigger",
"direction": "in"
}
],
"disabled": false
}
Counter/index.js
const df = require("durable-functions");
module.exports = df.entity(function(context) {
const currentValue = context.df.getState(() => 0);
switch (context.df.operationName) {
case "add": {
const amount = context.df.getInput();
context.df.setState(currentValue + amount);
break;
}
case "reset":
context.df.setState(0);
break;
case "get":
context.df.return(currentValue);
break;
}
});
以下代码显示了用 Python 编写并实现为持久函数的 Counter 实体。
import azure.functions as func
import azure.durable_functions as df
# Entity function called counter
@myApp.entity_trigger(context_name="context")
def Counter(context):
current_value = context.get_state(lambda: 0)
operation = context.operation_name
if operation == "add":
amount = context.get_input()
current_value += amount
elif operation == "reset":
current_value = 0
elif operation == "get":
context.set_result(current_value)
context.set_state(current_value)
从1.9.0版本开始,Durable Functions for Java支持通过基于类的语法定义实体。 可以扩展 AbstractTaskEntity<TState> 基类来定义实体。
以下示例演示在 Java 中实现为持久函数的 Counter 实体。
实体类:
import com.microsoft.durabletask.AbstractTaskEntity;
import com.microsoft.durabletask.TaskEntityOperation;
public class CounterEntity extends AbstractTaskEntity<Integer> {
public void add(int value) {
this.state += value;
}
public void subtract(int value) {
this.state -= value;
}
public int get() {
return this.state;
}
public void reset() {
this.state = 0;
}
@Override
protected Integer initializeState(TaskEntityOperation operation) {
return 0;
}
@Override
protected Class<Integer> getStateType() {
return Integer.class;
}
}
实体函数:
@FunctionName("Counter")
public String counterEntity(
@DurableEntityTrigger(name = "req") String req) {
return EntityRunner.loadAndRun(req, CounterEntity::new);
}
Durable Functions不支持Go。 要查看 Go 示例,请在本文章顶部选择 “持久任务 SDK” 。
用于.NET的 Durable Task SDK 支持使用基于类的语法定义实体。 实现 TaskEntity<TState> 基类来定义你的实体。
以下示例演示了使用 Counter Durable Task SDK 实现的实体:
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;
}
若要向工作器注册实体,请执行以下操作:
builder.Services.AddDurableTaskWorker()
.AddTasks(registry =>
{
registry.AddEntity<Counter>();
})
.UseDurableTaskScheduler(connectionString);
若要从编排器发出信号或调用实体,请按以下步骤操作:
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;
}
}
Durable Task SDK for JavaScript/TypeScript 支持使用基于类的语法定义实体。 扩展 TaskEntity<TState> 基类来定义你的实体。
以下示例演示了使用 Counter Durable Task SDK 实现的实体:
import { TaskEntity } from "@microsoft/durabletask-js";
class CounterEntity extends TaskEntity {
add(amount) {
this.state.value += amount;
return this.state.value;
}
get() {
return this.state.value;
}
reset() {
this.state.value = 0;
}
initializeState() {
return { value: 0 };
}
}
若要向工作器注册实体,请执行以下操作:
worker.addNamedEntity("Counter", () => new CounterEntity());
用于Python的 Durable Task SDK 支持使用基于函数的语法定义实体。
以下示例演示了使用 Counter Durable Task SDK 实现的实体:
from durabletask import task
def counter_entity(ctx: task.EntityContext):
"""Counter entity that maintains an integer state."""
current_value = ctx.get_state(lambda: 0)
operation = ctx.operation_name
if operation == "add":
amount = ctx.get_input()
current_value += amount
elif operation == "reset":
current_value = 0
elif operation == "get":
ctx.set_result(current_value)
ctx.set_state(current_value)
若要向工作器注册实体,请执行以下操作:
with DurableTaskSchedulerWorker(
host_address=endpoint,
secure_channel=secure_channel,
taskhub=taskhub,
token_credential=credential
) as worker:
worker.add_entity(counter_entity)
worker.start()
持久实体的PowerShell支持尚未上线。 请返回查看将来的更新。
Durable Task SDK for Java 支持使用基于类的语法定义实体。 可以扩展 AbstractTaskEntity<TState> 基类来定义实体。
以下示例演示了使用 Counter Durable Task SDK 实现的实体:
import com.microsoft.durabletask.AbstractTaskEntity;
import com.microsoft.durabletask.TaskEntityOperation;
public class CounterEntity extends AbstractTaskEntity<Integer> {
public void add(int amount) {
this.state += amount;
}
public void reset() {
this.state = 0;
}
public int get() {
return this.state;
}
@Override
protected Integer initializeState(TaskEntityOperation operation) {
return 0;
}
@Override
protected Class<Integer> getStateType() {
return Integer.class;
}
}
若要向工作器注册实体,请执行以下操作:
DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder()
.addEntity("Counter", CounterEntity::new)
.build();
worker.start();
若要从编排器发出信号或调用实体,请按以下步骤操作:
return ctx -> {
EntityInstanceId counterId = new EntityInstanceId("Counter", "myCounter");
// Signal the entity (fire-and-forget)
ctx.signalEntity(counterId, "add", 5);
// Call the entity and wait for response
int currentValue = ctx.callEntity(counterId, "get", Integer.class).await();
ctx.complete(currentValue);
};
Go SDK 支持基于函数的实体。 该计数器读取现有状态,处理操作,并持久化更新:
package main
import (
"fmt"
"github.com/microsoft/durabletask-go/task"
)
func counter(ctx *task.EntityContext) (any, error) {
var value int
if ctx.HasState() {
if err := ctx.GetState(&value); err != nil {
return nil, err
}
}
switch ctx.Operation {
case "get":
return value, nil
case "add":
var amount int
if err := ctx.GetInput(&amount); err != nil {
return nil, err
}
value += amount
case "reset":
value = 0
case "delete":
ctx.DeleteState()
return nil, nil
default:
return nil, fmt.Errorf("unknown counter operation %q", ctx.Operation)
}
if err := ctx.SetState(value); err != nil {
return nil, err
}
return value, nil
}
将以下函数保存在同一包的另一个文件中。 在创建工作进程之前,先在工作进程注册表上调用 registerCounter。 使用唯一的实体键作为其字符串输入来启动 UseCounter。
package main
import (
"fmt"
"github.com/microsoft/durabletask-go/api"
"github.com/microsoft/durabletask-go/task"
)
func useCounter(ctx *task.OrchestrationContext) (any, error) {
var key string
if err := ctx.GetInput(&key); err != nil {
return nil, err
}
if key == "" {
return nil, fmt.Errorf("entity key must not be empty")
}
id := api.NewEntityID("Counter", key)
if err := ctx.SignalEntity(id, "add", task.WithSignalEntityInput(1)); err != nil {
return nil, err
}
var value int
if err := ctx.CallEntity(id, "get").Await(&value); err != nil {
return nil, err
}
return value, nil
}
func registerCounter(registry *task.TaskRegistry) error {
if err := registry.AddEntityN("Counter", counter); err != nil {
return err
}
return registry.AddOrchestratorN("UseCounter", useCounter)
}
信号不会返回操作结果。 调用会等待返回结果。 单个实体实例的操作是串行执行的。 有关注册模型、工厂和锁定转移,请参见 Go SDK 实体代码。
Warning
Go 和 Python 使用了与 .NET 不同的多实体锁排序。 不要从 Go/Python 和 .NET 业务流程中以并发方式获取重叠的锁集。
访问权限实体
使用单向或双向通信访问实体:
- 使用双向(往返)通信调用实体。 发送操作消息到实体,然后在继续之前等待响应消息。 响应消息提供结果值或错误(例如,JavaScript 错误或.NET异常)。
- 使用单向(发后不理)通信向实体发出信号。 发送操作消息,但是不等待响应。 运行时保证传递,但发送方无法观察结果值或错误。
从客户端函数、编排函数或实体函数访问实体。 并非每个上下文都支持这两种通信类型:
- 客户端函数支持信号实体和读取实体状态。
- 业务流程协调程序函数支持向实体发出信号和调用实体。
- 实体函数支持信号实体。
使用单向或双向通信访问实体:
- 使用双向(往返)通信调用实体。 发送操作消息到实体,然后在继续之前等待响应消息。 响应消息提供结果值或错误。
- 使用单向(发后不理)通信向实体发出信号。 发送操作消息,但是不等待响应。 运行时保证传递,但发送方无法观察结果值或错误。
可以从客户端或编排器访问实体。 并非每个上下文都支持这两种通信类型:
- 客户端支持向实体发出信号并读取实体状态。
- 业务流程协调程序支持向实体发出信号和调用实体。
以下示例演示如何访问实体。
示例:客户端向实体发出信号
若要从普通的 Azure 函数(也称为客户端函数)访问实体,请使用实体客户端绑定。 以下示例展示了一个队列触发函数通过该绑定向实体发出信号。
注释
为简单起见,以下示例演示了用于访问实体的松散类型化语法。 通常, 通过接口访问实体 ,因为它们提供了更多的类型检查。
进程内:
进程内:
[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);
}
独立工作进程:
[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);
}
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):
entityId = df.EntityId("Counter", "myCounter")
await client.signal_entity(entityId, "add", 1)
return func.HttpResponse("Done", status_code=200)
@FunctionName("SignalCounter")
public HttpResponseMessage signalCounter(
@HttpTrigger(name = "req", methods = {HttpMethod.POST},
authLevel = AuthorizationLevel.ANONYMOUS)
HttpRequestMessage<Void> request,
@DurableClientInput(name = "durableContext") DurableClientContext durableContext) {
String entityKey = request.getQueryParameters().getOrDefault("key", "myCounter");
String operation = request.getQueryParameters().getOrDefault("op", "add");
String valueStr = request.getQueryParameters().getOrDefault("value", "1");
EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey);
if ("reset".equals(operation)) {
durableContext.signalEntity(entityId, operation);
} else {
int value = Integer.parseInt(valueStr);
durableContext.signalEntity(entityId, operation, value);
}
return request.createResponseBuilder(HttpStatus.ACCEPTED)
.body("Signal sent: " + operation + " on entity '" + entityKey + "'")
.build();
}
Durable Functions不支持Go。 要查看 Go 示例,请在本文章顶部选择 “持久任务 SDK” 。
术语 信号 表示实体 API 调用是单向和异步的。 客户端的函数无法知道实体何时处理该操作。 客户端函数无法观察结果值或异常。
若要从客户端访问实体,使用 DurableTaskClient 发出信号或读取实体状态。
// Signal an entity
var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
await client.Entities.SignalEntityAsync(entityId, nameof(Counter.Add), 1);
// Signal an entity
const counterId = new EntityInstanceId("Counter", "myCounter");
await client.signalEntity(counterId, "add", 1);
# Signal an entity
entity_id = EntityId("Counter", "myCounter")
client.signal_entity(entity_id, "add", 1)
持久实体的PowerShell支持尚未上线。 请返回查看将来的更新。
// Signal an entity
EntityInstanceId counterId = new EntityInstanceId("Counter", "myCounter");
client.getEntities().signalEntity(counterId, "add", 1);
术语 信号 表示实体 API 调用是单向和异步的。 客户端无法知道实体何时处理操作。
示例:客户端读取实体状态
从客户端函数查询实体状态:
进程内:
[FunctionName("QueryCounter")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function)] HttpRequestMessage req,
[DurableClient] IDurableEntityClient client)
{
var entityId = new EntityId(nameof(Counter), "myCounter");
EntityStateResponse<JObject> stateResponse = await client.ReadEntityStateAsync<JObject>(entityId);
return req.CreateResponse(HttpStatusCode.OK, stateResponse.EntityState);
}
独立工作进程:
[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;
}
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)
@FunctionName("GetCounter")
public HttpResponseMessage getCounter(
@HttpTrigger(name = "req", methods = {HttpMethod.GET},
authLevel = AuthorizationLevel.ANONYMOUS)
HttpRequestMessage<Void> request,
@DurableClientInput(name = "durableContext") DurableClientContext durableContext) {
String entityKey = request.getQueryParameters().getOrDefault("key", "myCounter");
EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey);
EntityMetadata metadata = durableContext.getEntityMetadata(entityId, true);
if (metadata == null) {
return request.createResponseBuilder(HttpStatus.NOT_FOUND)
.body("Entity '" + entityKey + "' not found")
.build();
}
Integer state = metadata.readStateAs(Integer.class);
return request.createResponseBuilder(HttpStatus.OK)
.header("Content-Type", "application/json")
.body("{\"key\": \"" + entityKey + "\", \"value\": " + state + "}")
.build();
}
Durable Functions不支持Go。 要查看 Go 示例,请在本文章顶部选择 “持久任务 SDK” 。
客户端可以查询实体的状态:
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}");
}
const counterId = new EntityInstanceId("Counter", "myCounter");
const metadata = await client.getEntity(counterId);
if (metadata.exists) {
console.log(`Current value: ${metadata.state?.value}`);
}
entity_id = EntityId("Counter", "myCounter")
state = client.get_entity_state(entity_id)
if state is not None:
print(f"Current value: {state}")
持久实体的PowerShell支持尚未上线。 请返回查看将来的更新。
EntityInstanceId counterId = new EntityInstanceId("Counter", "myCounter");
EntityMetadata entityMetadata = client.getEntities().getEntityMetadata(counterId, true);
if (entityMetadata != null) {
Integer state = entityMetadata.readStateAs(Integer.class);
System.out.printf("Current value: %d%n", state);
}
实体状态查询会访问持久追踪存储,返回实体最近持久化的状态。 这个状态始终是“已提交”状态,因此从不是在执行操作过程中假设的临时中间状态。 但与实体的内存中状态相比,此状态可能过时。 如下一部分所述,只有业务流程可以读取实体的内存中状态。
示例:业务流程向实体发出信号和调用实体
业务流程协调程序函数可以使用业务流程触发器绑定中的 API 访问实体。 以下示例代码展示了一个协调器函数如何调用并发出信号给 Counter 实体。
进程内:
[FunctionName("CounterOrchestration")]
public static async Task Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var entityId = new EntityId(nameof(Counter), "myCounter");
// Two-way call to the entity which returns a value - awaits the response
int currentValue = await context.CallEntityAsync<int>(entityId, "Get");
if (currentValue < 10)
{
// One-way signal to the entity which updates the value - does not await a response
context.SignalEntity(entityId, "Add", 1);
}
}
独立工作进程:
[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);
}
}
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
const currentValue = yield context.df.callEntity(entityId, "get");
return currentValue;
});
注释
JavaScript 不支持从协调器向实体发送信号。 请改用 callEntity。
@myApp.orchestration_trigger(context_name="context")
def orchestrator(context: df.DurableOrchestrationContext):
entityId = df.EntityId("Counter", "myCounter")
context.signal_entity(entityId, "add", 3)
state = yield context.call_entity(entityId, "get")
return state
@FunctionName("CounterOrchestration")
public String counterOrchestration(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
String entityKey = ctx.getInput(String.class);
EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey);
// Signal entity operations (fire-and-forget)
ctx.signalEntity(entityId, "add", 10);
ctx.signalEntity(entityId, "add", 5);
ctx.signalEntity(entityId, "subtract", 3);
// Call entity and wait for result
int value = ctx.callEntity(entityId, "get", Integer.class).await();
return "Counter '" + entityKey + "' final value: " + value;
}
Durable Functions不支持Go。 要查看 Go 示例,请在本文章顶部选择 “持久任务 SDK” 。
只有编排可以调用实体并获取响应,该响应可以是返回值或异常。 使用客户端绑定的客户端函数只能发送实体的信号。
编排器可以通过上下文的实体API访问实体:
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;
}
}
const counterOrchestration = async function* (ctx, entityKey) {
const counterId = new EntityInstanceId("Counter", entityKey);
// Two-way call to the entity which returns a value - awaits the response
const currentValue = yield* ctx.entities.callEntity(counterId, "get");
if (currentValue < 10) {
// One-way signal to the entity - does not await a response
ctx.entities.signalEntity(counterId, "add", 1);
}
return currentValue;
};
def counter_orchestration(ctx: task.OrchestrationContext, entity_key: str):
entity_id = EntityId("Counter", entity_key)
# Two-way call to the entity which returns a value
current_value = yield ctx.call_entity(entity_id, "get")
if current_value < 10:
# One-way signal to the entity
ctx.signal_entity(entity_id, "add", 1)
return current_value
持久实体的PowerShell支持尚未上线。 请返回查看将来的更新。
return ctx -> {
EntityInstanceId counterId = new EntityInstanceId("Counter", entityKey);
// Two-way call to the entity which returns a value - awaits the response
int currentValue = ctx.callEntity(counterId, "get", Integer.class).await();
if (currentValue < 10) {
// One-way signal to the entity - does not await a response
ctx.signalEntity(counterId, "add", 1);
}
ctx.complete(currentValue);
};
只有协调器可以调用实体并获取响应,响应可以是返回值或异常。 客户端只能向实体发出信号。
注释
从协调器调用实体类似于调用活动。 主要区别是实体是具有地址(实体 ID)且支持指定操作名称的持久化对象。 活动是无状态的,没有操作的概念。
示例:实体向另一个实体发出信号
当某个实体函数在执行操作时,可以向其他实体(甚至是自身)发送信号。
例如,修改前面的 Counter 实体示例,以便在计数器达到 100 时向监视器实体发送“里程碑到达”信号。
进程内:
case "add":
var currentValue = ctx.GetState<int>();
var amount = ctx.GetInput<int>();
if (currentValue < 100 && currentValue + amount >= 100)
{
ctx.SignalEntity(new EntityId("MonitorEntity", ""), "milestone-reached", ctx.EntityKey);
}
ctx.SetState(currentValue + amount);
break;
独立工作进程:
case "add":
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;
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;
注释
Python不支持实体到实体信号。 请改用协调器向实体发出信号。
以下示例演示实体到实体信号。 发生大型存款时, Account 实体会向实体 Audit 发出记录交易的信号。
public class AccountEntity extends AbstractTaskEntity<Integer> {
private static final int LARGE_TRANSACTION_THRESHOLD = 500;
public void deposit(int amount) {
this.state += amount;
String key = this.context.getId().getKey();
// Entity signals another entity: notify the audit entity of large deposits
if (amount >= LARGE_TRANSACTION_THRESHOLD) {
EntityInstanceId auditEntityId = new EntityInstanceId("Audit", "ledger");
String message = String.format("Large deposit of %d into account '%s'", amount, key);
this.context.signalEntity(auditEntityId, "record", message);
}
}
// ... other operations ...
@Override
protected Integer initializeState(TaskEntityOperation operation) {
return 0;
}
@Override
protected Class<Integer> getStateType() {
return Integer.class;
}
}
Durable Functions不支持Go。 要查看 Go 示例,请在本文章顶部选择 “持久任务 SDK” 。
实体协调
有时需要在多个实体之间协调运作。 例如,在银行应用程序中,实体可以表示单个银行账户。 将资金从一个帐户转移到另一个帐户时,需要确保源帐户有足够的资金。 还需要同时更新两者的帐户以确保操作的一致性。
示例:资金转移
以下示例代码使用协调器函数在两个账户实体之间转移资金。 要协调实体更新,请在编排中使用锁创建临界区。
注释
为简单起见,本示例重复使用了前面定义的 Counter 实体。 在实际应用程序中,最好定义更详细的 BankAccount 实体。
使用该 LockAsync 方法创建关键部分。
// This is a method called by an orchestrator function
public static async Task<bool> TransferFundsAsync(
string sourceId,
string destinationId,
int transferAmount,
IDurableOrchestrationContext context)
{
var sourceEntity = new EntityId(nameof(Counter), sourceId);
var destinationEntity = new EntityId(nameof(Counter), destinationId);
// Create a critical section to avoid race conditions.
// No operations can be performed on either the source or
// destination accounts until the locks are released.
using (await context.LockAsync(sourceEntity, destinationEntity))
{
ICounter sourceProxy =
context.CreateEntityProxy<ICounter>(sourceEntity);
ICounter destinationProxy =
context.CreateEntityProxy<ICounter>(destinationEntity);
int sourceBalance = await sourceProxy.Get();
if (sourceBalance >= transferAmount)
{
await sourceProxy.Add(-transferAmount);
await destinationProxy.Add(transferAmount);
// the transfer succeeded
return true;
}
else
{
// the transfer failed due to insufficient funds
return false;
}
}
}
在 .NET 中,LockAsync 返回 IDisposable。 销毁它就标志着关键节的结束。 使用 using 块来表示关键部分。
在上面的示例中,业务流程协调程序函数已将资金从源实体转到目标实体。 该 LockAsync 方法会锁定源账户和目的账户实体。 这种锁定确保在编排逻辑退出语句末尾 using 的关键部分之前,没有其他客户端能够查询或修改任一账户的状态。 此行为可防止透支源帐户。
使用该 context.df.lock 方法创建关键部分。
const df = require("durable-functions");
// This is an orchestrator function
module.exports = df.orchestrator(function* (context) {
const { sourceId, destinationId, transferAmount } = context.df.getInput();
const sourceEntity = new df.EntityId("Counter", sourceId);
const destinationEntity = new df.EntityId("Counter", destinationId);
// Create a critical section to avoid race conditions.
// No operations can be performed on either the source or
// destination accounts until the lock is released.
const lock = yield context.df.lock(sourceEntity, destinationEntity);
try {
const sourceBalance = yield context.df.callEntity(sourceEntity, "get");
if (sourceBalance >= transferAmount) {
yield context.df.callEntity(sourceEntity, "add", -transferAmount);
yield context.df.callEntity(destinationEntity, "add", transferAmount);
// the transfer succeeded
return true;
} else {
// the transfer failed due to insufficient funds
return false;
}
} finally {
lock.release();
}
});
在 JavaScript 中,context.df.lock 会返回一个 DurableLock。 调用 release 来结束临界区,或者在协调流程结束时让运行时自动释放锁。
在上面的示例中,业务流程协调程序函数已将资金从源实体转到目标实体。 该 lock 方法会锁定源账户和目的账户实体。 这种锁定确保在编排逻辑释放锁之前,没有其他客户端可以查询或修改任一账户的状态。 此行为可防止透支源帐户。
Durable Functions不支持Go。 要查看 Go 示例,请在本文章顶部选择 “持久任务 SDK” 。
注释
当业务流程终止(正常终止,或终止并出错)时,正在进行的所有关键节都会隐式结束,系统会释放所有锁。
关键节的行为
LockAsync 方法在编排中创建一个关键节。 这些关键部分可防止其他编排对一组指定的实体进行重叠更改。 在内部,LockAsync API 将“锁定”操作发送到实体,并在收到来自每个实体的“已获取锁”响应后返回。 锁定和解锁是所有实体都支持的内置操作。
其他客户端无法在实体被锁定时对其执行操作。 此行为确保每次只有一个编排实例可以锁定某个实体。 如果调用方尝试针对业务流程锁定的实体调用某个操作,该操作将被置于挂起操作队列中。 只有在持有锁的业务流程释放其锁之后,才会处理挂起的操作。
注释
此行为与大多数编程语言中使用的同步基元(例如 C# 中的 lock 语句)略有不同。 例如,在 C# 中,所有线程都需要使用 lock 语句来确保正确同步。 但是,实体不要求所有调用方必须显式锁定实体。 如果任一调用方锁定实体,则针对该实体的所有其他操作将被阻止,并排队在该锁的后面。
实体中的锁是持久性的,因此,即使回收了执行进程,这些锁也仍会保留。 系统将锁作为实体持久化状态的一部分进行保存。
与事务不同,在出错时关键节不会自动回滚更改。 请编写错误处理机制,例如通过捕获错误或异常来实现回滚或重试。 这是有意设计的。 一般情况下,很难或者根本无法自动回滚业务流程造成的所有影响,因为业务流程可能会运行活动,并调用无法回滚的外部服务。 此外,回滚尝试本身可能会失败,需要进一步进行错误处理。
context.df.lock 方法在编排中创建一个关键节。 这些关键部分可防止其他编排对一组指定的实体进行重叠更改。 在内部,lock API 向各个实体发送“锁定”操作,并在收到每个实体的“已获取锁”响应后返回一个 DurableLock。 锁定和解锁是所有实体都支持的内置操作。
注释
该 context.df.lock API 要求 3.4.0 npm 包的版本为 durable-functions 或更高版本,并且 3.13.0 扩展的版本为 Microsoft.Azure.WebJobs.Extensions.DurableTask 或更高版本。 在扩展包中出现v3.13.0之前,请 手动安装扩展。
查看扩展包版本发布时间。
要结束临界区,请对返回的 DurableLock 调用 release,或者让运行时在编排结束时自动释放该锁。 关键工作一完成就释放锁,可以让其他业务流程更早获取锁。 使用 context.df.isLocked 检查某个锁当前是否已被持有。
const lock = yield context.df.lock(sourceEntity, destinationEntity);
try {
yield context.df.callEntity(sourceEntity, "add", -amount);
yield context.df.callEntity(destinationEntity, "add", amount);
} finally {
lock.release();
}
其他客户端无法在实体被锁定时对其执行操作。 此行为确保每次只有一个编排实例可以锁定某个实体。 如果调用方尝试针对业务流程锁定的实体调用某个操作,该操作将被置于挂起操作队列中。 只有在持有锁的业务流程释放其锁之后,才会处理挂起的操作。
注释
这种行为与大多数编程语言中使用的同步原语略有不同。 实体并不要求所有呼叫者都必须明确获得锁。 如果任一调用方锁定实体,则针对该实体的所有其他操作将被阻止,并排队在该锁的后面。
实体中的锁是持久性的,因此,即使回收了执行进程,这些锁也仍会保留。 系统将锁作为实体持久化状态的一部分进行保存。
与事务不同,在出错时关键节不会自动回滚更改。 相反,应编写错误处理逻辑,例如回滚或重试,可通过捕获错误来实现。 这是有意设计的。 一般情况下,很难或者根本无法自动回滚业务流程造成的所有影响,因为业务流程可能会运行活动,并调用无法回滚的外部服务。 此外,回滚尝试本身可能会失败,需要进一步进行错误处理。
Durable Functions不支持Go。 要查看 Go 示例,请在本文章顶部选择 “持久任务 SDK” 。
关键节规则
与大多数编程语言中的低级锁定原语不同,临界部分 保证不会死锁。 为防止死锁,系统会强制执行以下限制:
- 不能嵌套临界区。
- 在临界区中无法创建子业务流程。
- 只能调用锁定在临界区的实体。
- 你不能通过在关键区段使用多个并行调用来调用同一个实体。
- 在临界区中,只能向锁集外的实体发送信号。
如果你违反这些规则,运行时会抛出错误,比如LockingRulesViolationException在 .NET 中。 错误信息说明了违反了哪条规则。
如果你违反了这些规则中的任意一个,运行时会报错。 错误信息说明了违反了哪条规则。
Durable Functions不支持Go。 要查看 Go 示例,请在本文章顶部选择 “持久任务 SDK” 。
将持久实体与虚拟演员进行比较
持久实体使用 执行组件模型中的许多想法。 如果你熟悉参与者,可能会认出本文中的一些概念。 持久实体非常类似于虚拟执行组件,也称为 Orleans 项目中的“粒度”。 例如:
- 使用实体 ID 标识持久实体。
- 持久实体操作连续运行以防止竞争情况。
- 调用实体或向实体发出信号会隐式创建实体。
- 运行时在未运行作时从内存中卸载实体。
关键不同点包括:
- 持久实体将持久性优先于延迟,因此它们可能不适合具有严格延迟要求的应用程序。
- 持久实体不会使消息超时。 Orleans 在可配置的时间段(默认为 30 秒)后超时消息。
- 实体可靠地按顺序传递消息。 Orleans 支持流消息的可靠、有序传递,但它不能保证所有粒度之间的消息都是如此。
- 业务流程是唯一可以对实体使用请求-响应的地方。 在实体内,使用单向消息传送(信号),就像在原始执行组件模型中一样。
- 持久实体不会死锁。 Orleans 会发生死锁,死锁将一直持续到消息超时。
- 持久实体与持久业务流程一起工作,并支持分布式锁定机制。
后续步骤