Grails de alta performance - parte 2

Este post faz parte de uma série que estou escrevendo sobre técnicas para fazer uma aplicação grails de alta performance. Estou usando uma aplicação de exemplo, para demonstrar estas técnicas. Esta aplicação se chama GPerform, e está disponível no GitHub, e é uma espécie de Twitter + Instagram (em menor escala, naturalmente), onde usuários enviam posts de texto curto + foto.

Na parte 1 desta série, eu mostrei a primeira prática para se desenvolver uma aplicação grails de alta performance: Não usar Collections para as relações hasMany.

Neste post vou mostrar os plugins que usei.

1) Spring-events plugin:

Este plugin dá a possibilidade de publicar eventos via Spring, de forma assíncrona.

Para instalar o plugin, basta executar "grails install-plugin spring-events".

Eu usei este plugin para lançar um evento indicando que alguém enviou um novo post no sistema, e que, portanto, o sistema precisa processar a foto (criando uma foto com tamanho (kb) menor, e outras fotos thumbnail).

Mas, para não deixar o usuário esperando enquanto o sistema processa as fotos, e assim dar uma impressão de muita velocidade de resposta para o usuário, eu quis fazer este processamento de forma assíncrona, dando uma resposta para o usuário quase que imediata após o envio do seu post.

A action que recebe o envio do post do usuário, com texto + foto, apenas grava a foto num diretório, salva a entidade Foto no banco de dados, e lança este evento assíncrono no Spring, retornando a resposta imediatamente para o usuário (sem processar a foto neste momento). Veja:

    def compartilhar = {
        
        String msgErro = ""
        
        if(!session.usuario){
            redirect(controller:'home')
            return
        }
        
        // Pega a foto do formulario e salva no diretorio para ser processada
        MultipartFile fotoOriginal = request.getFile('fotoOriginal')
        String extensao = identificaExtensao(fotoOriginal)
        
        String nomeArquivo = RandomStringUtils.random(10).encodeAsSHA256() + extensao
        
        try {
            File destino = new File(ConfigurationHolder.config.gp.fotos.originais.folder + File.separator + nomeArquivo)
            fotoOriginal.transferTo(destino)
            
            // Salva uma isntancia do objeto de metadados Foto
            Foto foto = new Foto(params)
            foto.autor = Usuario.get(session.usuario.id)
            foto.nomeArquivo = nomeArquivo
            if(foto.save(flush:true)){
                
                // metodo adicionado pelo spring-events Plugin
                publishEvent(new FotoSubidaEvent(new Expando(id:foto.id)))
                flash.message = "Foto enviada com sucesso. Aguarde processamento."
                redirect(action:index)
            }
            else{
                if(destino.exists()) destino.delete()
                msgErro = "Erro ao salvar os dados da foto."
                render(view:'index', model:[msgErro:msgErro, foto:foto])
            }
        } catch (IOException ioe) {
            msgErro = "Erro ao fazer upload do arquivo."
        } catch (IllegalStateException ie) {
            msgErro = "Erro ao fazer upload do arquivo."
        }
    }

Veja que o método importante aqui é o "publishEvent()", que publica o evento FotoSubidaEvent (eu sei que o nome ficou feio, mas ...).  Pronto, o evento foi publicado.

Agora, precisamos criar uma classe que vai ser o Listener deste evento. A maneira mais fácil que encontrei foi criar uma classe de serviço que implementa "ApplicationListener", ou seja, ela será um Listener para os eventos do tipo FotoSubidaEvent. Ao implemetar esta interface, sua classe de Service terá que ter um método "void onApplicationEvent(FotoSubidaEvent event)". Este método será chamado pelo Spring automaticamente quando um evento do tipo FotoSubidaEvent for lançado, de forma assíncrona.

2) Springcache plugin:

Este plugin faz uso do projeto Spring Cache,  e permite que se faça:
- Cache de métodos dos beans do Spring; por exemplo, para fazer cache do retorno de métodos das classes de serviços. 
- Fragmentos das páginas, ou páginas inteiras, geradas pelos controllers; por exemplo, para fazer o cache de uma determinada action de um controller; 

