Políticas de Rejeição em ThreadPoolExecutor: Casos Práticos em Java

O ThreadPoolExecutor do Java gerencia tarefas assíncronas através de um conjunto de threads reutilizáveis. Quando todos os núcleos estão ocupados e a fila de espera atinge sua capacidade, novas tarefas são rejeitadas. Para tratar essas rejeições, o executor utiliza a interface RejectedExecutionHandler.

Interface RejectedExecutionHandler

A interface possui um único método:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable tarefa, ThreadPoolExecutor executor);
}

Esse método é invocado quando o executor não consegue aceitar uma nova tarefa. Caso nenhum handler seja informado no construtor, o ThreadPoolExecutor adota o AbortPolicy por padrão.

Políticas nativas do JDK

O JDK oferece quatro implementações prontas:

  1. AbortPolicy: lança RejectedExecutionException, delegando o tratamento ao chamador.
  2. CallerRunsPolicy: executa a tarefa rejeitada na própria thread que invocou execute.
  3. DiscardOldestPolicy: descarta a tarefa mais antiga da fila e tenta reenviar a nova.
  4. DiscardPolicy: descarta a tarefa sem notificar o chamador.

Cenário prático: AbortPolicy

Em um sistema de notificações por push após pagamento, o seguinte padrão foi aplicado:

ThreadPoolExecutor workerPool = new ThreadPoolExecutor(...);

try {
    workerPool.execute(() -> notificationService.enviarPush(pedido));
} catch (RejectedExecutionException ex) {
    logger.warn("Push rejeitado: {}", pedido.getId(), ex);
}

Como o AbortPolicy é o padrão, a exceção precisa ser capturada para registro ou outra ação compensatória.

Cenário prático: handler personalizado

Para atender a uma regra de retry — duas tentativas com intervalo de 2 segundos entre elas —, uma opção é delegar a lógica de compensação para um handler customizado que persiste a tarefa em uma fila externa, como o Redis:

ThreadPoolExecutor workerPool = new ThreadPoolExecutor(
    ...,
    new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable tarefa, ThreadPoolExecutor executor) {
            logger.warn("Tarefa rejeitada, enviando para reprocessamento");
            redisTemplate.opsForList().leftPush("fila:push", Serializador.toJson(tarefa));
        }
    }
);

Em seguida, um agendador recupera os itens da fila e os reenvia ao executor. Essa abordagem separa a lógica de execução da lógica de recuperação, respeitando o padrão Strategy.

Cenário prático: CallerRunsPolicy

Em uma plataforma de entrega de payloads HTTP, o processo segue um modelo produtor-consumidor: uma thread periódica busca tarefas pendentes no banco e as entrega ao pool. Quando as chamadas downstream são lentas, o pool fica saturado. Configurar o CallerRunsPolicy faz com que a própria thread de polling execute a tarefa recusada, reduzindo a pressão sobre o banco de dados e evitando que novas buscas sejam realizadas enquanto o sistema está sobrecarregado.

ThreadPoolExecutor dispatcher = new ThreadPoolExecutor(
    4,
    8,
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(128),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

Com essa configuração, a thread que coleta as tarefas assume temporariamente o trabalho, agindo como uma válvula de pressão natural para o consumidor.

Tags: ThreadPoolExecutor RejectedExecutionHandler AbortPolicy CallerRunsPolicy Redis

Publicado em 7-11 07:31