Play framework e os caixas rápidos

Antes de iniciar gostaria de dizer que esse é meu primeiro post aqui no Blog, espero que o material seja útil e ajude vocês.

É muito comum em situações de stress uma aplicação Play passar para os seus usuários a sensação de travamento, aqui falarei um pouco sobre esse problema e dar uma dica de como resolvê-lo. Estou partindo do pressuposto que você conhece um pouco de Play framework.

Os caixas rápidos

Imagine que você é dono de um pequeno mercado e estamos no dia 24 de dezembro, já consegue visualizar? Muitos clientes estão fazendo aquela compra enorme, uma ceia para 20 ou 30 pessoas, você também tem muitos clientes que estão ali fazendo suas compras semanais e eventualmente aqueles que esqueceram um ingrediente de alguma receita exótica que viram na internet.

Você provavelmente quer todos os seus clientes saiam do seu mercado felizes e no menor tempo possível e que não desistam de comprar com você.

Uma estratégia costumeira no mundo do atendimento é a divisão do público, aqueles que tem demandas menores são atendidos em caixas separados (normalmente alocando menos recursos) e os que exigem mais tempo podem ser distribuídos entre os outros caixas.

Veremos aqui como usar o Play framework para garantir os encontros de família, as compras semanais e que as receitas exóticas sejam elogiadas!

Play framework

Confesso que não sou um grande conhecedor do Play mas um pouco que aprendi dele talvez seja útil para você, então vamos lá.

Como os próprios criadores do Play diriam ele é uma alternativa simples aos grandes e inchados Java Enterprise Frameworks 1. O que isso quer dizer? Trocando em miúdos ele já tem um bom conjunto de funcionalidades e uma forma de produzir aplicações web que torna o desenvolvimento ágil e pouco cansativo.

Além de funcionalidades o Play traz para o desenvolvedor um conceito bem interessante de programação não bloqueante. Esse conceito diz que uma thread não deve ser interrompida para que algum código muito oneroso computacionalmente seja executado. Mas se uma thread não pode ser interrompida e você precisa executar esse código muito oneroso como resolver esse problema?

Thread pool

Antes de responder essa questão algo importante precisa ser dito. O Play faz uso de um pool de threads2, quando uma requisição é feita uma thread desse pool é usada para processá-la. Idealmente esse número de threads deve ser igual a o número de núcleos + 1 do servidor onde a aplicação será executada.

Programação assíncrona

De forma muito simples eu diria para você que programação assíncrona seria algo como por exemplo:

Abra esse arquivo e quando ele estiver pronto chame essa função.

Tecnicamente falando é mais complexo do que isso, mas o conceito que quero chamar a atenção é a ideia de você "delegar" uma tarefa e quando essa tarefa estiver pronta algo deve ser feito. Pensando assim, enquanto essa tarefa é realizada eu estaria liberado para fazer outras coisas?

Se você pensou nisso está certo!

Se algo deve ser feito enquanto estou fazendo outra coisa isso exige que pelo menos dois códigos estejam executando ao mesmo tempo?

Novamente está certo!

Um jeito de programar uma tarefa para ser executada assincronamente em Java é implementando a interface Callable do pacote java.util.concurrent em conjunto com alguma implementação da interface ExecutorService do mesmo pacote.

A implementação da interface Callable representa a tarefa que será executada assincronamente. Como exemplo vamos imaginar um contador de 0 até 109.

package lemaf;

import java.util.concurrent.*;

public class Contador implements Callable<Integer> {

    @Override public Integer call() {
        int i;

        for (i=0; i<1e9; i++) {

        }

        return i;
    }
} 

Para simplificar o exemplo usarei a própria classe Contador como AppClass.

Agora como fazer essa árdua tarefa ser executada de forma assíncrona?

Aqui entra o ExecutorService, uma implementação dessa interface é responsável por executar em outra thread esse contador e fornecer um meio de obtermos o resultado.

package lemaf;

import java.util.concurrent.*;

public class Contador implements Callable<Integer> {

    @Override public Integer call() {
        int i;

        for (i=0; i<1e9; i++) {

        }

        return i;
    }

