Durable 코드 작성하기
워크플로우는 평범한 PHP 클래스입니다. 기본 클래스, 인터페이스 또는 프레임워크 타입이 시그니처에 필요하지 않습니다. Durable하게 만드는 것은 엔진이 저널(journal)에서 재생(replay)한다는 점이므로, 유일하게 지켜야 할 규칙은 **결정론(determinism)**입니다.
세 가지 형태
#[AsWorkflow('checkout')] // 클러스터가 인식하는 이름
final class CheckoutWorkflow
{
public function __construct(
private readonly WorkflowEnvironment $environment,
) {}
#[AsWorkflowMethod] // 워크플로우 클래스당 정확히 하나
public function run(string $orderId): string
{
$orders = $this->environment->activityStub(OrderActivities::class);
$receipt = $this->environment->await($orders->charge($orderId));
$this->environment->sleep(Duration::seconds(30), 'cooling down');
return $this->environment->await($orders->notify($receipt));
}
}
interface OrderActivities // CONTRACT가 이름을 전달합니다
{
#[AsActivityMethod(name: 'shop.order.charge')]
public function charge(string $orderId): string;
}
final class DoctrineOrderActivities implements OrderActivities // HANDLER가 실제 작업을 수행합니다
{
public function charge(string $orderId): string { /* 실제 부작용은 여기서 */ }
}
#[AsNexusService('billing')] // 다른 팀, 다른 배포
interface BillingContract
{
#[AsNexusOperation('check')]
public function check(string $order, int $amountInCents, string $currency): array;
}
절대 호출 지점에서 액티비티 이름을 문자열로 작성하지 마세요. 스텁(stub)이 계약의 속성을 읽으므로 이름은 정확히 한 곳에 존재합니다. 기존 워크플로우를 망가뜨리는 가장 흔한 방법은 메서드 이름을 바꾸면서 속성은 그대로 두는 것입니다. 실행 중인 워크플로우는 여전히 액티비티를 찾을 수 있습니다.
지켜야 할 규칙: 워크플로우 본문은 재생(replay)됩니다
#[AsWorkflowMethod] 안의 모든 코드는 엔진이 실행을 재개할 때마다 처음부터 다시 실행됩니다. — 다른 프로세스에서, 며칠 후일 수도 있습니다. 따라서 본문은 재생될 때 동일한 답을 반환해야 합니다:
| 워크플로우 본문에서 절대 사용 금지 | 대신 사용하세요 |
|---|---|
rand(), uniqid(), time(), new \DateTime() | $environment->sideEffect(fn() => …) — 결과는 한 번 저널에 기록됩니다 |
sleep(), usleep() | $environment->sleep($duration, 'why we wait') |
| 데이터베이스 쿼리, HTTP 호출, 파일 쓰기 | 액티비티(activity) |
배포 분기를 위한 if (getenv(…)) | $environment->version($changeId, $min, $max) |
액티비티(activity) 는 비결정성이 있어야 하는 곳입니다. 시도당 한 번 실행되며, 그 결과는 저널에 기록됩니다. 재생 시에는 코드 대신 저널이 응답합니다.
⚠ 액티비티는 재시도됩니다. 두 번째 시도도 안전하도록 작성하세요. 엔진은 결과가 한 번 기록되는 것을 보장하지만, 부작용이 한 번만 발생하는 것은 보장하지 않습니다. 카드 청구가 두 번 발생하는 것은 비즈니스 사고이지, 프레임워크 버그가 아닙니다.
대기 (Waiting)
스케줄 가능한 모든 것은 Awaitable을 반환합니다. 대기(await)하기 전까지는 아무 일도 일어나지 않습니다. — 이것이 병렬성을 얻는 방법이기도 합니다:
$a = $stub->reserve($order); // 스케줄만 되고, 대기하지는 않음
$b = $stub->quote($order);
[$reserved, $quoted] = $this->environment->await($this->environment->all($a, $b));
$first = $this->environment->await($this->environment->any($a, $b));
$twoOfThree = $this->environment->await($this->environment->some(2, $a, $b, $c));
await()은 선택적으로 마감 시간을 받습니다: await($awaitable, Duration::minutes(5)).
참조 파일
세 개 모두 읽지 말고 필요한 것만 읽으세요:
references/workflows.md— 신호(signals), 업데이트, 차일드 워크플로우, 타이머,continueAsNew, 버전 관리, 그리고 실행을 시작하는 방법 (웹 요청 안에서 인라인으로 절대 하지 마세요).references/activities.md—ActivityOptions, 재시도 정책, 네 가지 타임아웃, 재시도 불가 예외, 하트비트, 취소.references/nexus.md— Nexus 계약, 워크플로우가 연산을 수행할 때 계약이 왜 둘로 나뉘는지, 그리고 조용히 실패하는 페이로드 함정.references/migrations.md— 기존 코드를 건드리기 전에 이 문서를 읽으세요. Temporal PHP SDK에서 전환하거나, 한 Durable 버전에서 다음 버전으로 이동할 때:gplanchat/durable-rector가 재작성을 담당하며, 이 참조 문서는 주로 그 규칙이 추측하지 않기로 한 다섯 가지 사항에 관한 것입니다. — 그 중 두 가지는 이미 실행 중인 서버에서 조용히 실패합니다.
코드를 넘기기 전에 확인할 사항
- 워크플로우 클래스당 하나의
#[AsWorkflowMethod], 그리고 클래스는 호스트에 선언되어야 합니다 (Symfony/Sylius는 자동 설정, Magento는di.xml에 등록). - 모든 액티비티 호출은 계약(contract)에서 생성된 스텁을 통해 이루어집니다. — 문자열 이름 사용 금지.
- 워크플로우 본문에는 비결정적인 요소가 없어야 합니다.
- 모든 타이머에는 요약(summary)이 있어야 합니다: 이것은 관찰 타임라인에서 행의 이름이 되며,
TimerScheduled는 오전 3시에 운영자에게 아무 정보도 제공하지 않습니다. - 워크플로우는 보상할 수 없는 것은 캐치하지 마세요. 처리되지 않은 실패는 실행을 종료하고 가시적입니다. 삼킨 실패는 주문이 반쯤 처리된 채로 조용히 남습니다.