Funções da entidade

As funções de entidade definem operações para leitura e atualização de pequenos pedaços de estado, conhecidas como entidades duráveis. Como as funções de orquestrador, as funções de entidade são funções com um tipo de gatilho especial, o gatilho de entidade. Ao contrário das funções orquestradoras, as funções de entidade gerenciam o estado de uma entidade explicitamente, em vez de representar implicitamente o estado por meio do fluxo de controle. As entidades fornecem um meio para expandir aplicativos distribuindo o trabalho entre muitas entidades, cada uma com um estado de tamanho modesto.

Nota

As funções de entidade e as funcionalidades relacionadas só estão disponíveis em Durable Functions 2.0 e superior. Atualmente, eles são suportados no .NET in-proc, no trabalhador isolado do .NET, no JavaScript e no Python, mas não no PowerShell ou no Java.

Importante

Atualmente, as funções de entidade não são suportadas no PowerShell e no Java.

Conceitos gerais

As entidades comportam-se um pouco como pequenos serviços que comunicam através de mensagens. Cada entidade tem uma identidade única e um estado interno (se existir). Como serviços ou objetos, as entidades executam operações quando solicitadas a fazê-lo. Quando uma operação é executada, ela pode atualizar o estado interno da entidade. Também pode ligar para serviços externos e aguardar uma resposta. As entidades se comunicam com outras entidades, orquestrações e clientes usando mensagens que são enviadas implicitamente por meio de filas confiáveis.

Para evitar conflitos, todas as operações em uma única entidade têm a garantia de serem executadas em série, ou seja, uma após a outra.

Nota

Quando uma entidade é invocada, ela processa sua carga até a conclusão e, em seguida, agenda uma nova execução para ser ativada assim que as entradas futuras chegarem. Como resultado, os logs de execução da entidade podem mostrar uma execução extra após cada chamada de entidade; isso é esperado.

ID da Entidade

As entidades são acedidas através de um identificador único, o ID da entidade. Um ID de entidade é simplesmente um par de cadeias de caracteres que identifica exclusivamente uma instância de entidade. Consiste em:

  • Nome da entidade, que é um nome que identifica o tipo da entidade. Um exemplo é "Contador". Esse nome deve corresponder ao nome da função de entidade que implementa a entidade. Não é sensível ao caso.
  • Chave de entidade, que é uma cadeia de caracteres que identifica exclusivamente a entidade entre todas as outras entidades com o mesmo nome. Um exemplo é um GUID.

Por exemplo, uma função de Counter entidade pode ser usada para manter a pontuação em um jogo online. Cada instância do jogo tem um ID de entidade exclusivo, como @Counter@Game1 e @Counter@Game2. Todas as operações destinadas a uma entidade específica requerem a especificação de um ID de entidade como parâmetro.

Operações da entidade

Para invocar uma operação em uma entidade, especifique:

  • ID da entidade de destino.
  • Nome da operação, que é uma cadeia de caracteres que especifica a operação a ser executada. Por exemplo, a Counter entidade poderia apoiar add, getou reset operações.
  • Entrada de operação, que é um parâmetro de entrada opcional para a operação. Por exemplo, a operação add pode ter uma quantidade inteira como entrada.
  • Hora agendada, que é um parâmetro opcional para especificar o tempo de entrega da operação. Por exemplo, uma operação pode ser programada de forma confiável para ser executada vários dias no futuro.

As operações podem retornar um valor de resultado ou um resultado de erro, como um erro JavaScript ou uma exceção .NET. Esse resultado ou erro ocorre em orquestrações que chamaram a operação.

Uma operação de entidade também pode criar, ler, atualizar e excluir o estado da entidade. O estado da entidade é sempre persistente de forma duradoura no armazenamento.

Definir entidades

Você define entidades usando uma sintaxe baseada em função, onde as entidades são representadas como funções e as operações são explicitamente despachadas pelo aplicativo.

Atualmente, existem duas APIs distintas para definir entidades no .NET:

