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::Error가 RuntimeError를 상속받아 rescue로 뜻하지 않게 잡힐 수 있기 때문이에요. 반면 Timeout::ExitException은 Exception을 상속받아 rescue Exception으로만 잡혀요. 참고로 Timeout::ExitException은 Timeout.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_operation이 sec초 + cleanup이 걸리는 시간만큼 실행돼요.
타임아웃을 블로킹 연산에서만 발생시키고 싶다면 :immediate 대신 :on_blocking을 쓸 수 있어요. 다만 그 경우 블록이 sec초 후에도 블로킹 연산을 쓰지 않으면 블록이 중단되지 않아요.
인스턴스 메서드 (Public Instance Methods)
timeout(*args, &block) [Private]
클래스 메서드 timeout과 동일한 동작을 모듈을 include한 쪽에서 쓸 수 있게 한 메서드예요.