No caso da aplicação de exemplo, GPerform, usei o Springcache para fazer cache da action "HomeController.recentes()" , ou seja, eu coloquei em cache os posts de fotos mais recentes. Assim, não é preciso acessar o banco de dados o tempo todo para pegar os posts mais recentes, pois eles estão na memória do cache.

Para usar este plugin, basta instalar usando o comando "grails install-plugin springcache". Em seguida, coloquei uma anotação na action do meu controller (na action que quero que a resposta fique em cache):

    @Cacheable("recentesCache")
    def recentes = {
        log.info "Executando action 'recentes' e por isso nao ta usando o cache agora."
        def recentes = buscaFotosMaisRecentes(20)
        [fotosMaisRecentes:recentes]
    }

Veja que eu coloquei um nome neste cache ("recentesCache"), pois eu posso criar vários caches distintos, e controlar cada um da forma que eu quiser. Esta anotação @Cacheable("recentesCache") é que indica para o plugin que o retorno desta action precisa ser guardado em memória no cache. Só isso.

Agora o problema passa a ser como renovar o cache quando um novo post for enviado por um usuário do site. Para isso, basta limpar o cache (matar o cache) quando um novo post for inserido. No meu caso, o que fiz foi o seguinte: quando um usuário envia um novo post (que é um texto + foto), o controller recebe esta requisição, faz algumas verificações, salva a foto num diretório para ser processada posteriormente (de forma assíncrona -- falo sobre isso mais tarde), e grava os dados da entidade Foto no banco de dados (que são os metadados sobre a foto e o texto do post do usuário). Quando o sistema for processar a foto de forma assíncrona, aí sim a foto está pronta para ser mostrada no site, e é neste momento que eu preciso limpar o cache. O método que processa a foto e limpa o cache é um método de uma classe de Serviço (FotoService). Veja abaixo:


    @CacheFlush("recentesCache")
    void onApplicationEvent(FotoSubidaEvent event) {
        if(log.isInfoEnabled()) log.info "Executando evento FotoSubidaEvent..."
        
        Foto foto = Foto.get(event.source.id)
        if(foto){
            // processa a imagem da foto
        }
        
    }

Assim que este método for executado pela aplicação, o cache será limpado (jogado fora), pois este método tem a anotação @CacheFlush("recentesCache"). Veja que usei o mesmo nome "recentesCache" para indicar qual cache quero limpar.

Perceba também que este é aquele método "onApplicationEvent()", citado no item 1 acima, usado pelo Spring quando o framework detecta o lançamento do evento do tipo FotoSubidaEvent.

Pronto. Da próxima vez que a action HomeController.recentes() for executada, o Springcache vai ver que o "recentesCache" não existe mais, aí vai executar a action, pegar seu resultado e colocar em cache novamente. E enquanto este cache existir, a action não é executada, e ao invés disso, o conteúdo do cache é enviado para o usuário.

Um pequeno detalhe deste plugin é que ele tem uma configuração padrão que indica que o cache deve existir por apenas 120 segundos. Como era muito pouco para o meu caso, então configurei para que o cache vivesse por 3600 segundos. Veja como:

springcache {
    defaults {
        eternal = false
        diskPersistent = false
        timeToLive= 86400
    }
    caches {
        recentesCache {
            timeToLive= 3600
        }
    }
}



Ficamos por aqui com este artigo que já tá longo. Ainda falta coisa pra falar, como resources plugin, cache-headers plugin, cached-resources plugin e zipped-resources plugin. Até a próxima.

Lembre-se que você é bem vindo para contribuir com esta aplicação de exemplo GPerform via o GitHub. Abcs a todos.

GPerform: uma aplicação de exemplo com PERFORMANCE na alma

Venho pensando num tema legal para escrever um post. Aí resolvi dar uma atualizada, ler um pouco, ver umas apresentações, ..... E uma coisa que tem me interessado muito ultimamente é o assunto performance. Como uso muito Grails, então o assunto seria Grails e Performance. Então vamos lá.

