Alguma vez já usou a execução de jobs do Spring? Realmente é muito simples. Simplesmente funciona sem muita parafernália. Porém, recentemente topamos com um problema num cenário onde não desejamos a execução. Mas vamos por partes. Primeiro, de forma rápida, vamos ver como configurar o seu projeto Spring-boot para permitir a execução de jobs, e depois planteamos e resolvemos o problema.

Com sempre, pode ir no meu github e fazer um clone do projeto:

> git clone https://github.com/iundarigun/schedule-problem.git
> cd schedule-problem/job

Basicamente, temos um projeto web, que tem uma classe de serviço. O detalhe é que é um projeto modularizado. Se você não leu o artigo sobre modularização, da uma olhada no post Criando o projeto. Esse detalhe ainda não é importante, mas vamos revê-lo em breve.

Habilitando os jobs

Para habilitar e rodar os jobs, só precisamos fazer duas ações. Primeiro anotamos o projeto para habilitar o Scheduling.

@SpringBootApplication
@EnableScheduling
@ComponentScan(basePackages = "br.com.devcave.scheduleproblem")
public class JobApplication {

   public static void main(String[] args) {
      SpringApplication.run(JobApplication.class, args);
   }
}

A segunda, é anotar os métodos necessários com @Scheduled e especificar a cadência de execução:

@Service
@Log
public class AnythingService {

    @Scheduled(cron="0 0/1 * 1/1 * ?")
    public void validateSomething(){
        log.info("Executamos o metodo de validacao");
    }
}

Nota: para criar os cron da vida, pode usar o site http://www.cronmaker.com/. Também pode usar outras opções para indicar a cadência, mas elas implicam uma execução baseadas na hora de inicio da aplicação.

Pronto, no exemplo então a aplicação é executada uma vez por minuto.

Descrevendo o problema

Até aqui todo certo. Vamos agora no problema. Criamos também um projeto de web service Rest. E ele reusa o módulo common. Isso é feito assim para evitar a repetição de código em projetos que tem domino parecido. Por exemplo, o projeto jobs executa os agendamento e validações periódicas (execução e envio de relatórios, sincronização com outras aplicações, etc) e o projeto ws permite acesso aos dados da aplicação via API. Não queremos duplicar as entidades, repositórios e classes de serviços, então eles ficam no common.

No projeto atual (no mesmo git, pasta ws), criamos um endpoint para executar manualmente os jobs. No caso, a classe AnythingService é injetada no controller e executada quando recebe um post:

@RestController
@Log
public class AnythingController {

    @Autowired
    private AnythingService anythingService;

    @RequestMapping(value = "/validate-something", method = RequestMethod.POST)
    public void validateSomething(){
        log.info("validamos por ws");
        anythingService.validateSomething();
    }
}

E como não queremos duplicar a execução do job, nosso application não tem a anotação @EnableScheduling. Então, vamos iniciar a aplicação e nos deparamos com o seguinte:

wslog

Se reparamos, temos uma chamada feita pelo ws (thread nio-8080-exec-9), mas tanto antes como depois o jobs executou normalmente. E justamente isso é o que queremos evitar, pois teremos concorrência de acesso.

Solucionando o enigma

E claro que podemos usar estratégias várias para modificar esse comportamento. Por exemplo, usar o quartz, ou usar um elemento no properties para indicar se deve ou não deve executar, e muitas outras criativas para evitar o problema. E provavelmente seja uma boa ideia pensar nisso por se a sua aplicação precisa escalar instancias. Mas no caso que estamos vendo, queremos entender como solucionar isso configurando corretamente spring, pois no final das contas é ele quem gerencia isso.

Como sempre, depois de quebrar a cabeça um tempo, decidimos consultar o Lenda (esperemos que algum dia poste aqui!). Identificou que o problema está no actuator de Spring. Basicamente, o actuator fornece uma série de informações da sua aplicação que podem ser usada para várias coisas (por exemplo escalar ou verificar a saúde da aplicação). Infelizmente, também habilita os jobs.

Para evitar isso, podemos simplesmente colocar a seguinte propriedade no projeto:

spring.metrics.export.enabled=false

E pronto, os jobs não são ligados por livre espontânea obrigação. Podemos ver isso na documentação de spring:

metrics

Bom, é isso. Esperemos ter ajudado alguém com isso. Qualquer coisa, da um comentário ai!

Comentários