O Actor Model na JVM: Parte 3 - O Capítulo Final

19 de julho de 202511 min de leitura19 views

Guia completo de implementação do Actor Model com padrões avançados, estratégias de teste e lições do mundo real aprendidas ao construir sistemas concorrentes escaláveis.

O Actor Model na JVM: Parte 3 - O Capítulo Final
React to this article

Bem-vindo ao capítulo final de nossa jornada pelo Actor Model. Exploramos os desafios históricos da OOP em ambientes concorrentes e dissecamos as armadilhas específicas do estado mutável compartilhado. Agora é hora de arregaçar as mangas e construir um sistema completo baseado em Actors, explorando padrões avançados, estratégias de teste e as lições do mundo real que só vêm da experiência em produção.

Concorrência na JVM: Resumo do Desafio

Como discutimos em nossos artigos anteriores, concorrência na JVM é notoriamente complicada. Threads, locks e estado compartilhado podem rapidamente transformar até sistemas simples em uma bagunça de bugs e gargalos. O Actor Model oferece uma abordagem fundamentalmente diferente que elimina esses problemas no nível arquitetural.

Princípios Fundamentais do Actor Model

Antes de mergulhar na implementação, vamos solidificar nosso entendimento do que torna o Actor Model especial. Actors trazem vários princípios-chave para programação concorrente:

1. Encapsulamento

Cada actor possui completamente seu estado interno. Nenhum código externo pode acessar ou modificar diretamente dados do actor – toda interação acontece através de mensagens.

2. Passagem de Mensagens

Actors se comunicam exclusivamente através de passagem assíncrona de mensagens. Isso elimina a necessidade de locks e previne race conditions.

3. Isolamento

Actors são isolados uns dos outros. Uma falha em um actor não derruba diretamente outros, fornecendo tolerância a falhas natural.

4. Supervisão

Actors são organizados em hierarquias onde actors pais supervisionam seus filhos, fornecendo tratamento estruturado de erros e recuperação.

Hierarquia de supervisão de actors mostrando relacionamentos pai-filho. Quando um child actor falha, seu supervisor decide como lidar com a falha (restart, resume, stop ou escalate). Cada actor tem sua própria mailbox para processamento de mensagens. A ilustração da capa acima destaca esses relacionamentos no contexto para enfatizar tolerância a falhas estruturada em todo o sistema.

5. Transparência de Localização

Actors podem se comunicar uns com os outros independentemente de estarem na mesma JVM, processos diferentes ou até máquinas diferentes.

De Akka para Apache Pekko

Para nossa implementação, usaremos Apache Pekko, o sucessor orientado pela comunidade do Akka. Após a Lightbend mudar o modelo de licenciamento do Akka em 2022, a Apache Software Foundation criou Pekko como uma alternativa totalmente open-source.

Por Que Apache Pekko? É verdadeiramente open source sob a licença Apache 2.0, apoiado por desenvolvimento de comunidade ativa. Mantém compatibilidade de API com Akka 2.6.x, recebe atualizações regulares e patches de segurança, e oferece licenciamento amigável para empresas.

Configurando Seu Sistema de Actors

Vamos começar com as dependências básicas para um projeto Scala:

// build.sbt
libraryDependencies ++= Seq(
  "org.apache.pekko" %% "pekko-actor-typed" % "1.0.2",
  "org.apache.pekko" %% "pekko-stream" % "1.0.2",
  "org.apache.pekko" %% "pekko-http" % "1.0.1",
  "org.apache.pekko" %% "pekko-testkit" % "1.0.2" % Test
)

Construindo um Exemplo do Mundo Real: Sistema de Chat WebSocket

Vamos construir algo prático – um sistema de chat baseado em WebSocket que demonstra conceitos-chave do Actor Model.

1. Padrão Actor Simples

Primeiro, vamos criar um user actor básico que gerencia sessões de chat individuais:

import org.apache.pekko.actor.typed.{ActorRef, Behavior}
import org.apache.pekko.actor.typed.scaladsl.Behaviors
 
object UserActor {
  sealed trait Command
  case class SendMessage(content: String, replyTo: ActorRef[MessageSent]) extends Command
  case class MessageReceived(from: String, content: String) extends Command
 
  sealed trait Event
  case class MessageSent(success: Boolean) extends Event
 
  def apply(username: String): Behavior[Command] = {
    Behaviors.receive { (context, message) =>
      message match {
        case SendMessage(content, replyTo) =>
          context.log.info(s"$username sending: $content")
          // Process message (validation, persistence, etc.)
          replyTo ! MessageSent(true)
          Behaviors.same
 
        case MessageReceived(from, content) =>
          context.log.info(s"$username received from $from: $content")
          // Handle incoming message (display, notifications, etc.)
          Behaviors.same
      }
    }
  }
}

Este actor simples demonstra o princípio central: mensagens entram, mudanças de estado, mensagens saem. Sem locks, sem race conditions.

Ciclo de vida de mensagem no Actor Model: mensagens são enfileiradas na mailbox do actor e processadas sequencialmente uma de cada vez. Este design elimina race conditions sem exigir locks ou sincronização.

2. Padrão Actor com Estado

Agora vamos criar um chat room actor que mantém estado sobre usuários conectados:

object ChatRoomActor {
  sealed trait Command
  case class Join(username: String, userActor: ActorRef[UserActor.Command], replyTo: ActorRef[JoinResult]) extends Command
  case class Leave(username: String) extends Command
  case class BroadcastMessage(from: String, content: String) extends Command
 
  sealed trait Event
  case class JoinResult(success: Boolean, message: String) extends Event
 
  def apply(): Behavior[Command] = chatRoom(Map.empty)
 
  private def chatRoom(users: Map[String, ActorRef[UserActor.Command]]): Behavior[Command] = {
    Behaviors.receive { (context, message) =>
      message match {
        case Join(username, userActor, replyTo) =>
          if (users.contains(username)) {
            replyTo ! JoinResult(false, s"Username $username already taken")
            Behaviors.same
          } else {
            context.log.info(s"$username joined the chat")
            replyTo ! JoinResult(true, s"Welcome $username!")
 
            // Notify existing users
            users.values.foreach(_ ! UserActor.MessageReceived("System", s"$username joined"))
 
            chatRoom(users + (username -> userActor))
          }
 
        case Leave(username) =>
          context.log.info(s"$username left the chat")
          users.values.foreach(_ ! UserActor.MessageReceived("System", s"$username left"))
          chatRoom(users - username)
 
        case BroadcastMessage(from, content) =>
          context.log.info(s"Broadcasting message from $from: $content")
          users.foreach { case (username, userActor) =>
            if (username != from) {
              userActor ! UserActor.MessageReceived(from, content)
            }
          }
          Behaviors.same
      }
    }
  }
}

O ChatRoom actor demonstra como actors naturalmente gerenciam estado através do tratamento de mensagens, retornando novos behaviors com estado atualizado.

Transições de estado do ChatRoom actor mostrando como o estado evolui através do tratamento de mensagens. O actor mantém um mapa de usuários e o atualiza atomicamente com cada mensagem, prevenindo problemas de concorrência através de processamento sequencial.

3. Estratégias de Supervisão

Uma das características mais poderosas do Actor Model é sua hierarquia de supervisão. Vamos implementar um supervisor que gerencia nosso sistema de chat:

import org.apache.pekko.actor.typed.{SupervisorStrategy, DeathPactException}
import scala.concurrent.duration._
 
object ChatSystemSupervisor {
  sealed trait Command
  case class StartChatRoom(name: String, replyTo: ActorRef[ChatRoomStarted]) extends Command
 
  case class ChatRoomStarted(roomActor: ActorRef[ChatRoomActor.Command])
 