Quando você usa uma sintaxe baseada em função, as entidades são representadas como funções e as operações são explicitamente despachadas pelo aplicativo. Essa sintaxe funciona bem para entidades com estado simples, poucas operações ou um conjunto dinâmico de operações, como em estruturas de aplicativos. Essa sintaxe pode ser tediosa de manter porque não deteta erros de tipo em tempo de compilação.

As APIs específicas dependem se suas funções C# são executadas em um processo de trabalho isolado (recomendado) ou no mesmo processo que o host.

O código a seguir é um exemplo de uma entidade simples Counter implementada como uma função durável. Esta função define três operações, , , e , addresetgetcada uma das quais opera em um estado inteiro.

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

Para obter mais informações sobre a sintaxe baseada em função e como usá-la, consulte Sintaxe baseada em função.

As entidades duráveis estão disponíveis em JavaScript a partir da versão 1.3.0 do durable-functions pacote npm. O código a seguir é a entidade implementada Counter como uma função durável escrita em JavaScript.

Contador/função.json

{
  "bindings": [
    {
      "name": "context",
      "type": "entityTrigger",
      "direction": "in"
    }
  ],
  "disabled": false
}

Contador/índice.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;
    }
});

O código a seguir é a entidade implementada Counter como uma função durável escrita em 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)

Entidades de acesso

As entidades podem ser acessadas usando comunicação unidirecional ou bidirecional. A seguinte terminologia distingue as duas formas de comunicação:

  • Chamar uma entidade usa comunicação bidirecional (ida e volta). Você envia uma mensagem de operação para a entidade e, em seguida, aguarde a mensagem de resposta antes de continuar. A mensagem de resposta pode fornecer um valor de resultado ou um resultado de erro, como um erro JavaScript ou uma exceção .NET. Este resultado ou erro é então observado pelo chamador.
  • A sinalização de uma entidade usa comunicação unidirecional (disparar e esquecer). Você envia uma mensagem de operação, mas não espera por uma resposta. Embora a mensagem seja garantida para ser entregue eventualmente, o remetente não sabe quando e não pode observar qualquer valor de resultado ou erros.

As entidades podem ser acessadas de dentro das funções do cliente, das funções do orquestrador ou das funções da entidade. Nem todas as formas de comunicação são suportadas por todos os contextos:

  • De dentro dos clientes, você pode sinalizar entidades e ler o estado da entidade.
  • A partir de orquestrações, você pode sinalizar entidades e chamar entidades.
  • A partir de entidades, você pode sinalizar entidades.

Os exemplos a seguir ilustram essas várias maneiras de acessar entidades.

Exemplo: Cliente sinaliza uma entidade

Para acessar entidades de uma Função do Azure comum, que também é conhecida como uma função de cliente, use a associação de cliente de entidade. O exemplo a seguir mostra uma função acionada por fila sinalizando uma entidade usando essa ligação.

Nota

Para simplificar, os exemplos a seguir mostram a sintaxe vagamente tipada para acessar entidades. Em geral, recomendamos que você acesse entidades por meio de interfaces , pois isso fornece mais verificação de tipo.

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

O termo sinal significa que a invocação da API da entidade é unidirecional e assíncrona. Não é possível para uma função de cliente saber quando a entidade processou a operação. Além disso, a função cliente não pode observar quaisquer valores de resultado ou exceções.

Exemplo: O cliente lê um estado de entidade

As funções de cliente também podem consultar o estado de uma entidade, conforme mostrado no exemplo a seguir:

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

As consultas de estado da entidade são enviadas para o repositório de rastreamento durável e retornam o estado persistente mais recente da entidade. Este estado é sempre um estado "comprometido", ou seja, nunca é um estado intermediário temporário assumido no meio da execução de uma operação. No entanto, é possível que esse estado esteja obsoleto em comparação com o estado na memória da entidade. Somente orquestrações podem ler o estado na memória de uma entidade, conforme descrito na seção a seguir.

