Bloque de creación de actores de Dapr
Sugerencia
Este contenido es un extracto del libro electrónico "Dapr for .NET Developers" (Dapr para desarrolladores de .NET), disponible en Documentación de .NET o como un PDF descargable gratuito que se puede leer sin conexión.
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 el proyecto "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. Cuando no se ejecutan 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á 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 bastante bien es 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 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 de tipo 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 el tipo sidecar:
Figura 11-1. Llamadas API entre el servicio de actor y el sidecar de Dapr.
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, el 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:
Figura 11-2. Servicio de selección de ubicación de actor.
- En el inicio, el sidecar realiza una llamada al servicio de actor para obtener los tipos de actor registrados, así como los valores de configuración del actor.
- El sidecar envía la lista de tipos de actor registrados al servicio de selección de ubicación.
- 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 método ship
de una instancia OrderActor
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:
Figura 11-3. Llamada a un método de actor.
- El servicio llama a la API de actor en el sidecar. La carga JSON del cuerpo de la solicitud contiene los datos que se van a enviar al actor.
- 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. - La instancia del 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 método GetValue
es 1
. Cuando dos subprocesos llaman al método Increment
al mismo tiempo, existe el riesgo de que ambos llamen al método GetValue
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, se 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. En el ejemplo de actor siguiente se refleja estrechamente el código del ejemplo anterior, pero no es necesario 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 y cuando se active el actor. Los temporizadores no restablecerán el temporizador de inactividad, por lo que no pueden mantener un actor activo por sí solo.
- Los recordatorios sobreviven a las activaciones de actor. Si se desactiva un actor, se volverá a activar el actor. Los recordatorios restablecerán el temporizador de inactividad.
Los temporizadores se registran mediante la realización de 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 especificado está vacío, será un recordatorio único.
Nota
Los temporizadores y recordatorios respetan el modelo de acceso basado en turnos. Cuando se activa un temporizador o 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 de 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
- RethinkDB
- SQL Server
Para configurar un componente de almacén de estado para su uso 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 para Dapr
Puede crear una implementación de modelo de actor con solo llamadas HTTP/gRPC. Sin embargo, es mucho más cómodo usar los SDK para 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 de .NET para Dapr, 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 clase ScoreActor
de Actor
. La clase ScoreActor
también debe implementar la interfaz IScoreActor
:
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 argumento host
de tipo ActorHost
. La clase ActorHost
representa el host de un tipo de actor dentro del tiempo de ejecución del actor. Debe pasar este argumento al constructor de la clase base Actor
. 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 método IncrementScoreAsync
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 para el método IncrementScoreAsync
. El método AddOrUpdateStateAsync
toma tres argumentos:
- Clave del estado que se va a actualizar.
- Valor que se va a escribir si aún no hay ninguna puntuación almacenada en el almacén de estado.
- Un
Func
para 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 implementación de GetScoreAsync
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 paquete Dapr.Actors.AspNetCore
y realizar algunos cambios en el archivo Program
. En el ejemplo siguiente, la llamada a MapActorsHandlers
registra los puntos de conexión del actor de Dapr en el enrutamiento ASP.NET Core:
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 el sidecar de Dapr llama a la aplicación para hospedar e interactuar con instancias de actor.
Importante
Asegúrese de que la clase Program
(o Startup
) no contenga una llamada a app.UseHttpsRedirection
para redirigir a los clientes al punto de conexión HTTPS. Esto no funcionará con actores. Por diseño, un sidecar de Dapr envía solicitudes a través de HTTP sin cifrar de forma predeterminada. El middleware HTTPS bloqueará estas solicitudes cuando se habilite.
El archivo Program
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 extensión AddActors
:
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 ScoreActor
y las solicitudes entrantes y aceptarlas. 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 operación IncrementScoreAsync
en una instancia de ScoreActor
:
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 paquete Dapr.Actors
para llamar al servicio de actor. Para invocar una operación en un actor, debe poder abordarla. Necesitará dos partes para esto:
- 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 elemento
ActorAttribute
a la clase de implementación y estableciendo su propiedadTypeName
. ActorId
identifica de forma única una instancia de un tipo de actor. También puede usar esta clase para generar un identificador de actor aleatorio llamando aActorId.CreateRandom
.
En el ejemplo se usa ActorProxy.Create
para crear una instancia de proxy para ScoreActor
. El método Create
toma dos argumentos: ActorId
que identifica el 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 tanto las aplicaciones cliente como servidor necesitan usar las interfaces de actor, normalmente se almacenan en un proyecto compartido independiente.
El último paso del ejemplo llama al método IncrementScoreAsync
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 sidecar de Dapr. Por lo tanto, espere que una llamada de actor sea una llamada de red a otro nodo.
Llamada a actores de clientes de ASP.NET Core
El ejemplo de cliente de consola de la sección anterior 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 interfaz IActorProxyFactory
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 en IServiceCollection
toma 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 a AddActors
registra IActorProxyFactory
para la inserción de dependencias de .NET. Esto permite que ASP.NET Core inserte una instancia de IActorProxyFactory
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 clase base Actor
expone una clase IActorProxyFactory
a través de la propiedad ProxyFactory
. Para crear un proxy de actor desde un actor, use la propiedad ProxyFactory
de la clase base Actor
. En el ejemplo siguiente se muestra un objeto OrderActor
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 de llamadas Actor A -> Actor B -> Actor A
. En el momento de escribir esto, 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 que no son .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 método RegisterTimerAsync
de la clase base Actor
para programar temporizadores de actor. En el ejemplo siguiente, un objeto TimerActor
expone un método StartTimerAsync
. 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 método StartTimerAsync
llama a RegisterTimerAsync
para programar el temporizador. RegisterTimerAsync
toma cinco argumentos:
- Nombre del temporizador.
- Nombre del método al que se llamará cuando se active el temporizador.
- Estado que se pasa al método de devolución de llamada.
- Cantidad de tiempo que se debe esperar antes de invocar el método de devolución de llamada por primera vez.
- 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 método TimerCallbackAsync
recibe el estado de usuario en formato binario. En el ejemplo, la devolución de llamada descodifica el estado en una 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 interfaz IRemindable
:
public interface IRemindable
{
Task ReceiveReminderAsync(
string reminderName, byte[] state,
TimeSpan dueTime, TimeSpan period);
}
Se llama al método ReceiveReminderAsync
cuando se desencadena un aviso. Toma 4 argumentos:
- Nombre del recordatorio.
- Estado de usuario proporcionado durante el registro.
- Tiempo de vencimiento de invocación proporcionado durante el registro.
- Período de invocación proporcionado durante el registro.
Para registrar un recordatorio, use el método RegisterReminderAsync
de la clase base de actor. En el ejemplo siguiente se establece un recordatorio 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 método RegisterReminderAsync
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 recordatorio. 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 la aplicación Control de tráfico de Dapr 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 src/TrafficControlService/Controllers/TrafficController.cs
y quite la marca de comentario de la instrucción USE_ACTORMODEL
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 interfaz IVehicleActor
:
public interface IVehicleActor : IActor
{
Task RegisterEntryAsync(VehicleRegistered msg);
Task RegisterExitAsync(VehicleRegistered msg);
}
Las cámaras de entrada (simuladas) llaman al método RegisterEntryAsync
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 método RegisterExitAsync
. El método RegisterExitAsync
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 método RegisterEntryAsync
ya guardo instancia de VehicleState
. 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 método RegisterExitAsync
comprueba si el vehículo estaba conduciendo demasiado rápido. Si es así, el actor publica un mensaje en el tema de publicación y suscripción collectfine
:
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. _speedingViolationCalculator
encapsula 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 y suscripción de Dapr.
Ambas dependencias se registran en la clase Program.cs 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.