Depois de algumas leituras, acho que juntei diversas dicas, plugins, e técnicas que vão fazer uma aplicação grails voar em alta velocidade. Mas não queria apenas ficar na leitura, pois sabemos bem que a teoria é uma coisa, na prática.....

Então botei a mão na massa e resolvi criar uma nova aplicação que chamei de GPerform (Grails Performance). Criei uma aplicação (na verdade está sendo criada ainda) que é uma espécie de Twitter + Instagram (em menor escala, naturalmente). Você manda uma foto, com comentário. Você pode seguir pessoas, e pessoas podem te seguir. Você pode avaliar fotos como boa ou ruim. Tudo é público.

A aplicação é 0.1, ok? Mas está disponível no GitHub. Seja bem vindo para contribuir se quiser (estou dando meus primeiros passos com o Git, então paciência comigo....).

A primeira coisa a fazer foi criar o modelo de dados, e aí já começam as dicas para uma melhor performance da sua aplicação: Não usar Collections para as relações hasMany. E mais que isso: acabar com as relações diretas entre duas classes para os casos many-to-many e one-to-many. Pois é, parece estranho.  Mas tem fundamento.

Para esclarecer o fundamento, vou dar um pequeno exemplo. Imagina:

class ContaCorrente{
    
    String numero
    String agencia
    
    static hasMany = [transacoes: Transacao]
}


class Transacao{
    
    BigDecimal valor
    
    static belongsTo = [conta: ContaCorrente]
}


Imagine que você queira usar as facildades que o Grails dá para gerenciar esta relação one-to-many. Ou seja, você vai querer usar o método conta.addToTransacoes(t) que está disponível para você, claro.

Mas imagine quantas transações tem numa conta corrente al longo de um ano. Se você tiver 30 transações por mês, no ano serão 360. Ou seja, no final do ano, quando o seu sistema for adicionar mais uma transação na conta do usuário, ele irá buscar as 360 transações para montar a collection "conta.transacoes" e só depois irá adicionar a nova transação nesta collection, para aí gravar esta alteração na collection no banco de dados. Se inicialmente você queria fazer apenas um insert, você acaba fazendo um select que retorna 360 itens pra memória (que você não vai usar nenhum deles), um insert da nova transação, e um update da ContaCorrente (pois o GORM atualiza o campo version deste objeto pai).

É ou não é coisa demais, e é muito provável que a performance da aplicação será seriamente afetada?

Para resolver isso, Burt Beckwith sugere, em uma apresentação na infoQ, que não se use a relação one-to-many com o hasMany. Que se faça apenas o outro lado da relação, que é o many-to-one. Qual é a perda disso? Você não vai ter o método addToTransacoes(). Mas isso é fácil de resolver. E também não vai ter um atributo "transacoes" que te retorna todas as transacoes da sua conta (você provavelmente não iria usar nunca - nem deveria). (veja um post interessante sobre isso) Por exemplo:

class ContaCorrente{    

    String numero
    String agencia
    
    def buscaTransacoesRecentes(int qtde){
        Transacao.executeQuery('from Transacao t where t.conta = :c order by t.dateCreated desc', [c:this], [max:qtde])
    }

    def adicionaTransacao(BigDecimal valor){
        new Transacao(valor:valor, conta:this).save()
    }

}


class Transacao{
    
    BigDecimal valor
    Date dateCreated

    ContaCorrente conta

}

Pronto. Simples assim. Você agora conseguiu ter apenas o seu único insert que você queria. Sem selects desnecessários, e sem update desnecessário (não estou contando com o select da ContaCorrente, mas tbem não contei com ele ali em cima). Veja que eu criei métodos na classe ContaCorrente para adicionar uma nova transação e para buscar as mais recentes.

Acho que tá bom por hoje né? Terei que escrever outros posts para contar tudo. Tem ainda o uso de diversos plugins, como por exemplo: springcache, spring-events, resources, cache-headers, cached-resources, zipped-resources, e por aí vai. Já já eu volto.