Timeout

Timeout (오래 걸리는 블록 자동 종료)

Timeout정해진 시간 안에 끝나지 않으면 자동으로 중단시킬 수 있는 방법을 제공해요. 오래 걸릴 수 있는 연산을 일정 시간 안에 끝내도록 강제하고 싶을 때 써요.

require 'timeout'
status = Timeout.timeout(5) {
  # 5초 이상 걸리면 중단되어야 하는 작업
}

출처: Ruby 4.0 API

본문

상수 (Constants)

  • VERSION - 버전.

클래스 메서드 (Public Class Methods)

timeout(sec, klass = nil, message = nil) { |sec| ... }

블록 안의 연산을 수행하고, sec초 안에 완료되지 않으면 예외를 던져요.

  • sec - 블록이 끝나길 기다리는 초 수. 음수가 아닌 어떤 숫자나 nil도 쓸 수 있고, 분수 초를 위해 Float도 허용돼요. 0이나 nil은 그 블록을 타임아웃 없이 실행해요. 음수는 ArgumentError를 던져요.
  • klass - 블록이 sec초 안에 끝나지 않을 때 던질 Exception 클래스. 생략하면 기본값인 Timeout::Error를 사용해요.
  • message - Exception 클래스와 함께 던질 Error 메시지. 생략하면 기본값인 "execution expired".

블록이 sec초 안에 완료되면 그 블록의 결과를 돌려주고, 그렇지 않으면 klass 값에 따라 예외를 던져요.

블록을 종료하기 위해 던지는 예외는 주어진 klass, 또는 klass가 없으면 Timeout::ExitException이에요. 그 이유는 Timeout::ErrorRuntimeError를 상속받아 rescue로 뜻하지 않게 잡힐 수 있기 때문이에요. 반면 Timeout::ExitExceptionException을 상속받아 rescue Exception으로만 잡혀요. 참고로 Timeout::ExitExceptionTimeout.timeout 호출에 도달하면 Timeout::Error로 변환되므로, 그 호출 밖에서는 Timeout::Error로 보여요.

주의할 점은 코드 블록이 그 예외를 rescue해서 타임아웃을 무시할 수도 있다는 점이에요. 또 블록이 ensure로 예외 처리를 막을 수도 있어요. 그래서 이 메서드는 믿을 수 없는 블록(untrusted block)의 타임아웃을 강제하는 데는 의존할 수 없어요.

스케줄러(scheduler)가 정의돼 있으면 Scheduler#timeout_after를 호출해 타임아웃을 처리해요.

이 메서드는 Timeout 모듈의 메서드이면서 동시에 모듈 메서드예요. 그래서 클래스에 include Timeout해서 인스턴스 메서드 timeout으로 쓸 수도 있고, Timeout.timeout()처럼 직접 호출할 수도 있어요.

ensure 블록 안에서 타임아웃 예외가 발생하지 않게 하기

Timeout.timeout을 쓸 때, ensure 블록 안에서는 타임아웃 예외가 발생하지 않게 하고 싶을 수 있어요. 가장 간단하고 좋은 방법은 Timeout.timeout 호출을 begin/ensure/end 본문 안에 두는 거예요.

begin
  Timeout.timeout(sec) { some_long_operation }
ensure
  cleanup # 안전해요, 타임아웃에 방해받지 않아요
end

이 방법이 어려운 경우(예: some_long_operation 안에 타임아웃에 방해받으면 안 되는 ensure 블록이 있고, 그걸 밖으로 옮길 수 없는 경우) Thread.handle_interrupt로 타임아웃 예외를 지연시킬 수 있어요.

Thread.handle_interrupt(Timeout::Error => :never) {
  Timeout.timeout(sec, Timeout::Error) do
    setup # 몇 분이 걸려도 여기서는 타임아웃이 발생하지 않아요
    Thread.handle_interrupt(Timeout::Error => :immediate) {
      some_long_operation # 여기서는 타임아웃이 발생할 수 있어요
    }
  ensure
    cleanup # 몇 분이 걸려도 여기서는 타임아웃이 발생하지 않아요
  end
}

중요한 점은 Timeout.timeout에 exception klass를 넘겨줘야 한다는 거예요. 그렇지 않으면 동작하지 않아요. 특히 Thread.handle_interrupt(Timeout::ExitException => ...)을 쓰는 것은 지원되지 않고, 블록 밖에서 잘못된 예외를 던지는 것 같은 미묘한 오류를 일으키므로 쓰지 마세요.

Thread.handle_interrupt은 다소 위험한데, setup이나 cleanup이 막히면 현재 스레드도 막혀서 타임아웃이 결코 발생하지 않을 수 있기 때문이에요. 또 블록이 sec초보다 길게 실행될 수도 있어요. 예를 들어 some_long_operationsec초 + cleanup이 걸리는 시간만큼 실행돼요.

타임아웃을 블로킹 연산에서만 발생시키고 싶다면 :immediate 대신 :on_blocking을 쓸 수 있어요. 다만 그 경우 블록이 sec초 후에도 블로킹 연산을 쓰지 않으면 블록이 중단되지 않아요.

인스턴스 메서드 (Public Instance Methods)

timeout(*args, &block) [Private]

클래스 메서드 timeout과 동일한 동작을 모듈을 include한 쪽에서 쓸 수 있게 한 메서드예요.