  def apply(): Behavior[Command] = {
    Behaviors.receive { (context, message) =>
      message match {
        case StartChatRoom(name, replyTo) =>
          val chatRoom = context.spawn(
            Behaviors.supervise(ChatRoomActor())
              .onFailure[Exception](
                SupervisorStrategy.restart.withLimit(3, 1.minute)
              ),
            s"chatroom-$name"
          )
 
          replyTo ! ChatRoomStarted(chatRoom)
          Behaviors.same
      }
    }
  }
}

Este supervisor irá reiniciar chat rooms falhados até 3 vezes dentro de 1 minuto, escalar para o pai se o limite de reinício for excedido, e preservar a estrutura da hierarquia de actors.

Monitoramento e Observabilidade do Sistema

Sistemas de Actors em produção precisam de monitoramento abrangente. Aqui está como adicionar observabilidade:

Actor de Métricas Customizado

import org.apache.pekko.actor.typed.scaladsl.TimerScheduler
 
object MetricsActor {
  sealed trait Command
  case class RecordMessage(roomName: String, username: String) extends Command
  case object PrintStats extends Command
 
  def apply(): Behavior[Command] = {
    Behaviors.withTimers { timers =>
      timers.startTimerWithFixedDelay(PrintStats, 30.seconds)
      metricsCollector(Map.empty, 0)
    }
  }
 
  private def metricsCollector(roomStats: Map[String, Int], totalMessages: Int): Behavior[Command] = {
    Behaviors.receive { (context, message) =>
      message match {
        case RecordMessage(roomName, username) =>
          val currentCount = roomStats.getOrElse(roomName, 0)
          metricsCollector(
            roomStats + (roomName -> (currentCount + 1)),
            totalMessages + 1
          )
 
        case PrintStats =>
          context.log.info(s"Total messages: $totalMessages")
          roomStats.foreach { case (room, count) =>
            context.log.info(s"Room $room: $count messages")
          }
          Behaviors.same
      }
    }
  }
}

Integração WebSocket

Vamos conectar nosso sistema de Actors ao mundo real através de WebSockets:

import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.model.ws.{Message, TextMessage}
import org.apache.pekko.http.scaladsl.server.Directives._
import org.apache.pekko.stream.scaladsl.{Flow, Sink, Source}
 
class WebSocketChatServer(chatRoom: ActorRef[ChatRoomActor.Command])(implicit system: ActorSystem[_]) {
 
  def createWebSocketFlow(username: String): Flow[Message, Message, Any] = {
    // Create a user actor for this WebSocket connection
    val userActor = system.systemActorOf(UserActor(username), s"user-$username")
 
    // Join the chat room
    chatRoom ! ChatRoomActor.Join(username, userActor, system.ignoreRef)
 
    val incomingMessages: Sink[Message, Any] =
      Flow[Message]
        .collect {
          case TextMessage.Strict(text) => text
        }
        .to(Sink.foreach { text =>
          chatRoom ! ChatRoomActor.BroadcastMessage(username, text)
        })
 
    val outgoingMessages: Source[Message, Any] =
      Source.actorRef[String](bufferSize = 10, OverflowStrategy.dropHead)
        .map(TextMessage(_))
 
    Flow.fromSinkAndSource(incomingMessages, outgoingMessages)
  }
 
  def routes = path("chat" / Segment) { username =>
    get {
      handleWebSocketMessages(createWebSocketFlow(username))
    }
  }
}

Técnicas Avançadas de Logging

Logging eficaz é crucial para depurar sistemas de Actors:

import org.apache.pekko.actor.typed.scaladsl.ActorContext
import org.slf4j.MDC
 
object LoggingUtils {
  def withMDC[T](context: ActorContext[_], kvPairs: (String, String)*)(block: => T): T = {
    // Set up Mapped Diagnostic Context
    kvPairs.foreach { case (k, v) => MDC.put(k, v) }
    MDC.put("actorPath", context.self.path.toString)
    MDC.put("actorClass", context.self.path.name)
 
    try {
      block
    } finally {
      // Clean up MDC
      kvPairs.foreach { case (k, _) => MDC.remove(k) }
      MDC.remove("actorPath")
      MDC.remove("actorClass")
    }
  }
}
 