Exemplo: Orquestração sinaliza e chama uma entidade

As funções do Orchestrator podem acessar entidades usando APIs na ligação de gatilho de orquestração. O código de exemplo a seguir mostra uma função orchestrator chamando e sinalizando uma Counter entidade.

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

Nota

Atualmente, o JavaScript não suporta a sinalização de uma entidade de um orquestrador. Utilize callEntity em substituição.

@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

Apenas orquestrações são capazes de chamar entidades e obter uma resposta, que pode ser um valor de retorno ou uma exceção. As funções do cliente que usam a vinculação do cliente só podem sinalizar entidades.

Nota

Chamar uma entidade de uma função de orquestrador é semelhante a chamar uma função de atividade de uma função de orquestrador. A principal diferença é que as funções de entidade são objetos duráveis com um endereço, que é o ID da entidade. As funções de entidade suportam a especificação de um nome de operação. As funções de atividade, por outro lado, são apátridas e não têm o conceito de operações.

Exemplo: Entidade sinaliza uma entidade

Uma função de entidade pode enviar sinais para outras entidades, ou mesmo para si mesma, enquanto executa uma operação. Por exemplo, podemos modificar o exemplo de entidade anterior Counter para que ele envie um sinal de "marco alcançado" para alguma entidade de monitoramento quando o contador atingir o valor 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;

Nota

Python ainda não suporta sinais de entidade para entidade. Em vez disso, use um orquestrador para sinalizar entidades.

Coordenação das entidades

Pode haver momentos em que você precise coordenar operações entre várias entidades. Por exemplo, em um aplicativo bancário, você pode ter entidades que representam contas bancárias individuais. Quando transfere fundos de uma conta para outra, tem de garantir que a conta de origem tem fundos suficientes. Você também deve garantir que as atualizações das contas de origem e de destino sejam feitas de forma transacionalmente consistente.

Exemplo: Transferir fundos

O código de exemplo a seguir transfere fundos entre duas entidades de conta usando uma função orchestrator. A coordenação de atualizações de entidade requer o uso do LockAsync método para criar uma seção crítica na orquestração.

Nota

Para simplificar, este exemplo reutiliza a Counter entidade definida anteriormente. Numa aplicação real, seria melhor definir uma entidade mais detalhada BankAccount .

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

No .NET, retorna IDisposable, LockAsync que termina a seção crítica quando descartada. Este IDisposable resultado pode ser usado em conjunto com um using bloco para obter uma representação sintática da seção crítica.

No exemplo anterior, uma função orchestrator transfere fundos de uma entidade de origem para uma entidade de destino. O LockAsync método bloqueou as entidades da conta de origem e de destino. Esse bloqueio garantiu que nenhum outro cliente pudesse consultar ou modificar o estado de qualquer conta até que a lógica de orquestração saísse da seção crítica no final da using instrução. Esse comportamento evita a possibilidade de saque excessivo da conta de origem.

Nota

Quando uma orquestração termina, normalmente ou com um erro, todas as seções críticas em andamento são implicitamente encerradas e todos os bloqueios são liberados.

Comportamento da seção crítica

O LockAsync método cria uma seção crítica em uma orquestração. Essas seções críticas impedem que outras orquestrações façam alterações sobrepostas em um conjunto especificado de entidades. Internamente, a LockAsync API envia operações de "bloqueio" para as entidades e retorna quando recebe uma mensagem de resposta de "bloqueio adquirido" de cada uma dessas mesmas entidades. Tanto o bloqueio como o desbloqueio são operações incorporadas suportadas por todas as entidades.

Nenhuma operação de outros clientes é permitida em uma entidade enquanto ela estiver em um estado bloqueado. Esse comportamento garante que apenas uma instância de orquestração possa bloquear uma entidade de cada vez. Se um chamador tentar invocar uma operação em uma entidade enquanto ela estiver bloqueada por uma orquestração, essa operação será colocada em uma fila de operações pendente. Nenhuma operação pendente é processada até que a orquestração de retenção libere seu bloqueio.

