Neste cenário, o Akka é uma opção interessante que usa um paradigma diferente de concorrência que possibilita uma escalabilidade muito grande.
Criar aplicações que suportam alta concorrência sempre foi um desafio, independente da tecnologia adotada. O crescimento da capacidade de processamento dos computadores, resultado do aumento do número de núcleos, memória, disco, etc. permitiu a criação de aplicações com mais poder computacional e com suporte a um maior número de requisições.
E quando o número de requisições extrapola a capacidade de um servidor, podemos ainda estruturar a aplicação para funcionar em um cluster de máquinas, aumentando mais a capacidade de concorrência.
Nos primórdios do Java, quando precisávamos lidar com concorrência, criávamos uma ou várias threads e administrávamos manualmente esse volume necessário para a execução eficiente de determinada tarefa.
Na versão 5, o Java trouxe uma API de concorrência de alto nível, a java.util.concurrent [1], nos livrando da gestão manual das threads através da utilização de Executors. Algum tempo depois, no Java 7, foi introduzido o Fork/Join framework [2], otimizando a utilização de threads e elevando ainda mais a capacidade de concorrência.
Contudo, mesmo com estas evoluções na API de concorrência do Java, esta ainda se trata de algo de baixo nível quando desejamos criar aplicações com alta concorrência sem nos preocuparmos com detalhes de threads, pool de threads, etc. Neste contexto, o Akka [3] é um framework que nos oferece esta desejada abstração.
Escrito em Scala e com API para Scala e Java, o Akka foi desenvolvido sobre a API de concorrência do Java e que possibilita ao desenvolvedor utilizar um modelo de concorrência baseado em atores. Com o Akka, todas as buzzwords de concorrência, como threads, pools, locks, etc., deixam de fazer sentido.
Assim, nos concentramos apenas na estruturação da nossa aplicação em atores e na lógica de negócio de cada ator. Porém, antes de começarmos a falar de atores, vamos entender em que modelo de concorrência os atores se encaixam, para compreendê-los melhor.
Modelos de concorrência
De maneira generalista, podemos dividir os modelos de concorrência em dois paradigmas: concorrência usando estado compartilhado e concorrência usando troca de mensagens. O modelo de estado compartilhado tem sido largamente adotado no campo, mas em aplicações maiores ou com maior concorrência, o modelo de troca de mensagens é preferido, pois sua característica assíncrona facilita a distribuição do processamento, reduzindo gargalos.
Estado compartilhado x troca de mensagens
O modelo de estado compartilhado é o modelo que adotamos normalmente em nossas aplicações. Neste modelo o código a ser paralelizado é executado simultaneamente através da criação de processos ou threads. A complexidade aumenta quando estas threads precisam acessar a mesma informação (estado compartilhado).
Para resolver este problema usamos blocos synchronized, que acabam gerando um gargalo na aplicação, pois as instruções protegidas por synchronized são executadas apenas por uma thread por vez. Com o crescimento da aplicação, consequentemente a quantidade de blocos synchronized cresce, eventualmente causando lentidão e casualmente um dead-lock.
Derek Wyatt, engenheiro de software atuante na comunidade Akka/Scala, no seu livro “Akka Concurrency” [4], nos brinda com uma definição perfeita sobre este modelo: “Em concorrência com estado compartilhado tendemos a criar os problemas primeiro, então resolvê-los usando primitivas de sincronização.”
. A Figura 1 ilustra o modelo de estado compartilhado e seus gargalos.
Figura 1. Representação gráfica do modelo de concorrência baseado em estado compartilhado.
No modelo de troca de mensagens (ver Figura 2) não temos o problema de locks encontrado no modelo de estado compartilhado, pois os componentes não compartilham estado e se comunicam através de mensagens predominantemente assíncronas. O envio das mensagens normalmente é feito por algum outro software que atua como intermediário entre os componentes.
Este “desconhecimento” entre os componentes garante um bom nível de desacoplamento e favorece a distribuição, pois permite a troca de mensagens entre componentes que estejam em servidores diferentes ou até em outras redes. O modelo de atores é uma derivação do modelo de troca de mensagens, e falaremos sobre ele agora.
Figura 2. Representação gráfica do modelo de concorrência baseado em troca de mensagens.
Modelo de Atores
Em 1973, Carl Hewitt, Peter Bishop e Richard Steiger, introduziram pela primeira vez o modelo de atores, no paper “A Universal Modular Actor Formalism for Artificial Intelligence”. Nos anos seguintes, diversas intervenções foram feitas neste modelo e diversos papers foram publicados, o que culminou na definição da teoria do modelo de atores.
Fundamentalmente, o modelo de atores prega que “tudo são atores”. Os atores são definidos como entidades capazes de realizar um processamento computacional e que se comunicam entre si através do envio e recebimento de mensagens.
Além disso, podem criar outros atores, estabelecendo assim uma hierarquia entre eles. Como a comunicação entre atores deve ser feita estritamente pela troca de mensagens, o estado interno de um ator não é acessível por outros, mas somente por ele mesmo.
Aliando isso ao fato do processamento das mensagens ser sequencial, isto é, um ator poder ter várias mensagens pendentes, mas apenas uma ser processada por vez, os problemas com locks simplesmente deixam de existir. A Figura 3 mostra uma visão simplificada do modelo de atores.
Figura 3. Representação gráfica do modelo de concorrência baseado em atores
Com estas características, este tipo de solução vem sendo adotado para aplicações que processam grandes volumes de dados, pois para atender esse tipo de demanda usando o modelo convencional de threads e o processamento síncrono, ficamos limitados pela capacidade individual dos servidores.
Por sua vez, o modelo de atores permite uma distribuição mais fácil dos componentes da aplicação devido a sua natureza assíncrona e baseada em mensagens. O Erlang [5], linguagem originada na Ericsson com o propósito de suportar aplicações distribuídas altamente concorrentes, possui em seu core uma implementação deste modelo.
Contudo, apesar do Erlang ser uma linguagem de uso geral, não conquistou grande adoção. Em razão disso, faltava ao mercado uma solução para alta concorrência baseada em atores. Assim surgiu o Akka.
Akka
O Akka foi desenvolvido em 2009 por Jonas Bonér, inspirado pelo modelo de atores do Erlang. Seu primeiro release público, o Akka 0.5, foi anunciado em Janeiro de 2010. Atualmente esta solução é mantida pela Typesafe Inc., a mesma empresa que mantém o Scala e o Play Framework. Recentemente, o Akka passou a fazer parte da API padrão do Scala, substituindo o modelo de atores original desta linguagem, e apesar de ser escrito em Scala, possui uma API oficial também para Java.
De acordo com a definição da equipe, o Akka é um “toolkit e um runtime para construir aplicações altamente concorrentes, distribuídas e tolerantes a falhas na JVM”. Assim, aplicações criadas usando o Akka já nascem prontas para operar de maneira distribuída, pois esta característica está na essência do framework.
Criado com o foco em aplicações distribuídas e otimizado para operar de maneira standalone, o Akka provê:
· Atores: conforme explicado anteriormente, a base do Akka é sua implementação do modelo de atores;
· Futures: apesar de podermos utilizar atores para resolver praticamente todos os problemas, às vezes pode ser mais conveniente utilizar Futures. Os futures do Akka são muito parecidos com a implementação padrão do java.util.concurrent, porém possui integração com atores;
· Scheduler: integrado ao contexto de atores, o Akka permite o agendamento de tarefas usando vários tipos de schedulers.
Devido a estas características e recursos, o Akka vem sendo adotado por várias empresas de porte. Dentre elas, podemos citar a Amazon, Autodesk, Blizzard, VMware, dentre outros.
Hello World – Primeiros passos
Uma forma de desenvolver aplicações com o Akka é fazer com que o framework seja uma dependência do projeto e usar a própria aplicação para iniciar o sistema de atores. Além dessa, existe outra opção para desenvolver tais aplicações: usando o Typesafe Activator, que é um software que depois de instalado pode ser utilizado para criar e executar aplicações diversas baseadas em templates.
O Activator na verdade oferece muito mais, como por exemplo, uma IDE totalmente web para manutenção dos projetos gerados por ele. Tanto o Activator quanto as dependências do Akka podem ser obtidos na página de downloads do site do próprio Akka [6]. Neste artigo, adotaremos a primeira opção, pois esta permite uma compreensão melhor do funcionamento do framework.
Para criar uma aplicação usando o Akka podemos usar uma ferramenta de build como o Maven ou o SBT. Aqui optaremos pelo Maven, por ele ser mais empregado pela comunidade. Antes de criar a aplicação, você deve ter o Eclipse com o plugin do Maven (m2e) instalado, ou apenas o Maven, caso você prefira realizar o build pela linha de comando.
Dito isso, crie um diretório para hospedar o código da aplicação, e depois crie o pom.xml com a dependência do Akka, conforme indica a Listagem 1.
Listagem 1. pom.xml base de uma aplicação com Akka.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.devmedia.akka</groupId>
<artifactId>hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependencies>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_2.10</artifactId>
<version>2.2.3</version>
</dependency>
</dependencies>
</project>
Em seguida, crie uma classe no diretório src/main/java chamada Start. Esta classe será responsável por iniciar a aplicação e o sistema de atores do Akka. Inicialmente ela deve estar vazia, conforme a Listagem 2.
Listagem 2. Classe que irá iniciar o sistema de atores do Akka.
public class Start {
public static void main(String[] args) {
// aqui inicializaremos o Akka
}
}
Agora, antes de pormos as mãos na massa, vamos analisar o conceito de sistema de atores do Akka.
Actor System – O container de atores
Como principal característica, o Akka implementa um modelo de atores. De modo simples, este modelo diz que tudo são atores. Sendo assim, a lógica da aplicação que normalmente é programada em classes que costumamos chamar de “o modelo da aplicação” ou os “componentes de negócio”, deve ser estruturada em um ou vários atores que se comunicam de maneira assíncrona através da troca de mensagens, todos sob a gestão do actor system.
Veremos adiante que para criar um ator basta estender uma determinada classe do Akka e usar a sua API para solicitar a instanciação. Contudo, para dar vida aos atores, precisamos antes inicializar o container Akka, ou seja, iniciar o actor system.
O actor system é o ponto de partida de toda aplicação, por isso, deve ser criado assim que ela iniciar. Em relação ao número de actor systems, uma aplicação pode iniciar vários deles, porém este tipo de design não é indicado por questões de organização.
Para criar um actor system devemos usar a classe akka.actor.ActorSystem, como demonstra a Listagem 3.
Listagem 3. Criação do actor system.
import akka.actor.ActorSystem;
public class Start {
public static void main(String[] args) {
// Criação de um Actor System, container Akka.
ActorSystem system = ActorSystem.create("HelloSystem");
}
}
Podemos observar que o construtor do actor system recebe um texto como parâmetro para definir o seu nome. Este nome deve ser único por aplicação e também deve ser único por container, caso a aplicação seja instalada em um microcontainer Akka. Como veremos adiante, o nome do actor system também irá compor o path de localização do ator.
Atores em todos os lugares
De acordo com a explicação anterior, no modelo de atores um ator é um objeto capaz de receber e enviar mensagens, além de criar outros atores. No processamento das mensagens recebidas está a lógica de negócio do ator. No exemplo que estamos desenvolvendo, iremos criar um ator que recebe uma mensagem e exibe esta mensagem no console.
No Akka, para criarmos a classe de um ator, devemos estender a classe abstrata akka.actor.UntypedActor e implementar o método void onReceive(Object msg). Este método é responsável pelo processamento das mensagens. Deste modo, crie a classe EcoActor em src/main/java contendo o código da Listagem 4.
Listagem 4. Um ator que exibe no console a mensagem recebida.
import akka.actor.UntypedActor;
import akka.event.LoggingAdapter;
import akka.event.Logging;
public class EcoActor extends UntypedActor {
LoggingAdapter log = Logging.getLogger(getContext().system(), this);
@Override
public void onReceive(Object msg) throws Exception {
log.info("Mensagem recebida: " + msg);
}
}
Deixando os detalhes de criação do logger para depois, podemos reparar que a classe de um ator é simples. É importante notar que a mensagem pode ser qualquer objeto Java, porém é razoável que este implemente Serializable (apesar do Akka não obrigar) para não termos problemas em um ambiente distribuído, onde mensagens são serializadas para trafegar pela rede.
Voltando para a classe Start, agora que temos um ator implementado, podemos usar o actor system para efetivamente dar vida a este ator. Assim, usamos o método actorOf() de actor system para criar o ator. Este método recebe como parâmetro uma instância de akka.actor.Props e uma String com o nome que o ator terá no actor system. Props é uma classe que define um objeto de configuração, usado apenas para criar atores.
Para a construção do objeto Props, usamos o método estático Props.create(), passando como parâmetro a classe do ator. Existem muitas maneiras de construir Props, mas para o nosso exemplo, usaremos a forma mais simples. A Listagem 5 mostra a classe Start alterada, com a criação do ator.
Listagem 5. Criando o ator EcoActor.
import akka.actor.ActorSystem;
import akka.actor.ActorRef;
import akka.actor.Props;
public class Start {
public static void main(String[] args) {
// Criação de um Actor System, que é o container Akka.
ActorSystem system = ActorSystem.create("HelloSystem");
// Criando o ator EcoActor
ActorRef actor = system.actorOf(Props.create(EcoActor.class), "eco");
}
}
É importante perceber que o que obtemos ao criar um ator é um ActorRef e não um objeto com a classe do ator propriamente dita (EcoActor). Nesta informação reside um grande conceito do Akka e do modelo de atores: os detalhes do ator não são expostos para quem envia a mensagem, pois o estado interno deste deve ser visível apenas para ele mesmo. Isso fica bem claro na implementação do ator.
Basta percebermos que o código processado por ele (sua lógica de negócio) se restringe ao método onReceive(), que é chamado pelo Akka para processar as mensagens. Como as mensagens são processadas uma por vez (ou seja, apenas uma thread por vez), não existe nenhuma necessidade de blocos synchronized. Com isso, locks e deadlocks não existem neste modelo.
ActorRef é a interface para comunicação com o ator criado e o método ActorRef.tell() é utilizado para enviar uma mensagem para este ator. Este método recebe como parâmetro a mensagem e o ator que a está enviando. Como estamos enviando a mensagem do método main() da casse Start (isto é, não é um ator que está enviando a mensagem), usaremos ActorRef.noSender(), que é um método auxiliar que retorna uma constante para identificar ao actor system que não existe nenhum ator origem. A Listagem 6 mostra o envio dessa mensagem ao ator.
Listagem 6. Enviando uma mensagem para EcoActor.
import akka.actor.ActorSystem;
import akka.actor.ActorRef;
import akka.actor.Props;
public class Start {
public static void main(String[] args) {
// Criação de um Actor System, que é o container Akka.
ActorSystem system = ActorSystem.create("HelloSystem");
// Criando o ator EcoActor
ActorRef ecoActor = system.actorOf(Props.create(EcoActor.class), "eco");
// Enviando a mensagem ao ator
ecoActor.tell("Alô Mundo com Atores", ActorRef.noSender());
}
}
Com essas duas classes já é possível ver o Akka em ação, mas antes, precisamos compilar a aplicação. Para isso, usaremos o Maven através do comando mvn clean install. Em seguida, executaremos a aplicação também pelo Maven, com o plugin exec:java, como indica a Listagem 7.
Listagem 7. Executando a aplicação.
$ mvn clean install exec:java -Dexec.mainClass=Start
.
. (detalhes do build omitidos)
.
[INFO] [01/01/2014 19:00:00.384] [HelloSystem-akka.actor.default-dispatcher-3] [akka://HelloSystem/user/eco] Mensagem recebida: Alô Mundo com Atores
Note que a mensagem enviada (“Alô Mundo com Atores”) foi processada pelo ator e impressa no console conforme esperado. O log do actor system ainda informa o timestamp, o nome da thread e o nome do ator que está logando.
Falaremos destes detalhes adiante. Para interromper a execução, digite Ctrl+c, caso contrário o actor system continuará executando.
Antes de partir para a modelagem de uma aplicação mais próxima do real, vamos apresentar algumas funcionalidades do Akka usando o tradicional “Hello, World!”.
Atores criando atores
Conforme descrito anteriormente, um ator pode iniciar (ou criar) outros atores, estabelecendo uma hierarquia entre eles. Esta hierarquia é definida de acordo com a modelagem da aplicação. Contudo, nada impede de modelarmos uma aplicação usando o Akka e criar todos os atores na raiz do actor system, como fizemos com EcoActor.
Apesar dessa possibilidade, por questões de organização e de gestão de exceções, é importante definir uma estrutura coesa de atores e normalmente isso implica em vários níveis.
Para criar um ator a partir de outro, é indicado fazê-lo dentro do método preStart() de UntypedActor, apesar de nada impedir de fazermos isso sob demanda, ou seja, na primeira vez que o ator for utilizar o ator filho.
O actor system executa o prestar() (dentre outros que ainda analisaremos do ciclo de vida dos atores) antes de completar a inicialização de um ator. Sendo assim, este é um excelente lugar para inicializar eventuais estados internos do ator, como por exemplo, dependência de outros atores ou atributos privados utilizados por ele.
Tendo em vista estas informações, vamos alterar o código da aplicação exemplo para que o EcoActor crie um ator na sua inicialização e repasse toda mensagem recebida por ele para este ator “filho”.
Para isso, declare uma classe chamada ChildActor com o mesmo código de EcoActor (ver Listagem 4). Agora, devemos alterar EcoActor, sobrescrever o método preStart() e instanciar o ator “filho”, do tipo ChildActor. Assim como a criação de um ator via actor system, para instanciar um ator dentro de outro usamos o método actorOf() do contexto do ator.
Este contexto é obtido através do método getContext(). A Listagem 8 mostra a implementação do método preStart() e a criação do ator filho, bem como o repasse das mensagens recebidas em onReceive().
Listagem 8. Criando um ator filho via getContext().
public class EcoActor extends UntypedActor {
LoggingAdapter log = Logging.getLogger(getContext().system(), this);
//Declaramos o ator filho como atributo de EcoActor
private ActorRef childActor;
@Override
public void preStart() throws Exception {
super.preStart();
//Na inicialização do ator, instanciamos o ator filho
childActor = getContext().actorOf(Props.create(ChildActor.class), "childOfEco");
}
@Override
public void onReceive(Object msg) throws Exception {
log.info("Mensagem recebida: " + msg);
//Repassamos a mensagem recebida para o ator filho
childActor.tell(msg, getSelf());
}
}
Para ver os resultados destas alterações, devemos repetir os comandos de compilação e execução via Maven, como mostra a Listagem 9.
Listagem 9. Executando a aplicação.
$ mvn clean install exec:java -Dexec.mainClass=Start
.
. (detalhes do build omitidos)
.
[INFO] [01/01/2014 10:29:26.722] [HelloSystem-akka.actor.default-dispatcher-2] [akka://HelloSystem/user/eco] Mensagem recebida: Alô Mundo com Atores
[INFO] [01/01/2014 10:29:26.722] [HelloSystem-akka.actor.default-dispatcher-3] [akka://HelloSystem/user/eco/childOfEco] Mensagem recebida: Alô Mundo com Atores
O log exibido no resultado mostra claramente a hierarquia dos atores, exposta pelo path do ator. Repare que a mensagem foi recebida por EcoActor, que tem o path akka://HelloSystem/user/eco, e repassada ao ator filho, que tem o path akka://HelloSystem/user/eco/childOfEco.
O path de um ator é uma URI única que o identifica. Ela é composta pelo protocolo (akka://), o identificador do actor system (HelloSystem), pela raiz dos atores criados pela aplicação (user), o identificador do ator pai (caso exista) e o identificador do próprio ator. Sendo assim, na estrutura de atores criada pela aplicação exemplo, temos dois atores identificáveis por dois paths:
akka://HelloSystem/user/eco
akka://HelloSystem/user/eco/childOfEco
O identificador do actor system (HelloSystem) é definido no start da aplicação, e o identificador de cada ator é definido no momento de sua criação. Apenas o prefixo user é algo imposto pelo Akka, pois ele define a raiz da hierarquia dos atores. Na prática, todos os atores criados pelas aplicações desenvolvidas com Akka ficarão sob o guardião chamado user. Existem guardiões disponíveis para diversas finalidades, a saber:
- /user: É o guardião de todos os atores criados pelo código da aplicação desenvolvida usando Akka;
· /system: Este é o guardião de atores criados pelo próprio Akka, para manter o funcionamento do framework;
· /deadLetters: É um ator que recebe as mensagens enviadas para atores inexistentes, ou atores que não estão em funcionamento;
· /temp: É o guardião de atores criados para necessidades temporárias, isto é, são atores de vida curta;
· /remote: Este é um guardião para paths virtuais, que representam atores remotos, ou seja, que estejam instanciados em outras JVMs.
Mensagens tipadas
É muito comum no desenvolvimento de aplicações Akka um determinado ator ser programado para receber vários tipos de mensagens e realizar processamentos distintos de acordo com a mensagem. Neste caso o padrão adotado é utilizar classes diferentes para representar os diferentes tipos de mensagens.
A vantagem de usar este padrão é que ele deixa claro e visível os tipos de mensagens que um ator processa e principalmente os que ele não processa. Para exemplificar, vamos refatorar nosso código para enviar uma mensagem tipada para nossos atores e refutar qualquer outra mensagem. Para isto, criaremos uma classe chamada HelloMessage, conforme a Listagem 10.
Listagem 10. Classe que representará a mensagem.
public class HelloMessage {
}
Como pode ser observado, esta classe não contém nenhum estado interno, pois o seu propósito é apenas identificar um tipo de mensagem. Dito isso, depois de criá-la, devemos alterar os nossos atores, para deixar claro que eles recebem e processam apenas esse tipo de mensagem. Sendo assim, o método receive() de EcoActor deve ficar como exposto na Listagem 11.
Listagem 11. Alteração do método onReceive() de EcoActor.
@Override
public void onReceive(Object msg) throws Exception {
if (msg instanceof HelloMessage) {
log.info("Mensagem recebida: " + msg);
// repassamos a mensagem recebida para o ator filho
childActor.tell(msg, getSelf());
}
else {
// informa ao actor system que este ator não processa esta mensagem
unhandled(msg);
}
}
Repare que usamos instanceof para filtrar as mensagens processadas de acordo com o tipo destas. Neste código é importante ressaltar a chamada ao método unhandled(), que deixa claro ao actor system que esta mensagem não foi processada pelo ator. Deste modo, ela será considerada pelo actor system para eventuais tratamentos futuros.
Se o método unhandled() não for chamado, o actor system não tem como saber que a mensagem não foi processada. Para dar continuidade ao exemplo, devemos alterar também ChildActor, para receber apenas mensagens do tipo HelloMessage. Veja a Listagem 12.
Listagem 12. Alteração do método onReceive() de ChildActor.
@Override
public void onReceive(Object msg) throws Exception {
if (msg instanceof HelloMessage) {
log.info("Mensagem recebida: " + msg);
}
else {
unhandled(msg);
}
}
Ao executarmos a aplicação (Listagem 9) após essas alterações, será possível notar que nenhum log será exibido. Isso é facilmente explicado pelo fato de nossos atores agora apenas processarem mensagens do tipo HelloMessage e a mensagem enviada pela classe Start ser do tipo String. Para corrigir isso, devemos alterar a classe Start de acordo com a Listagem 13.
Listagem 13. Alterando o envio da mensagem em Start.
// Enviando a mensagem ao ator
ecoActor.tell(new HelloMessage(), ActorRef.noSender());
Com essa simples mudança, ao reexecutar a aplicação teremos o resultado apresentado na Listagem 14.
Listagem 14. Resultado da reexecução após as alterações.
$ mvn clean install exec:java -Dexec.mainClass=Start
.
. (detalhes do build omitidos)
.
[INFO] [02/02/2014 11:22:00.668] [HelloSystem-akka.actor.default-dispatcher-3] [akka://HelloSystem/user/eco] Mensagem recebida: HelloMessage@7ee14288
[INFO] [02/02/2014 11:22:00.668] [HelloSystem-akka.actor.default-dispatcher-4] [akka://HelloSystem/user/eco/childOfEco] Mensagem recebida: HelloMessage@7ee14288
Supervisores e estratégia de recuperação
Todos sabem o que acontece quando uma exceção é lançada em um sistema convencional. A exceção é lançada a camadas superiores de serviços até que uma dessas camadas tenha a capacidade de lidar com ela. No modelo de atores o que acontece é um pouco diferente, evidentemente por conta do desacoplamento entre os componentes e pela característica assíncrona das chamadas.
Quando uma exceção é lançada no processamento de uma mensagem, o supervisor do ator que lançou a exceção deve decidir o que vai acontecer com o ator. O supervisor de um ator é o ator que o criou, ou seja, o seu pai.
Caso o ator tenha sido criado diretamente pelo actor system (como os atores criados na classe Start nos exemplos), o ator não tem um pai conhecido ou explícito. Neste caso, o supervisor passa a ser o ator guardião, ou seja, /user.
Antes de vermos o que pode acontecer com um ator que lança uma exceção, vamos entender um pouco mais a respeito do ciclo de vida dos atores.
Ciclo de vida de um ator
É importante ressaltar que no Akka a(s) vida(s) dos atores é(são) representada(s) por encarnações (ver Figura 4). Isso significa que um ator pode “nascer”, “morrer” e “reencarnar” (nascer novamente) e assim sucessivamente. O termo encarnação é de fato utilizado na documentação do Akka e traz à luz alguns conceitos interessantes da vida do ator, a saber:
1. Inicialmente um ator simplesmente não existe até o ponto em que alguma chamada por actorOf() é executada, seja no actor system (raiz) ou pelo contexto de outro ator;
2. Após a chamada de actorOf() o ator ainda não está encarnado, ou seja, ainda não responde por mensagens, porém tem: o path reservado; um identificador único (UID); a instância do objeto criada; o método preStart() executado.
3. Após a execução do preStart() o ator está encarnado e recebendo/processando as mensagens destinadas a ele;
4. Neste ponto algo errado pode acontecer e o ator pode lançar uma exceção. Assim, o ator passa para um estado temporário que chamaremos de doente. Neste momento o supervisor do ator deve decidir o que fazer com o ator doente, tendo três opções:
a. Continuar. Simplesmente ignorar que a exceção aconteceu e continuar com a vida do ator, processando as mensagens normalmente;
b. Reiniciar o ator. O método preRestart() é executado na instância em execução, uma nova instância é criada e o método postRestart() é executado na nova instância. O path do ator se mantém, assim como o UID, caracterizando uma mesma encarnação;
c. Encerrar o ator (morte). O ator pode ser terminado pelo seu supervisor ou pelo envio de uma mensagem que o finalize (existe uma mensagem no Akka do tipo akka.actor.PoisonPill, que ao ser consumida por um ator causa sua interrupção). Neste ponto, o método postStop() é executado. Caso o ator seja iniciado novamente, uma nova encarnação será criada, tendo o mesmo path, porém com um novo UID.
Figura 4. Ciclo de vida dos atores no container do Akka.
A definição da estratégia de recuperação é feita no supervisor, ou seja, no ator pai, e vale para todos os seus filhos. Caso o supervisor de um ator não defina a estratégia de recuperação dos seus filhos, se o ator em questão lançar uma exceção, o Akka tenta usar a estratégia definida pelo pai do pai e assim sucessivamente até a raiz, isto é, o ator guardião /user.
Para definir uma estratégia de recuperação em um ator, devemos sobrescrever o método supervisorStrategy() de UntypedActor. Este método deve retornar uma instância de akka.actor.SupervisorStrategy, que pode ser de dois tipos:
· OneForOneStrategy: Se o retorno do método supervisorStrategy() for uma instância de OneForOneStrategy, significa que a ação tomada irá afetar apenas o ator que lançou a exceção, ou seja, o ator doente;
· AllForOneStrategy: Se o retorno do método supervisorStrategy() for uma instância de AllForOneStrategy, a ação tomada será aplicada a todos os atores filhos do supervisor que está definindo a estratégia.
O trecho de código da Listagem 15 mostra a criação de uma estratégia de recuperação implementada em um ator.
Listagem 15. Implementação de uma estratégia de recuperação.
@Override
public SupervisorStrategy supervisorStrategy() {
return new OneForOneStrategy(-1, Duration.Inf(), new Function<Throwable, Directive>() {
public Directive apply(Throwable t) throws Exception {
return OneForOneStrategy.resume();
}
});
}
Para implementar uma estratégia de recuperação em um supervisor (um ator com filhos), devemos sobrescrever o método supervisorStrategy(), que por sua vez, deve retornar uma instância de SupervisorStrategy. No caso do código exemplo, foi escolhida a opção akka.actor.OneForOneStrategy. Isso significa que a estratégia de recuperação será aplicada apenas no ator que lançou a exceção.
Para construir um objeto do tipo OneForOneStrategy devemos informar três parâmetros no construtor: maxNrOfRetries (número máximo de tentativas de reinício do ator), withinTimeRange (intervalo entre a primeira e a última tentativa de reinício) e decider.
Deixando de lado os dois primeiros parâmetros, o decider, ou decisor, deve ser uma implementação da interface akka.japi.Function e possuir apenas o método chamado apply(), que recebe como parâmetro um Throwable e retorna um akka.actor.SupervisorStrategy.Directive (explicado adiante).
Quando a aplicação estiver em execução e um determinado ator lançar uma exceção no processamento de uma mensagem, o Akka irá executar o método apply() do objeto decider definido na estratégia do ator pai passando como parâmetro a exceção lançada e obtendo como retorno uma diretiva a ser utilizada para tomar a devida ação.
As diretivas podem ser do tipo:
· resume: Ignora a exceção atirada pelo ator supervisionado (filho). A mensagem simplesmente deixa de ser processada e o ator continua processando normalmente outras mensagens;
- restart: Reinicia o ator, conforme explicado anteriormente, na descrição do ciclo de vida do ator;
- escalate: Repassa a decisão do que deve ser feito para o supervisor diretamente acima;
- stop: Finaliza (mata) o ator.
Voltando a falar dos dois primeiros parâmetros da construção de OneForOneStrategy, são aplicáveis somente à ação de restart. O primeiro parâmetro diz quantas vezes o ator pode ser reiniciado e o segundo parâmetro diz em qual intervalo esta quantidade de reinícios pode acontecer.
Este tipo de estratégia faz sentido em situações de erros intermitentes, por exemplo, uma dependência de uma conexão com o banco de dados. Caso este número seja ultrapassado no intervalo definido, o ator é finalizado (morto) e as mensagens enviadas para ele são perdidas.
Modelando atores
Assim como qualquer paradigma de desenvolvimento, o modelo de atores tem as suas peculiaridades. Ao desenvolver uma aplicação usando este modelo, devemos estruturar a aplicação em atores, considerando que a comunicação entre eles é assíncrona e realizada através troca de mensagens. Depois de uma vida inteira desenvolvendo aplicações com o modelo tradicional de concorrência, inicialmente este paradigma poderá parecer um pouco confuso.
Para facilitar o aprendizado a respeito, existe uma grande quantidade de material na internet relacionada à modelagem de atores, mas para ficar apenas no universo do Akka, uma boa referência é o site Let It Crash [7], mantido pela comunidade. Este site possui trechos de código, artigos, discussões e outros conteúdos relacionados ao desenvolvimento com Akka.
Próximos passos
Este artigo procurou introduzir os princípios básicos do paradigma de concorrência baseado em atores e o framework Akka. Por esse motivo, ainda existe muito a ser explorado.
Pensando em facilitar os próximos passos, e direcionando nosso foco para o Akka, a seguir listamos os tópicos mais importantes que o leitor pode se aprofundar:
· Mailbox: Apesar do Akka abstrair a existência deles, cada instância de ator possui uma caixa de mensagens, ou mailbox. Existem vários tipos de customizações que podem ser feitas com este recurso, como por exemplo, podemos criar uma caixa de mensagens para vários atores, balanceando o consumo, realizando uma espécie de load balancing de processamento de mensagens;
· Dispatchers: São implementações de ExecutionContext que regem a criação das threads de processamento de mensagens, schedulers e todo o funcionamento do framework. Também possui um alto grau de customização;
· Persistência: É possível configurar o Akka para persistir o estado interno de um ator, inclusive o seu mailbox. Sendo assim, caso a aplicação caia ou seja desligada indevidamente, no restart do container este estado é recuperado. Este módulo ainda é experimental no Akka e foi introduzido na versão 2.3.0 do framework;
· Extensões: O Akka oferece um mecanismo padrão para criar extensões. Com ele podemos atuar sobre elementos centrais do framework, como o Actor system, para modificar o seu comportamento.
O modelo de concorrência baseado em threads e estado compartilhado tem atendido razoavelmente as necessidades do mercado, porém, quando nos deparamos com o desafio de construir uma aplicação que suporte alta concorrência e alta disponibilidade, é importante considerarmos um modelo baseado em mensagens assíncronas.
No ecossistema do Java, o Akka é a ferramenta ideal para esta finalidade e tem se tornado mais completo a cada release. Além de suas qualidades técnicas, é seguro apostar no Akka não apenas por tratar-se de uma tecnologia suportada e mantida por uma empresa do porte da Typesafe, mas também por ser um excelente framework, leve, bem estruturado, modular e muito fácil de escalar.
[1]
Página da API de
concorrência do Java.
http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/package-
summary.html
[2]
Documentação do Fork/Join framework.
http://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html
[3]
Página principal do Akka.
http://akka.io
[4]
Página da Amazon do livro Akka Concurrency, de Derek Wyatt.
http://www.amazon.com/Akka-Concurrency-Derek-Wyatt/dp/0981531660
[5]
Página da linguagem de programação Erlang.
http://www.erlang.se
[6]
Página de downloads do Akka.
http://akka.io/downloads/
[7]
Página de um grupo de desenvolvedores da comunidade Akka.
http://letitcrash.com