// Usage in actors:
LoggingUtils.withMDC(context, "operation" -> "join", "username" -> username) {
  context.log.info("User joining chat room")
}

Estratégias de Teste

Testar sistemas de Actors requer técnicas especiais. Aqui está uma abordagem de teste abrangente:

Teste Unitário de Actors Individuais

import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import org.scalatest.wordspec.AnyWordSpecLike
 
class UserActorSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike {
 
  "UserActor" must {
    "handle send message command" in {
      val userActor = spawn(UserActor("testUser"))
      val probe = createTestProbe[UserActor.MessageSent]()
 
      userActor ! UserActor.SendMessage("Hello World", probe.ref)
 
      probe.expectMessage(UserActor.MessageSent(true))
    }
 
    "log received messages" in {
      val userActor = spawn(UserActor("testUser"))
 
      userActor ! UserActor.MessageReceived("otherUser", "Hello!")
 
      // Verify through log inspection or behavior observation
    }
  }
}

Teste de Integração com Test Probes

class ChatRoomIntegrationSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike {
 
  "ChatRoom integration" must {
    "handle user join and message broadcast" in {
      val chatRoom = spawn(ChatRoomActor())
      val user1Probe = createTestProbe[UserActor.Command]()
      val user2Probe = createTestProbe[UserActor.Command]()
      val joinProbe = createTestProbe[ChatRoomActor.JoinResult]()
 
      // User 1 joins
      chatRoom ! ChatRoomActor.Join("user1", user1Probe.ref, joinProbe.ref)
      joinProbe.expectMessage(ChatRoomActor.JoinResult(true, "Welcome user1!"))
 
      // User 2 joins
      chatRoom ! ChatRoomActor.Join("user2", user2Probe.ref, joinProbe.ref)
      joinProbe.expectMessage(ChatRoomActor.JoinResult(true, "Welcome user2!"))
 
      // User 1 receives join notification for user 2
      user1Probe.expectMessage(UserActor.MessageReceived("System", "user2 joined"))
 
      // Broadcast message
      chatRoom ! ChatRoomActor.BroadcastMessage("user1", "Hello everyone!")
 
      // User 2 should receive the message
      user2Probe.expectMessage(UserActor.MessageReceived("user1", "Hello everyone!"))
 
      // User 1 should not receive their own message
      user1Probe.expectNoMessage(100.millis)
    }
  }
}

Teste de Carga

Para prontidão de produção, inclua teste de carga:

class ChatSystemLoadSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike {
 
  "ChatRoom under load" must {
    "handle 1000 concurrent users" in {
      val chatRoom = spawn(ChatRoomActor())
      val users = (1 to 1000).map { i =>
        val probe = createTestProbe[UserActor.Command]()
        val joinProbe = createTestProbe[ChatRoomActor.JoinResult]()
 
        chatRoom ! ChatRoomActor.Join(s"user$i", probe.ref, joinProbe.ref)
        joinProbe.expectMessage(5.seconds, ChatRoomActor.JoinResult(true, s"Welcome user$i!"))
 
        (s"user$i", probe)
      }
 
      // Send 100 messages from random users
      (1 to 100).foreach { _ =>
        val randomUser = users(scala.util.Random.nextInt(1000))._1
        chatRoom ! ChatRoomActor.BroadcastMessage(randomUser, s"Message ${System.currentTimeMillis()}")
      }
 
      // Verify system remains responsive
      val testUser = users.head
      chatRoom ! ChatRoomActor.BroadcastMessage("testLoad", "System still responsive")
 
      // All other users should receive this message
      users.tail.foreach { case (_, probe) =>
        probe.expectMessageType[UserActor.MessageReceived](3.seconds)
      }
    }
  }
}

