El bloque de creación de actores de Dapr

Sugerencia

Este contenido es un extracto del libro electrónico, Dapr para desarrolladores de .NET, disponible en documentos de .NET o como un PDF descargable gratuito que se puede leer sin conexión.

Dapr for .NET Developers eBook cover thumbnail.

El modelo de actor se originó en 1973. Se propuso por Carl Hewitt como modelo conceptual de cálculo simultáneo, una forma de computación en la que se ejecutan varios cálculos al mismo tiempo. Los equipos muy paralelos aún no estaban disponibles en ese momento, pero los avances más recientes de las CPU de varios núcleos y los sistemas distribuidos han hecho que el modelo de actor sea popular.

En el modelo de actor, el actor es una unidad independiente de proceso y estado. Los actores están completamente aislados entre sí y nunca compartirán memoria. Los actores se comunican entre sí mediante mensajes. Cuando un actor recibe un mensaje, puede cambiar su estado interno y enviar mensajes a otros actores (posiblemente nuevos).

La razón por la que el modelo de actor facilita la escritura de sistemas simultáneos es que proporciona un modelo de acceso basado en turnos (o un solo subproceso). Varios actores se pueden ejecutar al mismo tiempo, pero cada actor procesará los mensajes recibidos de uno en uno. Esto significa que puede estar seguro de que, como máximo, un subproceso está activo dentro de un actor en cualquier momento. Esto facilita mucho la escritura de sistemas simultáneos y paralelos correctos.

Qué resuelve

Las implementaciones del modelo de actor suelen estar vinculadas a un lenguaje o plataforma específicos. Sin embargo, con el bloque de creación de actores de Dapr, puede aprovechar el modelo de actor desde cualquier lenguaje o plataforma.

La implementación de Dapr se basa en el patrón de actor virtual introducido por Project "Orleans". Con el patrón de actor virtual, no es necesario crear actores explícitamente. Los actores se activan implícitamente y se colocan en un nodo del clúster la primera vez que se envía un mensaje al actor. Al no ejecutar operaciones, los actores se descargan silenciosamente de la memoria. Si se produce un error en un nodo, Dapr mueve automáticamente los actores activados a nodos correctos. Además de enviar mensajes entre actores, el modelo de actor de Dapr también admite la programación del trabajo futuro mediante temporizadores y recordatorios.

Aunque el modelo de actor puede proporcionar grandes ventajas, es importante tener en cuenta cuidadosamente el diseño del actor. Por ejemplo, hacer que muchos clientes llamen al mismo actor provocarán un rendimiento deficiente porque las operaciones de actor se ejecutan en serie. Estos son algunos criterios para comprobar si un escenario es una buena opción para los actores de Dapr:

  • El espacio de problemas implica simultaneidad. Sin actores, tendría que introducir mecanismos de bloqueo explícitos en el código.
  • El espacio de problemas se puede dividir en unidades pequeñas, independientes y aisladas de estado y lógica.
  • No necesita lecturas de baja latencia del estado del actor. No se pueden garantizar lecturas de baja latencia porque las operaciones de actor se ejecutan en serie.
  • No es necesario consultar el estado en un conjunto de actores. La consulta entre actores es ineficaz porque el estado de cada actor debe leerse individualmente y puede introducir latencias impredecibles.

Un patrón de diseño que se ajusta a estos criterios es bastante bien la saga basada en orquestación o el patrón de diseño del administrador de procesos. Una saga administra una secuencia de pasos que se deben realizar para alcanzar algún resultado. La saga (o el administrador de procesos) mantiene el estado actual de la secuencia y desencadena el paso siguiente. Si se produce un error en un paso, la saga puede ejecutar acciones de compensación. Los actores facilitan el trato con la simultaneidad en la saga y realizan un seguimiento del estado actual. La aplicación de referencia eShopOnDapr usa el patrón saga y los actores dapr para implementar el proceso de pedidos.

Cómo funciona

El sidecar de Dapr proporciona la API HTTP/gRPC para invocar actores. Esta es la dirección URL base de la API HTTP:

http://localhost:<daprPort>/v1.0/actors/<actorType>/<actorId>/
  • <daprPort>: el puerto HTTP en el que Dapr escucha.
  • <actorType>: el tipo de actor.
  • <actorId>: el identificador del actor específico al que se va a llamar.

El sidecar administra cómo, cuándo y dónde se ejecuta cada actor, y también enruta los mensajes entre actores. Cuando un actor no se ha usado durante un período de tiempo, el tiempo de ejecución desactiva el actor y lo quita de la memoria. Cualquier estado administrado por el actor se conserva y estará disponible cuando el actor vuelva a activarse. Dapr usa un temporizador de inactividad para determinar cuándo se puede desactivar un actor. Cuando se llama a una operación en el actor (ya sea mediante una llamada de método o una activación de recordatorio), se restablece el temporizador de inactividad y la instancia del actor permanecerá activada.

La API sidecar es solo una parte de la ecuación. El propio servicio también debe implementar una especificación de API, ya que el código real que escriba para el actor se ejecutará dentro del propio servicio. En la figura 11-1 se muestran las distintas llamadas API entre el servicio y su sidecar:

Diagram of API calls between actor service and Dapr sidecar.

Figura 11-1. Llamadas API entre el servicio de actor y Dapr sidecar.

Para proporcionar escalabilidad y confiabilidad, los actores se dividen en todas las instancias del servicio de actor. El servicio de selección de ubicación de Dapr es responsable de realizar un seguimiento de la información de creación de particiones. Cuando se inicia una nueva instancia de un servicio de actor, sidecar registra los tipos de actor admitidos con el servicio de selección de ubicación. El servicio de selección de ubicación calcula la información de creación de particiones actualizada para el tipo de actor especificado y la difunde a todas las instancias. En la figura 11-2 se muestra lo que sucede cuando un servicio se escala horizontalmente a una segunda réplica:

Diagram of the actor placement service.

Figura 11-2. Servicio de selección de ubicación de actor.

  1. En el inicio, sidecar realiza una llamada al servicio de actor para obtener los tipos de actor registrados, así como los valores de configuración del actor.
  2. El sidecar envía la lista de tipos de actor registrados al servicio de selección de ubicación.
  3. El servicio de selección de ubicación difunde la información de creación de particiones actualizada a todas las instancias de servicio de actor. Cada instancia mantendrá una copia almacenada en caché de la información de creación de particiones y la usará para invocar actores.

Importante

Dado que los actores se distribuyen aleatoriamente entre instancias de servicio, debe esperarse que una operación de actor siempre requiera una llamada a otro nodo de la red.

En la ilustración siguiente se muestra una instancia de servicio de ordenación que se ejecuta en el pod 1, llame al ship método de una OrderActor instancia con el identificador 3. Dado que el actor con identificador 3 se coloca en una instancia diferente, esto da como resultado una llamada a otro nodo del clúster:

Diagram of calling an actor method.

Figura 11-3. Llamar a un método de actor.

  1. El servicio llama a la API de actor en sidecar. La carga JSON del cuerpo de la solicitud contiene los datos que se van a enviar al actor.
  2. El sidecar usa la información de creación de particiones almacenada localmente en caché del servicio de selección de ubicación para determinar qué instancia de servicio de actor (partición) es responsable de hospedar el actor con el identificador 3. En este ejemplo, es la instancia de servicio en el pod 2. La llamada se reenvía al sidecar adecuado.
  3. La instancia sidecar del pod 2 llama a la instancia de servicio para invocar al actor. La instancia de servicio activa el actor (si aún no lo ha hecho) y ejecuta el método de actor.

Modelo de acceso basado en turnos

El modelo de acceso basado en turnos garantiza que en cualquier momento haya como máximo un subproceso activo dentro de una instancia de actor. Para comprender por qué esto es útil, considere el ejemplo siguiente de un método que incrementa un valor de contador:

public int Increment()
{
    var currentValue = GetValue();
    var newValue = currentValue + 1;

    SaveValue(newValue);

    return newValue;
}

