graceful shutdown시 인터럽트되는 스레드 관리

hongo·2025년 4월 17일
0

tomcat패키지의 GracefulShutdown 클래스 일부

    private void doShutdown(GracefulShutdownCallback callback) {
        List<Connector> connectors = this.getConnectors();
        connectors.forEach(this::close);

        try {
            Container[] var3 = this.tomcat.getEngine().findChildren();
            int var4 = var3.length;

            for (int var5 = 0; var5 < var4; ++var5) {
                Container host = var3[var5];
                Container[] var7 = host.findChildren();
                int var8 = var7.length;

                for (int var9 = 0; var9 < var8; ++var9) {
                    Container context = var7[var9];

                    while (!this.aborted && this.isActive(context)) { // 해당 컨테이너가 비동기 작업을 실행중이거나, 사용중인 자원이 있다면 isActive는 true
                        Thread.sleep(50L);
                    }

                    if (this.aborted) { // 프로퍼티 파일에서 설정한 타임아웃이 지났을 경우 abort된다.
                        logger.info("Graceful shutdown aborted with one or more requests still active");
                        callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
                        return;
                    }
                }
            }
        } catch (InterruptedException var11) {
            Thread.currentThread().interrupt();
        }

        logger.info("Graceful shutdown complete");
        callback.shutdownComplete(GracefulShutdownResult.IDLE);
    }

타임아웃 시간까지 스레드가 종료되지 않으면 인터럽트된다.
타임아웃 시간안에 종료되지 못한 비동기 스레드(비동기 작업을 실행하는 스레드)들을 안전하게 재처리하기 위해 추가적인 설정이 있으면 좋을 것 같았다.

동기로 동작하는 API요청의 경우 사용자가 에러 응답을 받기 때문에 재시도 요청을 빠르게 할 수 있지만, Event로 발행되는 비동기 스레드에서 인터럽트가 발생할 경우 재시도를 하기가 번거로울 것 같았다. 그래서 인터럽트 될 비동기 스레드들을 자세하게 로깅하는 기능을 추가해보려고 한다. (비동기 스레드가 시작될 때 체크하고, 종료될 때 체크해서 시작만 체크된 비동기 스레드를 확인하는 방법도 있겠지만 셧다운으로 인해 종료될 확률은 극히 드문 케이스일 것 같으니 매번 체크하기보단 인터럽트가 되었을 때만 체크하는 방식이 좋아보였음)

예외를 처리하기위한 기능으로 ControllerAdvice가 있지만, ControllerAdvice의 경우는 Controller = api로 들어온 요청만 캐치하기 때문에 비동기 작업을 수행하는 스레드나, 스케줄링 등으로 실행되는 스레드를 캐치하긴 어렵다.

챗지피티한테 물어봤을 때, Application 클래스에 @PreDestory, @PostDestroy메서드를 만들어서 셧다운 이후에도 실행중인 스레드들에 대해 로깅을 처리하는 것을 추천해줬지만, 테스트 결과 @PreDestory보다 doShutdown()이 먼저 호출되어서 스레드들이 전부 인터럽트된 이후에 @PreDestory가 동작했다.

Applilcation이 셧다운 되는 명령을 받았고, GracefulShutdown의 doShutdown메서드가 호출되기 전에 인터셉트할 수 있는 기능을 찾아봤는데 챗지피티가 ApplicationListener<ContextClosedEvent>를 추천해줬다.

다음과 같이 application의 close()가 호출된 이후에 실행할 메서드를 구현할 수 있다.


@Component
class ShutdownCustom(
    private val checkAtShutdownThreadContext: CheckAtShutdownThreadContext
) : ApplicationListener<ContextClosedEvent> {

    override fun onApplicationEvent(event: ContextClosedEvent) {
        val threadContext: Map<String, MethodArgs> = checkAtShutdownThreadContext.getContext()

        // Map 순회
        for ((threadName, methodArgs) in threadContext) {
            // 로깅 예시
            println("Thread: $threadName")
            println("  Method: ${methodArgs.method.declaringClass.simpleName}.${methodArgs.method.name}()")
            println("  Params: ${methodArgs.args.joinToString()}")
        }
    }
}

나는 셧다운에 의해 강제로 종료될 비동기 스레드들의 메서드명과 파라미터를 알고 싶어 다음과 같이 AOP와 커스텀 어노테이션을 생성했다.

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CheckAtShutdown
@Aspect
@Component
class CheckAtShutdownThreadContext {

    // 스레드 이름 → Method 및 파라미터 저장용 DTO
    private val context: MutableMap<String, MethodArgs> = ConcurrentHashMap()

    // DTO 클래스
    data class MethodArgs(val method: Method, val args: Array<Any?>)

    // @CheckAtShutdown 가 붙은 메서드만 포인트컷
    @Pointcut("@annotation(com.example.trace.CheckAtShutdown)")
    fun checkAtShutdownMethods() {
        // pointcut signature method
    }

    @Around("checkAtShutdownMethods()")
    @Throws(Throwable::class)
    fun aroundCheck(pjp: ProceedingJoinPoint): Any? {
        val threadName = Thread.currentThread().name
        val sig = pjp.signature as MethodSignature
        val method = sig.method
        val args = pjp.args

        // 진입 시점에 메서드 파라미터들 저장
        context[threadName] = MethodArgs(method, args)

        return try {
            pjp.proceed()
        } finally {
            // 완료 시점에 컨텍스트에서 제거
            context.remove(threadName)
        }
    }

    // ContextClosedEvent에서 호출해 사용
    fun getContext(): Map<String, MethodArgs> =
        Collections.unmodifiableMap(context)
}
profile
https://github.com/hgo641

0개의 댓글