Temporal 및 Temporal Ruby 소개
Temporal은 크래시에도 살아남아 장기 실행될 수 있는 견고한 코드를 작성하기 위한 시스템 및 프로그래밍 모델입니다. Temporal 워크플로우는 결정론적 코드 집합으로, 사이드 이펙트가 있는 동작을 이벤트로 기록하여 크래시 발생 시 중단된 지점부터 재개할 수 있도록 합니다. Ruby SDK는 이러한 Temporal의 기능을 Ruby 개발자들이 네이티브 Ruby 방식으로 활용할 수 있도록 지원합니다.
예제 코드 설명
본문에서는 간단한 원클릭 구매 워크플로우를 예시로 들어 Activity와 Workflow 구현 방법을 제시합니다.
-
Activity 구현:
PurchaseActivity는 HTTP POST 요청을 통해 구매를 처리하며, HTTP 응답 코드에 따라 재시도 가능 여부를 결정하는 예외 처리를 포함합니다. -
Workflow 구현:
OneClickBuyWorkflow는 10초 이내에 취소되지 않으면 구매를 확정하는 로직을 포함합니다.Temporalio::Workflow.sleep을 통한 내구성 있는 타이머, 워크플로우 취소 및 업데이트 기능을 활용합니다. -
Worker 및 Client 실행: 구현된 Activity와 Workflow는 Worker를 통해 Temporal 서버와 통신하며 실행됩니다. Client는 워크플로우 시작, 업데이트, 취소, 상태 쿼리 및 결과 대기 등 다양한 상호작용을 수행할 수 있습니다.
고급 SDK 구현 세부 사항
### Rust Core + Ruby C 확장
Temporal Ruby SDK는 TypeScript, Python, .NET SDK와 마찬가지로 Temporal의 공통 Rust Core를 활용하여 gRPC 클라이언트 및 Worker 상태 머신 등 복잡한 로직을 처리합니다. 이는 의존성을 줄이고 일관된 기능을 제공하는 이점을 가집니다. Ruby와 Rust 간의 브릿지는 Magnus와 rb-sys를 사용하며, Ruby의 GVL(Global VM Lock) 문제를 해결하기 위해 별도의 Ruby 스레드에서 Rust 코드를 실행하고 콜백을 통해 비동기적으로 Ruby 큐에 응답을 전달하는 방식을 사용합니다.
내구성 있는 파이버 스케줄러
Temporal 워크플로우 코드는 결정론적이어야 하므로, Ruby SDK는 커스텀 Fiber::Scheduler를 구현합니다. 이 스케줄러는 모든 비동기 작업이 결정론적으로 작동하도록 보장하며, run_until_all_yielded 메서드를 통해 이벤트 루프를 구성하여 파이버를 실행하고 외부 자극(Activity 완료 등)을 기다립니다. kernel_sleep 및 timeout_after를 구현하여 sleep 및 Timeout.timeout을 내구성 있게 만들지만, 기본적으로 비활성화됩니다.
추가 비동기 구성 요소
Ruby 표준 라이브러리의 부족한 부분을 보완하기 위해 다음과 같은 고수준 비동기 구성 요소를 제공합니다.
-
Temporalio::Cancellation: 계층적이고 보호될 수 있으며 비동기 호출을 중단할 수 있는 취소 메커니즘을 제공합니다. -
Temporalio::Workflow::Future: 여러 동시 작업을 쉽게 기다릴 수 있는 고수준의 동시성 제어 기능을 제공합니다. -
Temporalio::Workflow.wait_condition: 블록이 참(truthy)이 될 때까지 기다리는 강력한 기본 요소로, 워크플로우 상태 변화에 따라 대기하는 데 사용됩니다.
불법 호출 추적
워크플로우 코드의 결정론성을 유지하기 위해 Time.now, 스레드, 시스템 랜덤 등 비결정론적 호출은 금지됩니다. Ruby SDK는 TracePoint를 활용하여 워크플로우 스레드에서 발생하는 모든 호출을 검사하고, 설정된 illegal_workflow_calls 목록에 해당하는 호출을 탐지하여 예외를 발생시킵니다. 특정 경우(예: Time.new의 매개변수 유무)에는 TracePoint 바인딩을 통해 호출 매개변수에 접근하여 안전성을 판단합니다.
개발 중 배운 점
### 암시적으로 사용되는 동기화 구조
초기에는 sleep, Timeout.timeout, Queue, Mutex, Logger 등 표준 라이브러리 동기화 구조를 워크플로우에서 직접 사용하는 것을 허용했습니다. 그러나 Mutex와 같은 경우, 실제 블록킹 여부가 파이버 스케줄러에 정확히 전달되지 않아 드문 경쟁 조건과 교착 상태를 유발할 수 있음이 발견되었습니다. 이에 따라 SDK는 이러한 표준 라이브러리 구조의 암시적 사용을 금지하고, Temporalio::Workflow 모듈 내에서 워크플로우에 안전한 대안을 명시적으로 제공하도록 변경했습니다.
IO 대기 파이버 스케줄러
워크플로우에서는 IO가 비결정론적이기 때문에 `Fiber::Scheduler
io_wait는 처음에는 NotImplementedError를 발생시켰습니다. 하지만 텔레메트리, 디버거와 같이 결정론성을 위반해도 되는 특정 시나리오를 위해, IO.select를 재사용하는 비내구성/블록킹 형태의 io_wait 구현을 Temporalio::Workflow::Unsafe.io_enabled` 블록을 통해 옵트인 방식으로 제공합니다.
변환 및 런타임 타입 힌트
Ruby는 다른 정적/동적 타입 언어와 달리 런타임에 접근 가능한 명확한 타입 힌트가 부족합니다. 기본적으로 JSON 페이로드는 Hash로 역직렬화되어 사용자에게 불편함을 줄 수 있습니다. 이를 해결하기 위해 workflow_arg_hint, workflow_result_hint와 같은 “힌트” 개념을 도입했습니다. 이 힌트는 워크플로우 매개변수, 결과, 업데이트 매개변수 등 변환이 발생할 수 있는 모든 곳에서 제공될 수 있으며, 커스텀 컨버터가 JSON 데이터를 원하는 타입으로 변환하는 데 활용될 수 있습니다. 이는 사용자에게 유연하고 강력한 타입 변환 제어 기능을 제공합니다.