Supongamos que el valor actual devuelto por el GetValue método es 1. Cuando dos subprocesos llaman al Increment método al mismo tiempo, existe el riesgo de que ambos llamen al GetValue método antes de que uno de ellos llame a SaveValue. Esto da como resultado que ambos subprocesos comiencen por el mismo valor inicial (1). A continuación, los subprocesos incrementan el valor en 2 y lo devuelven al autor de la llamada. El valor resultante después de las dos llamadas ahora es 2 en lugar de 3 lo que debe ser. Este es un ejemplo sencillo para ilustrar el tipo de problemas que se pueden introducir en el código al trabajar con varios subprocesos y es fácil de resolver. Sin embargo, en las aplicaciones del mundo real, los escenarios simultáneos y paralelos pueden ser muy complejos.

En los modelos de programación tradicionales, puede resolver este problema mediante la introducción de mecanismos de bloqueo. Por ejemplo:

public int Increment()
{
    int newValue;

    lock (_lockObject)
    {
        var currentValue = GetValue();
        newValue = currentValue + 1;

        SaveValue(newValue);
    }

    return newValue;
}

Desafortunadamente, el uso de mecanismos de bloqueo explícitos es propenso a errores. Pueden conducir fácilmente a interbloqueos y pueden tener un impacto grave en el rendimiento.

Gracias al modelo de acceso basado en turnos, no es necesario preocuparse por varios subprocesos con actores, lo que facilita mucho la escritura de sistemas simultáneos. El ejemplo de actor siguiente refleja estrechamente el código del ejemplo anterior, pero no requiere que los mecanismos de bloqueo sean correctos:

public async Task<int> IncrementAsync()
{
    var counterValue = await StateManager.TryGetStateAsync<int>("counter");

    var currentValue = counterValue.HasValue ? counterValue.Value : 0;
    var newValue = currentValue + 1;

    await StateManager.SetStateAsync("counter", newValue);

    return newValue;
}

Recordatorios y temporizadores

Los actores pueden usar temporizadores y recordatorios para programar llamadas a sí mismos. Ambos conceptos admiten la configuración de un tiempo de vencimiento. La diferencia radica en la duración de los registros de devolución de llamada:

  • Los temporizadores solo permanecerán activos siempre que se active el actor. Los temporizadores no restablecerán el temporizador de inactividad, por lo que no pueden mantener un actor activo por sí mismo.
  • Recordatorios de activaciones de actor de vida. Si se desactiva un actor, se volverá a activar el actor. Los avisos restablecerán el temporizador de inactividad.

Los temporizadores se registran realizando una llamada a la API de actor. En el ejemplo siguiente, se registra un temporizador con un tiempo de vencimiento de 0 y un período de 10 segundos.

curl -X POST http://localhost:3500/v1.0/actors/<actorType>/<actorId>/timers/<name> \
  -H "Content-Type: application/json" \
  -d '{
        "dueTime": "0h0m0s0ms",
        "period": "0h0m10s0ms"
      }'

Dado que el tiempo de vencimiento es 0, el temporizador se activará inmediatamente. Una vez finalizada la devolución de llamada del temporizador, el temporizador esperará 10 segundos antes de volver a activarse.

Los recordatorios se registran de forma similar. En el ejemplo siguiente se muestra un registro de recordatorio con un tiempo de vencimiento de 5 minutos y un período vacío:

curl -X POST http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name> \
  -H "Content-Type: application/json" \
  -d '{
        "dueTime": "0h5m0s0ms",
        "period": ""
      }'

Este recordatorio se activará en 5 minutos. Dado que el período dado está vacío, este será un recordatorio único.

Nota:

Los temporizadores y recordatorios respetan el modelo de acceso basado en turnos. Cuando se activa un temporizador o un recordatorio, la devolución de llamada no se ejecutará hasta que haya finalizado cualquier otra invocación de método o devolución de llamada de temporizador o recordatorio.

Persistencia del estado