    public static void main(String a[]) {
        Contador contador = new Contador();

        ExecutorService executorService = Executors.newFixedThreadPool(2);

        Future<Integer> future = executorService.submit(contador);

        while (!future.isDone()) {
            System.out.println("Estou esperando...");
            try {
                Thread.sleep(250);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
                break;
            }
        }

        try {
            System.out.println("O resultado do contador é " + future.get());
        } catch (InterruptedException | ExecutionException exception) {
            System.out.println("Ops!");
            exception.printStackTrace();
        }

        System.exit(0);

    }
}

Se você observar o código verá que uso a classe Executors, inclusive descobri essa classe durante a pesquisa para esse artigo e recomendo a leitura do JavaDoc, essa classe é usada como static factory, através dela obtenho uma implementação com um pool de tamanho 2. Após criar uma instância dessa interface é utilizado o método submit, esse método é usado para enviar para o ExecutorService o meu contador e como retorno temos um objeto do tipo Future e esse objeto é o meio pelo qual podemos saber se essa tarefa foi ou não executada. Deixo como exercício a leitura e compreensão do código.

Os caixas rápidos e o Play

Agora que já sabemos como executar uma tarefa assincronamente em Java como fazer para que o Play separe as filas, ou seja, libere recursos para possamos executar os nossos códigos onerosos e libere os caixas rápidos para as tarefas curtas?

Para isso faremos uso de um método muito interessante que o Play disponibilizado na classe abstrata Controller, esse método é o await, recomendo a leitura o JavaDoc esse método oferece algumas sobrecargas bem úteis. Para que esse artigo não fique muito extenso faremos uso o da sobrecarga que exige apenas um objeto do tipo Future.

package controllers;

import play.mvc.Controller;  
import java.util.concurrent.*;

public class Mercado extends Controller {

    private static final    ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static byte caixa_rapido(byte itens) {

        for (byte i=0; i<itens; i++) {
            // Faça algo
        }

        return  itens;
    }

    public static Long caixa_lento(Long itens) {

        Long resultado = await(executorService.submit(new Contador(itens)));

        return itens;
    }

}

Imagino que você percebeu que em nenhum momento no código é feito algum loop ou alguma checagem do objeto Future retornado pelo ExecutorService. Aliás, uma leitura imediata do código dá a sensação de que o código é síncrono. Aqui entra o interessante desse método, quando o usamos é disparado um objeto do tipo Suspend, não se preocupe você não precisa fazer nada, quando isso ocorre digamos que a pilha de execução da thread atual é "congelada" e o Play monitora para nós o Future retornado pelo ExecutorService, quando a tarefa é executada a pilha de execução é "descongelada" e o processamento da requisição continua do ponto onde parou e o resultado que o objeto Future retornou é atribuído na variável resultado do nosso exemplo acima.

Nesse caso o que fizemos foi criar uma Action que trata os casos onde a execução pode ser muito demorada e liberamos a thread do pool do Play para atender outras requisições.

Importante: O Play quando encontra uma chamada do método await manipula o bytecode gerado para que seja possível esse "congelamento", essa é a parte que desconheço os detalhes.

Então por padrão o Play prefere caixas rápidos?

Sim e não. Sim porque ele é projetado para realizar operações rápidas. Não porque oferece um bom suporte para que possamos desenvolver tarefas assíncronas de forma bem simples.

Conclusão

Com esse recurso você pode usar um pool de threads seu e delegar para ele a execução de tarefas mais complexas, é importante lembrar que isso apenas evita a sensação de travamento de sua aplicação, pois as atividades onerosas continuarão onerosas, você apenas está liberando os caixas rápidos para atender as requisições leves e com isso evitar que encontros de famílias sejam desmarcados, que seus clientes habituais desistam de você e que algumas receitas exóticas fiquem com sensação de "faltou alguma coisa".

E o mais importante, que em 24 de dezembro do próximo ano você mantenha os clientes e que consiga alguns novos!

Críticas são bem vindas!

Eventuais erros ou falta de rigor técnico aqui nesse artigo podem e devem ser comentados caso você sinta que ocorreram. Agradeço a leitura e espero você para um próximo texto!