Lições do Mundo Real Aprendidas

Após implementar sistemas de Actors em produção, aqui estão os insights-chave:

1. Design para Fluxo de Mensagens

Pense sobre seu sistema em termos de fluxos de mensagens ao invés de interações de objetos. Desenhe diagramas de sequência de mensagens antes de escrever código.

2. Abrace Assincronia

Não lute contra a natureza assíncrona de actors. Use padrões ask com moderação e prefira tell com callbacks ou IDs de correlação de mensagens.

3. Monitore Tamanhos de Mailbox

Actors com mailboxes crescendo indicam problemas de backpressure. Implemente circuit breakers e load shedding.

4. Planeje para Falhas

Projete sua hierarquia de supervisão cuidadosamente. Nem toda falha deve reiniciar um actor – às vezes degradação graciosa é melhor.

5. Teste Protocolos de Mensagens

Seus protocolos de mensagens são seus contratos de API. Teste-os completamente, incluindo condições de erro e casos extremos.

Considerações de Performance

Sistemas de Actors bem projetados alcançam números impressionantes em diversas dimensões. O throughput de mensagens pode chegar a milhões por segundo, enquanto a eficiência de memória permanece alta já que actors têm overhead menor que threads tradicionais. A escalabilidade é linear através de cores com design adequado, e a latência de passagem de mensagens pode alcançar níveis sub-microssegundo.

Dicas de Otimização

  1. Agrupe operações relacionadas dentro de actors
  2. Use mensagens imutáveis para prevenir compartilhamento acidental
  3. Implemente mecanismos de backpressure
  4. Perfile tamanhos de mailbox e tempos de processamento
  5. Considere pooling de Actors para trabalho intensivo em CPU

O Caminho à Frente

O Actor Model representa uma mudança fundamental em como abordamos programação concorrente. Ao eliminar estado mutável compartilhado e abraçar passagem de mensagens, podemos construir sistemas que são mais resilientes a falhas, mais fáceis de raciocinar, naturalmente escaláveis e manuteníveis ao longo do tempo.

Conclusão

Percorremos desde os desafios históricos da OOP em ambientes concorrentes, através das armadilhas específicas do estado compartilhado, até uma implementação completa de um sistema de chat baseado em Actors. O Actor Model não é apenas outro padrão de programação concorrente – é uma forma diferente de pensar sobre como sistemas devem ser estruturados.

Principais conclusões de nossa série de três partes:

  1. OOP tradicional luta com programação concorrente devido ao estado mutável compartilhado
  2. Race conditions, deadlocks e outros bugs de concorrência são eliminados por design em sistemas de Actors
  3. Arquiteturas de passagem de mensagens fornecem tolerância a falhas natural e escalabilidade
  4. Apache Pekko oferece uma implementação pronta para produção e open-source para a JVM
  5. Testes e monitoramento requerem técnicas especializadas mas fornecem excelente observabilidade

A transição para pensamento baseado em Actors nem sempre é fácil – requer desaprender alguns hábitos profundamente enraizados de OOP. Mas para sistemas que precisam lidar com concorrência em escala, o Actor Model fornece um caminho para construir aplicações robustas, manuteníveis e performáticas.

Se você está construindo sistemas de chat em tempo real, plataformas IoT, sistemas de trading financeiro ou microserviços distribuídos, os princípios que exploramos nesta série servirão bem.


Obrigado por se juntar a nós nesta jornada através do Actor Model na JVM. O futuro da programação concorrente é passagem de mensagens, e com Apache Pekko, esse futuro está disponível hoje.

Recursos e Leitura Adicional

Actor Model on the JVMPart 3 of 3
Series Progress3 / 3
Arthur CostaA

Arthur Costa

Senior Full-Stack Engineer & Tech Lead

Senior Full-Stack Engineer with 8+ years in React, TypeScript, and Node.js. Expert in performance optimization and leading engineering teams.

View all articles →