Blog

spring boot + embedded tomcat で async request を graceful shutdown する

graceful shutdown 厨みたいになってますが、まあいいとして。。 embedded tomcat はちょっと見た感じ async request を綺麗に graceful shutdown する方法が見当たらなかった。

結局、各コントローラなりサーブレットなりで個々に graceful shutdown するのが良さそう。 例えば以下のように。

package com.example;

import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import javax.annotation.PreDestroy;
import java.util.concurrent.*;

@SpringBootApplication
@Slf4j
public class SpringBootTomcatApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTomcatApplication.class, args);
    }

    @RestController
    public static class MyController {
        private final ExecutorService pool = Executors.newFixedThreadPool(10);

        @GetMapping("/")
        public String ok() {
            return "OK";
        }

        @GetMapping("/sleep")
        public DeferredResult<String> sleep() {
            DeferredResult<String> objectDeferredResult = new DeferredResult<>();
            pool.submit(() -> {
                try {
                    log.info("Sleeping");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                log.info("Send result");
                objectDeferredResult.setResult("OK");
            });
            return objectDeferredResult;
        }

        /**
         * Graceful shutdown.
         */
        @PreDestroy
        public void stop() {
            log.info("STOP");
            pool.shutdown();
            try {
                if (!pool.awaitTermination(7, TimeUnit.SECONDS)) {
                    log.info("shutdown failed");
                    pool.shutdownNow();
                } else {
                    log.info("shutdown succeeded");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

jetty や undertow なら全体で処理できるからそれでもいいけど、まあ基本的には各コントローラでやるほうが綺麗かも。 あるいは処理が okhttp 等の場合だと、各 Service なりなんなりで @PreDestroy でやるのがいいかも。

【追記】

ThreadPoolTaskExecutor 使えばいいと @making さんと @kazuki43zoo さんから聞いたので、そうしてみたらすっきりしました。 (やってることは内部的には同じ)

package com.example;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@Slf4j
public class SpringBootTomcatApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTomcatApplication.class, args);
    }

    @Bean(name = "async1")
    public AsyncTaskExecutor mvcAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(10);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(7);
        return executor;
    }

    @RestController
    public static class MyController {
        @Autowired
        @Qualifier("async1")
        AsyncTaskExecutor asyncTaskExecutor;

        @GetMapping("/")
        public String ok() {
            return "OK";
        }

        @GetMapping("/sleep")
        public DeferredResult<String> sleep() {
            DeferredResult<String> objectDeferredResult = new DeferredResult<>();
            asyncTaskExecutor.submit(() -> {
                try {
                    log.info("Sleeping");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                log.info("Send result");
                objectDeferredResult.setResult("OK");
            });
            return objectDeferredResult;
        }
    }
}