Melhores Práticas para Akka.NET
Quando Usar Esta Skill
Use esta skill quando:
- Projetando padrões de comunicação entre atores
- Decidindo entre EventStream e DistributedPubSub
- Implementando tratamento de erros em atores
- Entendendo estratégias de supervisão
- Escolhendo entre padrões Props e DependencyResolver
- Projetando distribuição de trabalho entre nós
- Criando sistemas de atores testáveis que podem executar com ou sem infraestrutura de cluster
- Abstraindo o Cluster Sharding para cenários de teste local
Arquivos de Referência
- work-distribution-patterns.md: Filas de banco de dados, limitação de taxa com Akka.Streams, padrão outbox
- cluster-local-abstractions.md: GenericChildPerEntityParent, IPubSubMediator, configuração do modo de execução
- async-cancellation-patterns.md: CancellationToken com escopo de ator, CTS vinculado, tratamento de timeout
1. EventStream vs DistributedPubSub
Crítico: EventStream é APENAS LOCAL
Context.System.EventStream é local a um único processo ActorSystem. Ele NÃO funciona entre nós do cluster.
// RUIM: Isso funciona apenas em um único servidor
// Quando você adiciona um segundo servidor, os assinantes no servidor 2 não receberão eventos do servidor 1
Context.System.EventStream.Subscribe(Self, typeof(PostCreated));
Context.System.EventStream.Publish(new PostCreated(postId, authorId));
Quando EventStream é apropriado:
- Logging e diagnóstico dentro de um único processo
- Barramento de eventos local para aplicativos verdadeiramente de processo único
- Cenários de desenvolvimento/teste
Use DistributedPubSub para Múltiplos Nós
Para eventos que precisam alcançar atores em vários nós do cluster, use Akka.Cluster.Tools.PublishSubscribe:
using Akka.Cluster.Tools.PublishSubscribe;
public class TimelineUpdatePublisher : ReceiveActor
{
private readonly IActorRef _mediator;
public TimelineUpdatePublisher()
{
// Obter o mediador do DistributedPubSub
_mediator = DistributedPubSub.Get(Context.System).Mediator;
Receive<PublishTimelineUpdate>(msg =>
{
// Publicar em um tópico - alcança todos os assinantes em todos os nós
_mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update));
});
}
}
Configuração do Akka.Hosting para DistributedPubSub
builder.WithDistributedPubSub(role: null); // Disponível em todas as funções, ou especifique uma função
Padrões de Design de Tópicos
| Padrão | Formato do Tópico | Caso de Uso |
|---|---|---|
| Por usuário | timeline:{userId} | Atualizações de timeline, notificações |
| Por entidade | post:{postId} | Atualizações de engajamento em postagens |
| Broadcast | system:announcements | Notificações em todo o sistema |
| Baseado em função | workers:rss-poller | Distribuição de trabalho |
2. Estratégias de Supervisão
Esclarecimento Importante: Supervisão é para FILHOS
Uma estratégia de supervisão definida em um ator determina como esse ator supervisiona seus filhos, NÃO como o próprio ator é supervisionado.
public class ParentActor : ReceiveActor
{
// Esta estratégia se aplica aos filhos de ParentActor, NÃO ao ParentActor em si
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
maxNrOfRetries: 10,
withinTimeRange: TimeSpan.FromSeconds(30),
decider: ex => ex switch
{
ArithmeticException => Directive.Resume,
NullReferenceException => Directive.Restart,
ArgumentException => Directive.Stop,
_ => Directive.Escalate
});
}
}
Estratégia de Supervisão Padrão
O OneForOneStrategy padrão já inclui limitação de taxa:
- 10 reinicializações em 1 segundo = o ator é permanentemente parado
- Isso evita loops infinitos de reinicialização
Raramente você precisa de uma estratégia personalizada, a menos que tenha requisitos específicos.
Quando Definir Supervisão Personalizada
Bons motivos:
- O ator lança exceções que indicam corrupção de estado irrecuperável -> Reiniciar
- O ator lança exceções que NÃO devem causar reinicialização (falhas esperadas) -> Continuar
- Falhas de filhos devem afetar irmãos -> Use
AllForOneStrategy - Necessidade de limites de retry diferentes do padrão
Maus motivos:
- "Só por precaução" - o padrão já é seguro
- Não entender o que o ator faz - entenda primeiro
3. Tratamento de Erros: Supervisão vs Try-Catch
Quando Usar Try-Catch (Na Maioria dos Casos)
Use try-catch quando:
- A falha é esperada (timeout de rede, entrada inválida, serviço externo indisponível)
- Você sabe exatamente por que a exceção ocorreu
- Você pode lidar com ela graciosamente (repetir, retornar resposta de erro, registrar e continuar)
- Reiniciar não ajudaria (o mesmo erro ocorreria novamente)
public class RssFeedPollerActor : ReceiveActor
{
public RssFeedPollerActor()
{
ReceiveAsync<PollFeed>(async msg =>
{
try
{
var feed = await _httpClient.GetStringAsync(msg.FeedUrl);
var items = ParseFeed(feed);
// Processar itens...
}
catch (HttpRequestException ex)
{
// Falha esperada - registrar e agendar nova tentativa
_log.Warning("Feed {Url} indisponível: {Error}", msg.FeedUrl, ex.Message);
Context.System.Scheduler.ScheduleTellOnce(
TimeSpan.FromMinutes(5), Self, msg, Self);
}
catch (XmlException ex)
{
// Formato de feed inválido - registrar e marcar como ruim
_log.Error("Feed {Url} tem formato inválido: {Error}", msg.FeedUrl, ex.Message);
Sender.Tell(new FeedPollResult.InvalidFormat(msg.FeedUrl));
}
});
}
}
Quando Deixar a Supervisão Cuidar
Deixe as exceções propagarem (acionar supervisão) quando:
- Você não tem ideia por que a exceção ocorreu
- O estado do ator pode estar corrompido
- Uma reinicialização ajudaria (estado fresco, reconectar recursos)
- É um erro de programação (NullReferenceException, InvalidOperationException de lógica ruim)
Anti-Padrão: Engolir Exceções Desconhecidas
// RUIM: Engolir exceções esconde problemas
catch (Exception ex)
{
_log.Error(ex, "Erro ao processar trabalho");
// Ator continua com estado potencialmente corrompido
}
// BOM: Tratar exceções conhecidas, deixar as desconhecidas propagarem
catch (HttpRequestException ex)
{
// Falha conhecida e esperada - tratar graciosamente
_log.Warning("Falha na requisição HTTP: {Error}", ex.Message);
Sender.Tell(new WorkResult.TransientFailure());
}
// Exceções desconhecidas propagam para a supervisão
4. Props vs DependencyResolver
Quando Usar Props Simples
Use Props.Create() quando:
- O ator não precisa de
IServiceProviderouIRequiredActor<T> - Todas as dependências podem ser passadas via construtor
- O ator é simples e autossuficiente
// Ator simples sem necessidade de DI
public static Props Props(PostId postId, IPostWriteStore store)
=> Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));
Quando Usar DependencyResolver
Use resolver.Props<T>() quando:
- O ator precisa de
IServiceProviderpara criar serviços com escopo - O ator usa
IRequiredActor<T>para obter referências a outros atores - O ator tem muitas dependências que já estão no contêiner DI
// Registro com DI
builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(resolver.Props<OrderProcessorActor>(), "order-processor");
registry.Register<OrderProcessorActor>(actor);
});
Considerações sobre Implantação Remota
Você quase nunca precisa de implantação remota. Se você não está fazendo implantação remota (e provavelmente não está):
Props.Create(() => new Actor(...))com closures é suficiente- O "aviso de problema de serialização" não se aplica
Para a maioria dos aplicativos, use cluster sharding em vez de implantação remota - ele lida com a distribuição automaticamente.
5. Padrões de Distribuição de Trabalho
Quando você tem muitos trabalhos em segundo plano (feeds RSS, envio de e-mail, etc.), não os processe todos de uma vez - isso causa problemas de 'thundering herd'.
Três padrões para resolver isso:
- Fila de Trabalho Baseada em Banco de Dados - Use
FOR UPDATE SKIP LOCKEDpara distribuição natural entre nós - Limitação de Taxa com Akka.Streams - Limitar o processamento dentro de um único nó
- Fila Durável (Padrão Outbox) - Outbox baseado em banco de dados para processamento confiável
Veja work-distribution-patterns.md para exemplos de código completos.
6. Resumo de Erros Comuns
| Erro | Por que é Errado | Correção |
|---|---|---|
| Usar EventStream para pub/sub entre nós | EventStream é apenas local | Use DistributedPubSub |
| Definir supervisão para "proteger" um ator | Supervisão protege filhos | Entenda a hierarquia |
| Capturar todas as exceções | Esconde bugs, corrompe estado | Capture apenas erros esperados |
| Usar DependencyResolver sempre | Adiciona complexidade desnecessária | Use Props simples quando possível |
| Processar todos os trabalhos em segundo plano de uma vez | Thundering herd, exaustão de recursos | Use fila de banco de dados + limitação de taxa |
| Lançar exceções para falhas esperadas | Aciona reinicializações desnecessárias | Retorne tipos de resultado, use mensagens |
7. Referência Rápida
Árvore de Decisão para Padrões de Comunicação
Precisa se comunicar entre atores?
├── Apenas no mesmo processo? -> EventStream é suficiente
├── Entre nós do cluster?
│ ├── Ponto a ponto? -> Use ActorSelection ou IActorRef conhecido
│ └── Pub/sub? -> Use DistributedPubSub
└── Fire-and-forget para sistema externo? -> Considere padrão outbox
Árvore de Decisão para Tratamento de Erros
Exceção ocorreu no ator?
├── Falha esperada (timeout HTTP, entrada inválida)?
│ └── Try-catch, tratar graciosamente, continuar
├── Estado pode estar corrompido?
│ └── Deixe a supervisão reiniciar
├── Causa desconhecida?
│ └── Deixe a supervisão reiniciar
└── Erro de programação (ref nula, lógica ruim)?
└── Deixe a supervisão reiniciar, corrija o bug
Árvore de Decisão para Props
Criando Props de ator?
├── Ator precisa de IServiceProvider?
│ └── Use resolver.Props<T>()
├── Ator precisa de IRequiredActor<T>?
│ └── Use resolver.Props<T>()
├── Ator simples com parâmetros de construtor?
│ └── Use Props.Create(() => new Actor(...))
└── Implantação remota necessária?
└── Provavelmente não - use cluster sharding
8. Abstrações de Modo Cluster/Local
Para aplicativos que precisam executar tanto em produção em cluster quanto em ambientes locais/teste, use padrões de abstração para alternar entre implementações:
- Enum
AkkaExecutionMode- Controla quais implementações são usadas (LocalTest vs Clustered) GenericChildPerEntityParent- Imita o comportamento de sharding localmente usando o mesmoIMessageExtractorIPubSubMediator- Abstrai DistributedPubSub para implementações locais/de cluster intercambiáveis
Veja cluster-local-abstractions.md para o código de implementação completo.
9. Logging em Atores
Use ILoggingAdapter, Não ILogger
Em atores, use ILoggingAdapter de Context.GetLogger() em vez de ILogger<T> injetado por DI:
public class MyActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public MyActor()
{
Receive<MyMessage>(msg =>
{
_log.Info("Processando mensagem para usuário {UserId}", msg.UserId);
_log.Error(ex, "Falha ao processar {MessageType}", msg.GetType().Name);
});
}
}
Por que ILoggingAdapter:
- Integra-se ao pipeline de logging e supervisão do Akka
- Suporta logging semântico/estruturado a partir da v1.5.57
- Nomes de métodos:
Info(),Debug(),Warning(),Error()(não variantesLog*) - Não requer DI - obtido diretamente do contexto do ator
Não injete ILogger<T> em atores - isso ignora a infraestrutura de logging do Akka.
Logging Semântico (v1.5.57+)
// Placeholders nomeados para melhor agregação e consulta de logs
_log.Info("Pedido {OrderId} processado para cliente {CustomerId}", order.Id, order.CustomerId);
// Prefira placeholders nomeados em vez de posicionais
// Bom: {OrderId}, {CustomerId}
// Evite: {0}, {1}
10. Gerenciando Operações Assíncronas com CancellationToken
Quando atores iniciam operações assíncronas via PipeTo, essas operações podem sobreviver ao ator se não forem gerenciadas adequadamente. Práticas principais:
- CTS do ator em PostStop - Sempre cancele e descarte em
PostStop() - Novo CTS por operação - Cancele o anterior antes de iniciar um novo trabalho
- Passe token para todos os lugares - Consultas EF Core, chamadas HTTP, etc.
- CTS vinculado para timeouts - Chamadas externas recebem timeouts curtos para evitar travamentos
- Tratamento gracioso - Diferencie timeout de desligamento nos blocos catch
Veja async-cancellation-patterns.md para o código de implementação completo.