Padrões de Atores com Akka.Hosting
Quando Usar Este Skill
Use este skill quando:
- Construir atores entidade que representam objetos de domínio (usuários, pedidos, faturas, etc.)
- Precisar de atores que funcionem tanto em testes unitários (sem cluster) quanto em produção (com sharding de cluster)
- Configurar tarefas agendadas com akka-reminders
- Registrar atores com métodos de extensão do Akka.Hosting
- Criar padrões de configuração de atores reutilizáveis
Princípios Principais
- Abstração do Modo de Execução - Mesmo código de ator executado localmente (testes) ou em cluster (produção)
- GenericChildPerEntityParent para Local - Simula semânticas de sharding sem sobrecarga de cluster
- Extratores de Mensagem para Roteamento - Reutiliza a interface IMessageExtractor do Akka.Cluster.Sharding
- Métodos de Extensão do Akka.Hosting - Configuração fluente que compõe bem
- ITimeProvider para Testabilidade - Use ActorSystem.Scheduler em vez de DateTime.Now
Modos de Execução
Defina um enum para controlar o comportamento do ator:
/// <summary>
/// Determina como o Akka.NET deve ser configurado
/// </summary>
public enum AkkaExecutionMode
{
/// <summary>
/// Sistema de atores puramente local - sem remoting, sem clustering.
/// Use GenericChildPerEntityParent em vez de ShardRegion.
/// Ideal para testes unitários e cenários simples.
/// </summary>
LocalTest,
/// <summary>
/// Clustering completo com ShardRegion.
/// Use para testes de integração e produção.
/// </summary>
Clustered
}
GenericChildPerEntityParent
Um ator pai leve que roteia mensagens para entidades filhas, simulando semânticas de sharding de cluster sem exigir um cluster:
using Akka.Actor;
using Akka.Cluster.Sharding;
/// <summary>
/// Um ator pai genérico "um filho por entidade".
/// </summary>
/// <remarks>
/// Reutiliza IMessageExtractor do Akka.Cluster.Sharding para roteamento consistente.
/// Ideal para testes unitários onde a sobrecarga de clustering é desnecessária.
/// </remarks>
public sealed class GenericChildPerEntityParent : ReceiveActor
{
public static Props CreateProps(
IMessageExtractor extractor,
Func<string, Props> propsFactory)
{
return Props.Create(() =>
new GenericChildPerEntityParent(extractor, propsFactory));
}
private readonly IMessageExtractor _extractor;
private readonly Func<string, Props> _propsFactory;
public GenericChildPerEntityParent(
IMessageExtractor extractor,
Func<string, Props> propsFactory)
{
_extractor = extractor;
_propsFactory = propsFactory;
ReceiveAny(message =>
{
var entityId = _extractor.EntityId(message);
if (entityId is null) return;
// Obtém filho existente ou cria um novo
Context.Child(entityId)
.GetOrElse(() => Context.ActorOf(_propsFactory(entityId), entityId))
.Forward(_extractor.EntityMessage(message));
});
}
}
Extratores de Mensagem
Crie extratores que implementam IMessageExtractor do Akka.Cluster.Sharding:
using Akka.Cluster.Sharding;
/// <summary>
/// Roteia mensagens para atores entidade com base em um ID fortemente tipado.
/// </summary>
public sealed class OrderMessageExtractor : HashCodeMessageExtractor
{
public const int DefaultShardCount = 40;
public OrderMessageExtractor(int maxNumberOfShards = DefaultShardCount)
: base(maxNumberOfShards)
{
}
public override string? EntityId(object message)
{
return message switch
{
IWithOrderId msg => msg.OrderId.Value.ToString(),
_ => null
};
}
}
// Define uma interface para mensagens que visam uma entidade específica
public interface IWithOrderId
{
OrderId OrderId { get; }
}
// Use IDs fortemente tipados
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
Métodos de Extensão do Akka.Hosting
Crie métodos de extensão que abstraem o modo de execução:
using Akka.Cluster.Hosting;
using Akka.Cluster.Sharding;
using Akka.Hosting;
public static class OrderActorHostingExtensions
{
/// <summary>
/// Adiciona OrderActor com suporte para modos local e clusterizado.
/// </summary>
public static AkkaConfigurationBuilder WithOrderActor(
this AkkaConfigurationBuilder builder,
AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
string? clusterRole = null)
{
if (executionMode == AkkaExecutionMode.LocalTest)
{
// Modo não clusterizado: Use GenericChildPerEntityParent
builder.WithActors((system, registry, resolver) =>
{
var parent = system.ActorOf(
GenericChildPerEntityParent.CreateProps(
new OrderMessageExtractor(),
entityId => resolver.Props<OrderActor>(entityId)),
"orders");
registry.Register<OrderActor>(parent);
});
}
else
{
// Modo clusterizado: Use ShardRegion
builder.WithShardRegion<OrderActor>(
"orders",
(system, registry, resolver) =>
entityId => resolver.Props<OrderActor>(entityId),
new OrderMessageExtractor(),
new ShardOptions
{
StateStoreMode = StateStoreMode.DData,
Role = clusterRole
});
}
return builder;
}
}
Compondo Múltiplos Atores
Crie um método de conveniência que registra todos os atores do domínio:
public static class DomainActorHostingExtensions
{
/// <summary>
/// Adiciona todos os atores do domínio de pedidos com suporte a sharding.
/// </summary>
public static AkkaConfigurationBuilder WithOrderDomainActors(
this AkkaConfigurationBuilder builder,
AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
string? clusterRole = null)
{
return builder
.WithOrderActor(executionMode, clusterRole)
.WithPaymentActor(executionMode, clusterRole)
.WithShipmentActor(executionMode, clusterRole)
.WithNotificationActor(); // Singleton, sem sharding necessário
}
}
Usando ITimeProvider para Agendamento
Registre o Scheduler do ActorSystem como um ITimeProvider para lógica baseada em tempo testável:
public static class SharedAkkaHostingExtensions
{
public static IServiceCollection AddAkkaWithTimeProvider(
this IServiceCollection services,
Action<AkkaConfigurationBuilder, IServiceProvider> configure)
{
// Registra ITimeProvider usando o Scheduler do ActorSystem
services.AddSingleton<ITimeProvider>(sp =>
sp.GetRequiredService<ActorSystem>().Scheduler);
return services.ConfigureAkka((builder, sp) =>
{
configure(builder, sp);
});
}
}
// No seu ator, injete ITimeProvider
public class SubscriptionActor : ReceiveActor
{
private readonly ITimeProvider _timeProvider;
public SubscriptionActor(ITimeProvider timeProvider)
{
_timeProvider = timeProvider;
// Use _timeProvider.GetUtcNow() em vez de DateTime.UtcNow
// Isso permite que os testes controlem o tempo
}
}
Integração com Akka.Reminders
Para tarefas agendadas duráveis que sobrevivem a reinicializações, use akka-reminders:
using Akka.Reminders;
using Akka.Reminders.Sql;
using Akka.Reminders.Sql.Configuration;
using Akka.Reminders.Storage;
public static class ReminderHostingExtensions
{
/// <summary>
/// Configura akka-reminders com armazenamento PostgreSQL.
/// </summary>
public static AkkaConfigurationBuilder WithPostgresReminders(
this AkkaConfigurationBuilder builder,
string connectionString,
string schemaName = "reminders",
string tableName = "scheduled_reminders",
bool autoInitialize = true)
{
return builder.WithLocalReminders(reminders => reminders
.WithResolver(sys => new GenericChildPerEntityResolver(sys))
.WithStorage(system =>
{
var settings = SqlReminderStorageSettings.CreatePostgreSql(
connectionString,
schemaName,
tableName,
autoInitialize);
return new SqlReminderStorage(settings, system);
})
.WithSettings(new ReminderSettings
{
MaxSlippage = TimeSpan.FromSeconds(30),
MaxDeliveryAttempts = 3,
RetryBackoffBase = TimeSpan.FromSeconds(10)
}));
}
/// <summary>
/// Configura akka-reminders com armazenamento em memória para testes.
/// </summary>
public static AkkaConfigurationBuilder WithInMemoryReminders(
this AkkaConfigurationBuilder builder)
{
return builder.WithLocalReminders(reminders => reminders
.WithResolver(sys => new GenericChildPerEntityResolver(sys))
.WithStorage(system => new InMemoryReminderStorage())
.WithSettings(new ReminderSettings
{
MaxSlippage = TimeSpan.FromSeconds(1),
MaxDeliveryAttempts = 3,
RetryBackoffBase = TimeSpan.FromMilliseconds(100)
}));
}
}
Resolvedor de Lembretes Personalizado para Child-Per-Entity
Roteie callbacks de lembretes para atores GenericChildPerEntityParent:
using Akka.Actor;
using Akka.Hosting;
using Akka.Reminders;
/// <summary>
/// Resolve destinos de lembretes para atores GenericChildPerEntityParent.
/// </summary>
public sealed class GenericChildPerEntityResolver : IReminderActorResolver
{
private readonly ActorSystem _system;
public GenericChildPerEntityResolver(ActorSystem system)
{
_system = system;
}
public IActorRef ResolveActorRef(ReminderEntry entry)
{
var registry = ActorRegistry.For(_system);
return entry.Key switch
{
var k when k.StartsWith("order-") =>
registry.Get<OrderActor>(),
var k when k.StartsWith("subscription-") =>
registry.Get<SubscriptionActor>(),
_ => throw new InvalidOperationException(
$"Formato de chave de lembrete desconhecido: {entry.Key}")
};
}
}
Atores Singleton (Não Shardeados)
Para atores que devem ter apenas uma instância:
public static AkkaConfigurationBuilder WithEmailSenderActor(
this AkkaConfigurationBuilder builder)
{
return builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(
resolver.Props<EmailSenderActor>(),
"email-sender");
registry.Register<EmailSenderActor>(actor);
});
}
Tipos Marcadores para Registro
Quando você precisa referenciar atores que são registrados como pais:
/// <summary>
/// Tipo marcador para ActorRegistry recuperar o gerenciador de pedidos
/// (GenericChildPerEntityParent para OrderActors).
/// </summary>
public sealed class OrderManagerActor;
// Uso no método de extensão
registry.Register<OrderManagerActor>(parent);
// Uso no controller/serviço
public class OrderService
{
private readonly IActorRef _orderManager;
public OrderService(IRequiredActor<OrderManagerActor> orderManager)
{
_orderManager = orderManager.ActorRef;
}
public async Task<OrderResponse> CreateOrder(CreateOrderCommand cmd)
{
return await _orderManager.Ask<OrderResponse>(cmd);
}
}
Gerenciamento de Escopo de DI em Atores
Atores não têm escopos automáticos de DI. Diferente de controladores ASP.NET (onde cada requisição HTTP cria um escopo), atores são de longa duração. Se você precisar de serviços com escopo (como DbContext), injete IServiceProvider e crie escopos manualmente.
Padrão: Escopo Por Mensagem
public sealed class OrderProcessingActor : ReceiveActor
{
private readonly IServiceProvider _serviceProvider;
private readonly IActorRef _notificationActor;
public OrderProcessingActor(
IServiceProvider serviceProvider,
IRequiredActor<NotificationActor> notificationActor)
{
_serviceProvider = serviceProvider;
_notificationActor = notificationActor.ActorRef;
ReceiveAsync<ProcessOrder>(HandleProcessOrder);
}
private async Task HandleProcessOrder(ProcessOrder msg)
{
// Cria escopo para esta mensagem - descartado após o processamento
using var scope = _serviceProvider.CreateScope();
// Resolve serviços com escopo dentro do escopo
var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var paymentService = scope.ServiceProvider.GetRequiredService<IPaymentService>();
var emailComposer = scope.ServiceProvider.GetRequiredService<IOrderEmailComposer>();
// Trabalha com serviços de escopo
var order = await orderRepository.GetByIdAsync(msg.OrderId);
var payment = await paymentService.ProcessAsync(order);
// Alterações no DbContext são confirmadas quando o escopo é descartado
}
}
Por Que Este Padrão
| Benefício | Explicação |
|---|---|
| DbContext fresco por mensagem | Sem rastreamento de entidades obsoletas entre mensagens |
| Descarte adequado | Conexões de banco de dados liberadas após cada mensagem |
| Isolamento | Erros de uma mensagem não corrompem o estado de outra |
| Testável | Pode injetar mock IServiceProvider em testes |
Serviços Singleton - Injeção Direta
Para serviços stateless e thread-safe, injete diretamente (sem escopo necessário):
public sealed class NotificationActor : ReceiveActor
{
private readonly IEmailLinkGenerator _linkGenerator; // Singleton - OK!
private readonly IMjmlTemplateRenderer _renderer; // Singleton - OK!
public NotificationActor(
IEmailLinkGenerator linkGenerator,
IMjmlTemplateRenderer renderer)
{
_linkGenerator = linkGenerator;
_renderer = renderer;
Receive<SendNotification>(msg =>
{
// Use serviços singleton - sem necessidade de escopo
var link = _linkGenerator.Generate(msg.TrackingId);
var html = _renderer.Render(msg.Template);
// ...
});
}
}
Resumo
| Conceito | Abordagem | Quando Usar |
|---|---|---|
| Modo Local | GenericChildPerEntityParent + IMessageExtractor | Testes unitários, desenvolvimento local |
| Modo Cluster | ShardRegion + IMessageExtractor | Produção, testes de integração |
| Tarefas Agendadas | Akka.Reminders + IReminderActorResolver | Tarefas duráveis que sobrevivem a reinicializações |
| Tempo Testável | ITimeProvider (ActorSystem.Scheduler) | Lógica baseada em tempo |
| Escopo de DI | IServiceProvider.CreateScope() por mensagem | Serviços com escopo em atores de longa duração |
| Atores Singleton | WithActors + ActorRegistry | Serviços de infraestrutura sem estado |