El estado del actor se conserva mediante el bloque de creación de administración de estado de Dapr. Dado que los actores pueden ejecutar varias operaciones de estado en un solo turno, el componente de almacén de estado debe admitir transacciones de varios elementos. En el momento de escribir, los siguientes almacenes de estado admiten transacciones de varios elementos:

  • Azure Cosmos DB
  • MongoDB
  • MySQL
  • PostgreSQL
  • Redis
  • ReplantearDB
  • SQL Server

Para configurar un componente de almacén de estado para usarlo con actores, debe anexar los siguientes metadatos a la configuración del almacén de estado:

- name: actorStateStore
  value: "true"

Este es un ejemplo completo de un almacén de estado de Redis:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"

Uso del SDK de .NET de Dapr

Puede crear una implementación de modelo de actor con solo llamadas HTTP/gRPC. Sin embargo, es mucho más conveniente usar los SDK de Dapr específicos del lenguaje. En el momento de escribir, los SDK de .NET, Java y Python proporcionan una amplia compatibilidad para trabajar con actores.

Para empezar a trabajar con el SDK de actores dapr de .NET, agregue una referencia de paquete al Dapr.Actors proyecto de servicio. El primer paso para crear un actor real es definir una interfaz que deriva de IActor. Los clientes usan la interfaz para invocar operaciones en el actor. Este es un ejemplo sencillo de una interfaz de actor para mantener las puntuaciones:

public interface IScoreActor : IActor
{
    Task<int> IncrementScoreAsync();

    Task<int> GetScoreAsync();
}

Importante

El tipo de valor devuelto de un método de actor debe ser Task o Task<T>. Además, los métodos de actor pueden tener como máximo un argumento. Tanto el tipo de valor devuelto como los argumentos deben ser System.Text.Json serializables.

A continuación, implemente el actor derivando una ScoreActor clase de Actor. La ScoreActor clase también debe implementar la IScoreActor interfaz :

public class ScoreActor : Actor, IScoreActor
{
    public ScoreActor(ActorHost host) : base(host)
    {
    }

    // TODO Implement interface methods.
}

El constructor del fragmento de código anterior toma un host argumento de tipo ActorHost. La ActorHost clase representa el host de un tipo de actor dentro del tiempo de ejecución del actor. Debe pasar este argumento al constructor de la Actor clase base. Los actores también admiten la inserción de dependencias. Los argumentos adicionales que agregue al constructor de actor se resuelven mediante el contenedor de inserción de dependencias de .NET.

Ahora vamos a implementar el IncrementScoreAsync método de la interfaz :

public Task<int> IncrementScoreAsync()
{
    return StateManager.AddOrUpdateStateAsync(
        "score",
        1,
        (key, currentScore) => currentScore + 1
    );
}

En el fragmento de código anterior, una sola llamada a StateManager.AddOrUpdateStateAsync proporciona la implementación completa del IncrementScoreAsync método . El AddOrUpdateStateAsync método toma tres argumentos:

  1. Clave del estado que se va a actualizar.
  2. Valor que se va a escribir si aún no hay ninguna puntuación almacenada en el almacén de estado.
  3. para Func llamar a si ya hay una puntuación almacenada en el almacén de estado. Toma la clave de estado y la puntuación actual, y devuelve la puntuación actualizada para volver a escribir en el almacén de estado.

La GetScoreAsync implementación lee la puntuación actual del almacén de estado y la devuelve al cliente:

public async Task<int> GetScoreAsync()
{
    var scoreValue = await StateManager.TryGetStateAsync<int>("score");
    if (scoreValue.HasValue)
    {
        return scoreValue.Value;
    }

    return 0;
}

Para hospedar actores en un servicio ASP.NET Core, debe agregar una referencia al Dapr.Actors.AspNetCore paquete y realizar algunos cambios en el Program archivo. En el ejemplo siguiente, la llamada a registra MapActorsHandlers los puntos de conexión de Dapr Actor en ASP.NET Core enrutamiento:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Actors building block does not support HTTPS redirection.
//app.UseHttpsRedirection();
app.MapControllers();
// Add actor endpoints.
app.MapActorsHandlers();

Los puntos de conexión de actores son necesarios porque Dapr sidecar llama a la aplicación para hospedar e interactuar con instancias de actor.

