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.

2 comentários:

Matheus Moreira 16 de setembro de 2011 09:50

Felipe, bom dia. Qual versão do Grails você usa no projeto? Tô gostando bastante de seus posts sobre desempenho.

[]'s

Felipe Nascimento 19 de setembro de 2011 11:56

Olá Matheus

no momento o projeto está com a versão 1.3.7
Obrigado pela mensagem. Vou tentar continuar os posts. Abcao