Завершение работы TaskScheduler не останавливает выполнение метода @Scheduler

У меня есть класс с методом, помеченным @Scheduled

@Component
@Slf4j
public class MyScheduler {

    @Scheduled(cron = "${polling-job-cron}") //each minute
    public void pollingJob() {
        log.info("starting polling job...");
        //some work
        log.info("polling job finished.");
    }
}

и конфигурация для taskScheduler:

 @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("mynameofscheduler");
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        scheduler.setAwaitTerminationSeconds(30);
        scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        return scheduler;
    }

Я пытаюсь использовать плавное завершение работы, используя класс, ожидающий ContextClosedEvent:

@Component
@Slf4j
public class GracefulShutdown implements ApplicationListener<ContextClosedEvent> {
    private final ApplicationContext context;
    private final ThreadPoolTaskScheduler taskScheduler;

    public GracefulShutdown(ApplicationContext context,
                            ThreadPoolTaskScheduler taskScheduler) {
        this.context = context;
        this.taskScheduler = taskScheduler;
    }

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        log.info("Graceful shutdown - start");
        log.info("Closing task scheduler");
        taskScheduler.shutdown(); //1
        taskScheduler.getScheduledThreadPoolExecutor().shutdown(); //2
        log.error("Closed task scheduler");
        //give k8s a chance to hit in readinessProbe and stop sending requests to this pod
        try {
            Thread.sleep(80000); //3
        } catch (InterruptedException error) {
            log.info("error while trying to sleep");
            error.printStackTrace();
        }
        log.info("Closing spring context with startup date, {}, parent: {}, id: {}, name: {}",
            context.getStartupDate(), context.getParent(), context.getId(), context.getDisplayName());
        ((ConfigurableApplicationContext) context).close();
        log.info("Graceful shutdown - end");
    }

и даже несмотря на то, что я закрываю taskScheduler и базовый taskExecutor, новые задачи по-прежнему выполняются @Scheduled. Код GracefulShutdown запускается при отправке SIGTERM, а кроме закрытия taskScheduler он работает нормально.

Graceful shutdown - start
Closing task scheduler
Closed task scheduler
starting polling job...
polling job finished
starting polling job...
polling job finished.

threadPoolPrefix регистрируется перед этими строками (я вырезал это выше, так как строка была слишком длинной для чтения):

{"timeMillis":1534234560001,"thread":"mynameofscheduler","level":"INFO","loggerName":"myclassr","message":"starting polling job..."

Я подумал, что, возможно, используется какой-то другой taskScheduler, и я закрываю не тот, но все это mynameofscheduler, которое настроено в @Bean


person freakman    schedule 14.08.2018    source источник
comment
Spring уже вызывает метод shutdown, поэтому повторять это не нужно. Ваш код также приведет к ошибке, поскольку контекст уже закрыт, и вы снова вызываете close. AFAIK, который вызовет уже закрытое исключение (или что-то подобное). Поэтому я бы сказал, что ваша попытка корректного выключения на самом деле препятствует корректному завершению работы.   -  person M. Deinum    schedule 14.08.2018
comment
К сожалению, я не стал закрывать taskScheduler без причины. Я заметил, что он все еще запускает метод @Scheduled, пока Thread.sleep (80000) (что является грязным трюком для кубернетов). Так что нет - он не выключает его, по крайней мере, не так, как я ожидал :) + Я не получаю никаких ошибок, о которых вы упомянули.   -  person freakman    schedule 14.08.2018
comment
Он завершает работу, потому что ThreadPoolTaskScheduler реализует DisposableBean, а метод destroy вызывается, когда контекст закрывается / уничтожается. Так что да, это выключение, но выключение не мешает ему принимать другие задачи (это то, что вы видите в своем журнале), повторное выключение (и снова) не изменит этого. Также вы уверены, что это TaskScheduler, который используется вашим @Scheduled?   -  person M. Deinum    schedule 14.08.2018
comment
да, это TaskScheduler, так как его threadNamePrefix регистрируется в той же строке, что и «начало опроса ..» (я вырезал это, так как строка действительно длинная, но я отредактирую / добавлю ее в вопрос)   -  person freakman    schedule 14.08.2018
comment
Тогда странно, что он еще не выключился (и, видимо, ваше исправление тоже не помогает). Есть ли (по какой-то причине) несколько экземпляров? Не могли бы вы создать MCVE для его воспроизведения?   -  person M. Deinum    schedule 14.08.2018
comment
Просто сделал небольшой тест, у меня действительно работает. Однако ContextClosedEvent запускается до того, как фактические beans будут остановлены, поэтому, если вы спите внутри этого ApplicationListener, это фактически предотвращает остановку ApplicationContext и, таким образом, выключение TaskScheduler. Выключение TaskScheduler вручную фактически прекращает планирование событий в моем небольшом тесте.   -  person M. Deinum    schedule 14.08.2018
comment
ОК будет копаться, исполнительный механизм / выключение делают аналогичный сон в org.springframework.boot.actuate.context.ShutdownEndpoint, но, возможно, мне придется сделать это в новом потоке явно   -  person freakman    schedule 14.08.2018
comment
Это не похоже. Поскольку это спит перед закрытием контекста. Вы спите, пока закрывается контекст. Это похоже, но это не так. sleep в ShutdownEndpoint не останавливает правильное закрытие контекста, потому что это еще не началось. Ваш блокирует дальнейшую обработку doClose в AbstractApplicationContext, предотвращая завершение работы beans.   -  person M. Deinum    schedule 14.08.2018


Ответы (2)


спасибо M. Деинум. Я испортил пружину, перекрывшую поток. Я исправил это, зарегистрировав хук выключения:

 public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(AgileStreamApplication.class, args);
    Runtime.getRuntime().addShutdownHook(new Thread(new GracefulShutdownHook(context)));
}

и теперь мне не нужно явно закрывать taskSchedulers. Это делается к весне.

person freakman    schedule 16.08.2018

Потому что по умолчанию ScheduledThreadPoolExecutor будет ждать завершения выполнения всех отложенных запланированных задач, даже если запланированные задачи в это время не выполняются.

Попробуйте это ниже:

@Bean
public ThreadPoolTaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler() {
        private static final long serialVersionUID = -1L;
        @Override
        public void destroy() {
            this.getScheduledThreadPoolExecutor().setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
            super.destroy();
        }
    };
    scheduler.setPoolSize(5);
    scheduler.setThreadNamePrefix("mynameofscheduler");
    scheduler.setWaitForTasksToCompleteOnShutdown(true);
    scheduler.setAwaitTerminationSeconds(30);
    scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    return scheduler;
}

Тогда ScheduledThreadPoolExecutor будет ждать только запланированных задач, которые в настоящее время выполняются, чтобы завершить выполнение.

person hkw    schedule 14.12.2018