Importante

Asegúrese de que la Startup clase no contiene una app.UseHttpsRedirection llamada para redirigir los clientes al punto de conexión HTTPS. Esto no funcionará con actores. Por diseño, un sidecar dapr envía solicitudes a través de HTTP sin cifrar de forma predeterminada. El middleware HTTPS bloqueará estas solicitudes cuando se habilite.

El Program archivo también es el lugar para registrar los tipos de actor específicos. En el ejemplo siguiente se registra el ScoreActor mediante el método de AddActors extensión :

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddActors(options =>
{
    options.Actors.RegisterActor<ScoreActor>();
});

En este momento, el servicio ASP.NET Core está listo para hospedar y ScoreActor aceptar solicitudes entrantes. Las aplicaciones cliente usan servidores proxy de actor para invocar operaciones en actores. En el ejemplo siguiente se muestra cómo una aplicación cliente de consola invoca la IncrementScoreAsync operación en una ScoreActor instancia de :

var actorId = new ActorId("scoreActor1");

var proxy = ActorProxy.Create<IScoreActor>(actorId, "ScoreActor");

var score = await proxy.IncrementScoreAsync();

Console.WriteLine($"Current score: {score}");

En el ejemplo anterior se usa el Dapr.Actors paquete para llamar al servicio de actor. Para invocar una operación en un actor, debe poder abordarla. Necesitará dos partes para esto:

  1. El tipo de actor identifica de forma única la implementación del actor en toda la aplicación. De forma predeterminada, el tipo de actor es el nombre de la clase de implementación (sin espacio de nombres). Puede personalizar el tipo de actor agregando un ActorAttribute elemento a la clase de implementación y estableciendo su TypeName propiedad.
  2. Identifica ActorId de forma única una instancia de un tipo de actor. También puede usar esta clase para generar un identificador de actor aleatorio llamando a ActorId.CreateRandom.

En el ejemplo se usa ActorProxy.Create para crear una instancia de proxy para .ScoreActor El Create método toma dos argumentos: la ActorId identificación del actor específico y el tipo de actor. También tiene un parámetro de tipo genérico para especificar la interfaz de actor que implementa el tipo de actor. Como las aplicaciones cliente y de servidor necesitan usar las interfaces de actor, normalmente se almacenan en un proyecto compartido independiente.

El último paso del ejemplo llama al IncrementScoreAsync método en el actor y genera el resultado. Recuerde que el servicio de selección de ubicación de Dapr distribuye las instancias de actor entre los sidecars de Dapr. Por lo tanto, espere que una llamada de actor sea una llamada de red a otro nodo.

Llamar a actores desde clientes de ASP.NET Core

En el ejemplo de cliente de consola de la sección anterior se usa el método estático ActorProxy.Create directamente para obtener una instancia de proxy de actor. Si la aplicación cliente es una aplicación ASP.NET Core, debe usar la IActorProxyFactory interfaz para crear servidores proxy de actor. La principal ventaja es que permite administrar la configuración en un solo lugar. El AddActors método de extensión toma IServiceCollection un delegado que permite especificar opciones de tiempo de ejecución de actor, como el punto de conexión HTTP del sidecar de Dapr. En el ejemplo siguiente se especifica el uso personalizado JsonSerializerOptions para la persistencia del estado de actor y la deserialización de mensajes:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddActors(options =>
{
    var jsonSerializerOptions = new JsonSerializerOptions()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true
    };

    options.JsonSerializerOptions = jsonSerializerOptions;
    options.Actors.RegisterActor<ScoreActor>();
});

La llamada para AddActors registrar la IActorProxyFactory inserción de dependencias de .NET. Esto permite ASP.NET Core insertar una IActorProxyFactory instancia en las clases de controlador. En el ejemplo siguiente se llama a un método de actor desde una clase de controlador ASP.NET Core:

[ApiController]
[Route("[controller]")]
public class ScoreController : ControllerBase
{
    private readonly IActorProxyFactory _actorProxyFactory;