Nota

Esse comportamento é ligeiramente diferente das primitivas de sincronização usadas na maioria das linguagens de programação, como a lock instrução em C#. Por exemplo, em C#, a instrução deve ser usada por todos os threads para garantir a lock sincronização adequada entre vários threads. As entidades, no entanto, não exigem que todos os chamadores bloqueiem explicitamente uma entidade. Se algum chamador bloquear uma entidade, todas as outras operações nessa entidade serão bloqueadas e enfileiradas atrás desse bloqueio.

Os bloqueios em entidades são duráveis, por isso persistem mesmo que o processo de execução seja reciclado. Os bloqueios são persistidos internamente como parte do estado durável de uma entidade.

Ao contrário das transações, as seções críticas não revertem automaticamente as alterações quando ocorrem erros. Em vez disso, qualquer tratamento de erros, como reversão ou nova tentativa, deve ser explicitamente codificado, por exemplo, detetando erros ou exceções. Esta escolha de design é intencional. Reverter automaticamente todos os efeitos de uma orquestração é difícil ou impossível em geral, porque as orquestrações podem executar atividades e fazer chamadas para serviços externos que não podem ser revertidos. Além disso, as tentativas de reverter podem falhar e exigir mais tratamento de erros.

Regras da secção crítica

Ao contrário das primitivas de bloqueio de baixo nível na maioria das linguagens de programação, as seções críticas têm a garantia de não ficarem bloqueadas. Para evitar impasses, aplicamos as seguintes restrições:

  • As seções críticas não podem ser aninhadas.
  • Seções críticas não podem criar suborquestrações.
  • As seções críticas podem chamar apenas entidades que bloquearam.
  • As seções críticas não podem chamar a mesma entidade usando várias chamadas paralelas.
  • As seções críticas podem sinalizar apenas entidades que não bloquearam.

Qualquer violação dessas regras causa um erro de tempo de execução, como LockingRulesViolationException no .NET, que inclui uma mensagem que explica qual regra foi quebrada.

Comparação com atores virtuais

Muitas das características das entidades duráveis são inspiradas no modelo do ator. Se você já está familiarizado com atores, talvez reconheça muitos dos conceitos descritos neste artigo. As entidades duráveis são semelhantes aos atores virtuais, ou grãos, como popularizado pelo projeto de Orleans. Por exemplo:

  • As entidades duráveis são endereçáveis por meio de um ID de entidade.
  • As operações duráveis da entidade são executadas em série, uma de cada vez, para evitar condições de corrida.
  • As entidades duráveis são criadas implicitamente quando são chamadas ou sinalizadas.
  • As entidades duráveis são silenciosamente descarregadas da memória quando não executam operações.

Existem algumas diferenças importantes que merecem ser notadas:

  • As entidades duráveis priorizam a durabilidade sobre a latência e, portanto, podem não ser apropriadas para aplicativos com requisitos estritos de latência.
  • As entidades duráveis não têm tempos limite internos para mensagens. Em Orleans, todas as mensagens expiram após um tempo configurável. A predefinição é 30 segundos.
  • As mensagens enviadas entre entidades são entregues de forma fiável e em ordem. Em Orleans, a entrega confiável ou ordenada é suportada para conteúdo enviado através de fluxos, mas não é garantida para todas as mensagens entre grãos.
  • Os padrões de solicitação-resposta em entidades são limitados a orquestrações. De dentro das entidades, apenas mensagens unidirecionais (também conhecidas como sinalização) são permitidas, como no modelo original de ator, e ao contrário dos grãos em Orleans.
  • Entidades duráveis não bloqueiam. Em Orleans, os impasses podem ocorrer e não se resolvem até que as mensagens atinjam o tempo limite.
  • Entidades duráveis podem ser usadas com orquestrações duráveis e suportam mecanismos de bloqueio distribuídos.

Próximos passos