DEV Community

LeonardoMarques
LeonardoMarques

Posted on

[Java] Solucionando concorrência em schedules

Vivi esse problema em ambiente de trabalho e precisei estudar um pouco mais a fundo para conseguir solucioná-lo. E claro, de forma colaborativa com meus colegas, principalmente o Rafa que foi essencial na solução que vou apresentar. Um salve pro Caio também que nos ajudou a seguir pelo caminho dos schedules, vocês já vão entender.

Tudo começou com um projeto de Jobs usando Spring Batch do Java. Todos os jobs eram iniciados por schedules. Inicialmente era um job apenas, então rodava sozinho de tempos em tempos, digamos assim. O problema é que a medida que foram surgindo mais jobs, começamos a perceber a degradação da execução. E aqui entra nossa maior dor: um desses jobs iniciava um fluxo de tarifação que concorria com outros produtos da empresa para tarifar. Se a gente demorasse muito, digamos que perdíamos nossa chance de tarifar o cliente. E adivinha o que estava acontecendo? Esses jobs estavam concorrendo e executando de forma síncrona travando a thread principal e impedindo outros jobs de executarem. Uma espécie de dança das cadeiras se for mais fácil de visualizar.

Inicialmente achávamos que o problema era com os jobs e focamos esforços em resolver isso, até encontramos formas de paralelizar o fluxo "do meio" de um job. A título de curiosidade: a estrutura básica de um job geralmente tem um reader, um processor e um writer. Dependendo do seu problema você consegue paralelizar o processor e/ou o writer usando Future do Java.
Se quiser aprofundar, dá uma olhada nesse vídeo:
https://www.youtube.com/watch?v=AbQcWO91Bx4

Mas como eu disse, nosso problema não era nos jobs em si e descobrimos isso adicionando logs assim que o schedule executava, antes de acionar o job. E nosso colega Caio fez muitos questionamentos sobre essa parte, dessa forma decidimos investigar e estudar mais sobre schedules para conseguir responder os seus questionamentos com confiança. A pista de que o problema estava ai foi que nosso log não estava acontecendo, ou seja, o schedule sequer estava sendo executado, mesmo com seu tempo bem baixo (5s).

O ChatGPT nos ajudou muito nesse aprofundamento e aos poucos, conseguimos entender exatamente o que estava acontecendo. E não só teoricamente, mas o Rafael fez uma PoC (Prova de Conceito) para validar nossa teoria que era: estávamos lidando com um problema de concorrência e a única forma de ter os schedules executando de forma independente era criando um ThreadPool dedicado para cada um deles.

Essa prova de conceito se tornou um case de projeto e pode ser encontrado nesse link: https://github.com/StringRafa/SchedulerThreadPool
Mas resumidamente, conforme a documentação do repositório:

A solução utiliza executores dedicados para cada scheduler, configurados por meio de:

  • @Async com Executors Dedicados: Cada agendador foi configurado para usar um executor específico, garantindo que as tarefas sejam processadas de forma independente.
  • Isolamento de Tarefas: Cada tarefa tem seu próprio executor, evitando interferências entre tarefas que possuem tempos de execução distintos.

Vejamos alguns trechos de códigos que refletem a solução:

Primeiro definimos um executor dedicado através de uma classe de configuração:

@Configuration
public class TaskExecutorConfig {

    @Bean(name = "threadPool-task1")
    public Executor task1() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(1);
        executor.setQueueCapacity(5);
        executor.setThreadNamePrefix("threadPool-task1-");
        executor.initialize();
        return executor;
    }

}
Enter fullscreen mode Exit fullscreen mode

Agora vincule esse taskExecutor dedicado ao seu método agendado:

@Component
public class ScheduledTasks {

    @Async("threadPool-task1")
    @Scheduled(fixedRate = 5000)
    public void executeTask1() {
        //Sua lógica de negócio
    }
}
Enter fullscreen mode Exit fullscreen mode

Observações:

  • Se você define um @Scheduled sem definir nada, por default ele executará na thread principal da sua aplicação. Se existir outros schedules independente da configuração, eles ficarão aguardando a execução desse schedule, pois ele está ocupando a thread principal, então você tem um problema de concorrência.
  • Se você definir um @Scheduled com @Async sem definir um threadExecutor/threadPool específico, ele executará em um executor chamado SingleAsyncThreadPool, nesse caso ele executará uma tarefa por vez de forma sequêncial, mas ainda sofrerá bloqueio de algum scheduled que estiver usando a thread principal, como no primeiro caso. Esse tipo de configuração é ideal para tarefas sequenciais simples que não precisam de ajustes ou gerenciamento avançado.

Não sei se existem outras formas de resolver esse problema, caso saibam podem deixar nos comentários sua contribuição. Mas na situação que vivi, a única forma que encontramos de isolar cada agendador de forma mutuamente exclusiva, ou seja, sem que a execução deles não impactassem uns aos outros e mesmo assim fossem altamente configurações e flexíveis, foi usando um ThreadPoolTaskExecutor como exemplificado anteriormente.

Bom, o artigo de hoje era esse, espero que essa solução possa ajudar você se um dia passar por esse mesmo problema de concorrência. Estamos há um tempo rodando com essa solução em produção e foi com certeza um sucesso: até os gráficos do NewRelic ficam melhores de ver cada pool de thread executando, sua porcentagem de processamento ao longo do tempo, e até os logs agora possuem a threadpool de execução, facilitando entender como a dinâmica dos agendadores está ocorrendo.

Obrigado e até a próxima!
Um salve e agradecimento a Caio Senna e @rafael_j_souza

Top comments (0)