    public ScoreController(IActorProxyFactory actorProxyFactory)
    {
        _actorProxyFactory = actorProxyFactory;
    }

    [HttpPut("{scoreId}")]
    public Task<int> IncrementAsync(string scoreId)
    {
        var scoreActor = _actorProxyFactory.CreateActorProxy<IScoreActor>(
            new ActorId(scoreId),
            "ScoreActor");

        return scoreActor.IncrementScoreAsync();
    }
}

Los actores también pueden llamar directamente a otros actores. La Actor clase base expone una IActorProxyFactory clase a través de la ProxyFactory propiedad . Para crear un proxy de actor desde un actor, use la ProxyFactory propiedad de la Actor clase base. En el ejemplo siguiente se muestra un OrderActor objeto que invoca operaciones en otros dos actores:

public class OrderActor : Actor, IOrderActor
{
    public OrderActor(ActorHost host) : base(host)
    {
    }

    public async Task ProcessOrderAsync(Order order)
    {
        var stockActor = ProxyFactory.CreateActorProxy<IStockActor>(
            new ActorId(order.OrderNumber),
            "StockActor");

        await stockActor.ReserveStockAsync(order.OrderLines);

        var paymentActor = ProxyFactory.CreateActorProxy<IPaymentActor>(
            new ActorId(order.OrderNumber),
            "PaymentActor");

        await paymentActor.ProcessPaymentAsync(order.PaymentDetails);
    }
}

Nota:

De forma predeterminada, los actores dapr no son reentrantes. Esto significa que no se puede llamar a un actor dapr más de una vez en la misma cadena. Por ejemplo, no se permite la cadena Actor A -> Actor B -> Actor A de llamadas. En el momento de escribir, hay una característica en versión preliminar disponible para admitir la reentrada. Sin embargo, aún no hay compatibilidad con el SDK. Para más información, consulte la documentación oficial.

Llamada a actores de non-.NET

Hasta ahora, los ejemplos usaban servidores proxy de actor fuertemente tipados basados en interfaces de .NET para ilustrar las invocaciones de actor. Esto funciona bien cuando tanto el host de actor como el cliente son aplicaciones .NET. Sin embargo, si el host de actor no es una aplicación .NET, no tiene una interfaz de actor para crear un proxy fuertemente tipado. En estos casos, puede usar un proxy con tipo débil.

Los servidores proxy con tipo débil se crean de forma similar a los servidores proxy fuertemente tipados. En lugar de confiar en una interfaz de .NET, debe pasar el nombre del método de actor como una cadena.

[HttpPut("{scoreId}")]
public Task<int> IncrementAsync(string scoreId)
{
    var scoreActor = _actorProxyFactory.CreateActorProxy(
        new ActorId(scoreId),
        "ScoreActor");

    return scoreActor("IncrementScoreAsync");
}

Recordatorios y temporizadores

Use el RegisterTimerAsync método de la Actor clase base para programar temporizadores de actor. En el ejemplo siguiente, un objeto TimerActor expone un StartTimerAsync método . Los clientes pueden llamar al método para iniciar un temporizador que escribe repetidamente un texto determinado en la salida del registro.

public class TimerActor : Actor, ITimerActor
{
    public TimerActor(ActorHost host) : base(host)
    {
    }

    public Task StartTimerAsync(string name, string text)
    {
        return RegisterTimerAsync(
            name,
            nameof(TimerCallback),
            Encoding.UTF8.GetBytes(text),
            TimeSpan.Zero,
            TimeSpan.FromSeconds(3));
    }

    public Task TimerCallbackAsync(byte[] state)
    {
        var text = Encoding.UTF8.GetString(state);

        Logger.LogInformation($"Timer fired: {text}");

        return Task.CompletedTask;
    }
}

El StartTimerAsync método llama RegisterTimerAsync a para programar el temporizador. RegisterTimerAsync toma cinco argumentos:

  1. Nombre del temporizador.
  2. Nombre del método al que se llamará cuando se active el temporizador.
  3. Estado que se va a pasar al método de devolución de llamada.
  4. Cantidad de tiempo que se debe esperar antes de invocar el método de devolución de llamada por primera vez.
  5. Intervalo de tiempo entre invocaciones de método de devolución de llamada. Puede especificar TimeSpan.FromMilliseconds(-1) para deshabilitar la señalización periódica.

El TimerCallbackAsync método recibe el estado de usuario en formato binario. En el ejemplo, la devolución de llamada descodifica el estado en un string antes de escribirlo en el registro.

Los temporizadores se pueden detener llamando a UnregisterTimerAsync:

public class TimerActor : Actor, ITimerActor
{
    // ...

    public Task StopTimerAsync(string name)
    {
        return UnregisterTimerAsync(name);
    }
}

Recuerde que los temporizadores no restablecen el temporizador de inactividad del actor. Cuando no se realizan otras llamadas en el actor, puede desactivarse y el temporizador se detendrá automáticamente. Para programar el trabajo que restablece el temporizador de inactividad, use recordatorios que veremos a continuación.

Para usar recordatorios en un actor, la clase de actor debe implementar la IRemindable interfaz :

public interface IRemindable
{
    Task ReceiveReminderAsync(
        string reminderName, byte[] state,
        TimeSpan dueTime, TimeSpan period);
}

Se ReceiveReminderAsync llama al método cuando se desencadena un aviso. Toma 4 argumentos:

  1. Nombre del aviso.
  2. Estado de usuario proporcionado durante el registro.
  3. Tiempo de vencimiento de invocación proporcionado durante el registro.
  4. Período de invocación proporcionado durante el registro.

Para registrar un recordatorio, use el RegisterReminderAsync método de la clase base de actor. En el ejemplo siguiente se establece un aviso para que se active una sola vez con un tiempo de vencimiento de tres minutos.

public class ReminderActor : Actor, IReminderActor, IRemindable
{
    public ReminderActor(ActorHost host) : base(host)
    {
    }

    public Task SetReminderAsync(string text)
    {
        return RegisterReminderAsync(
            "DoNotForget",
            Encoding.UTF8.GetBytes(text),
            TimeSpan.FromSeconds(3),
            TimeSpan.FromMilliseconds(-1));
    }

    public Task ReceiveReminderAsync(
        string reminderName, byte[] state,
        TimeSpan dueTime, TimeSpan period)
    {
        if (reminderName == "DoNotForget")
        {
            var text = Encoding.UTF8.GetString(state);

            Logger.LogInformation($"Don't forget: {text}");
        }

        return Task.CompletedTask;
    }
}

El RegisterReminderAsync método es similar a RegisterTimerAsync pero no es necesario especificar explícitamente un método de devolución de llamada. Como se muestra en el ejemplo anterior, se implementa IRemindable.ReceiveReminderAsync para controlar los avisos desencadenados.

Los recordatorios restablecen el temporizador de inactividad y son persistentes. Incluso si el actor está desactivado, se reactivará en el momento en que se active un aviso. Para detener la activación de un recordatorio, llame a UnregisterReminderAsync.

Aplicación de ejemplo: Control de tráfico de Dapr

La versión predeterminada de Dapr Traffic Control no usa el modelo de actor. Sin embargo, contiene una implementación alternativa basada en actores del servicio TrafficControl que puede habilitar. Para usar actores en el servicio TrafficControl, abra el archivo y quite la src/TrafficControlService/Controllers/TrafficController.cs marca de comentario de la USE_ACTORMODEL instrucción en la parte superior del archivo:

#define USE_ACTORMODEL

Cuando el modelo de actor está habilitado, la aplicación usa actores para representar vehículos. Las operaciones que se pueden invocar en los actores del vehículo se definen en una IVehicleActor interfaz:

public interface IVehicleActor : IActor
{
    Task RegisterEntryAsync(VehicleRegistered msg);
    Task RegisterExitAsync(VehicleRegistered msg);
}

Las cámaras de entrada (simuladas) llaman al RegisterEntryAsync método cuando se detecta por primera vez un nuevo vehículo en el carril. La única responsabilidad de este método es almacenar la marca de tiempo de entrada en el estado del actor:

var vehicleState = new VehicleState
{
    LicenseNumber = msg.LicenseNumber,
    EntryTimestamp = msg.Timestamp
};
await StateManager.SetStateAsync("VehicleState", vehicleState);

Cuando el vehículo llega al final de la zona de la cámara de velocidad, la cámara de salida llama al RegisterExitAsync método . El RegisterExitAsync método obtiene primero los estados actuales y lo actualiza para incluir la marca de tiempo de salida:

var vehicleState = await StateManager.GetStateAsync<VehicleState>("VehicleState");
vehicleState.ExitTimestamp = msg.Timestamp;

Nota:

El código anterior supone actualmente que el RegisterEntryAsync método ya guardó una VehicleState instancia de . El código se puede mejorar comprobando primero para asegurarse de que el estado existe. Gracias al modelo de acceso basado en turnos, no se requieren bloqueos explícitos en el código.

Una vez actualizado el estado, el RegisterExitAsync método comprueba si el vehículo estaba conduciendo demasiado rápido. Si es así, el actor publica un mensaje en el collectfine tema pub/sub:

int violation = _speedingViolationCalculator.DetermineSpeedingViolationInKmh(
    vehicleState.EntryTimestamp, vehicleState.ExitTimestamp);

if (violation > 0)
{
    var speedingViolation = new SpeedingViolation
    {
        VehicleId = msg.LicenseNumber,
        RoadId = _roadId,
        ViolationInKmh = violation,
        Timestamp = msg.Timestamp
    };

    await _daprClient.PublishEventAsync("pubsub", "collectfine", speedingViolation);
}

El código anterior usa dos dependencias externas. Encapsula _speedingViolationCalculator la lógica de negocios para determinar si un vehículo se ha impulsado demasiado rápido. _daprClient Permite al actor publicar mensajes mediante el bloque de creación de publicación/sub de Dapr.

Ambas dependencias se registran en la Startup clase y se insertan en el actor mediante la inserción de dependencias del constructor:

private readonly DaprClient _daprClient;
private readonly ISpeedingViolationCalculator _speedingViolationCalculator;
private readonly string _roadId;

public VehicleActor(
    ActorHost host, DaprClient daprClient,
    ISpeedingViolationCalculator speedingViolationCalculator)
    : base(host)
{
    _daprClient = daprClient;
    _speedingViolationCalculator = speedingViolationCalculator;
    _roadId = _speedingViolationCalculator.GetRoadId();
}

La implementación basada en actores ya no usa directamente el bloque de creación de administración de estado de Dapr. En su lugar, el estado se conserva automáticamente después de ejecutar cada operación.

Resumen

El bloque de creación de actores de Dapr facilita la escritura de sistemas simultáneos correctos. Los actores son pequeñas unidades de estado y lógica. Usan un modelo de acceso basado en turnos que le ahorra tener que usar mecanismos de bloqueo para escribir código seguro para subprocesos. Los actores se crean implícitamente y se descargan silenciosamente de la memoria cuando no se realizan operaciones. Cualquier estado almacenado en el actor se conserva y carga automáticamente cuando se reactiva el actor. Normalmente, las implementaciones del modelo de actor se crean para un lenguaje o plataforma específicos. Sin embargo, con el bloque de creación de actores de Dapr, puede aprovechar el modelo de actor desde cualquier lenguaje o plataforma.

Los actores admiten temporizadores y recordatorios para programar el trabajo futuro. Los temporizadores no restablecen el temporizador de inactividad y permitirán que el actor se desactive cuando no se realice ninguna otra operación. Los avisos restablecen el temporizador de inactividad y también se conservan automáticamente. Tanto los temporizadores como los recordatorios respetan el modelo de acceso basado en turnos, asegurándose de que ninguna otra operación se pueda ejecutar mientras se controlan los eventos de temporizador o recordatorio.

El estado del actor se conserva mediante el bloque de creación de administración de estado de Dapr. Cualquier almacén de estado que admita transacciones de varios elementos se puede usar para almacenar el estado del actor.

Referencias