[Flowpath] 실행 이력에서 Retry까지, Rust 배치 Runtime 구현기

주재완·2026년 7월 23일

Flowpath

목록 보기
3/3
post-thumbnail

개요

지난 글에서는 아직 commit되지 않은 Chunk가 persistent state를 변경하지 않도록 Source, Cursor, Pipeline, Committer의 실행 계약을 만들었습니다. 하지만 이 상태를 어느 실행의 기록으로 남길지, 실패 후 어느 Cursor부터 다시 시작할지, 일시적인 오류 및 재처리에 대해서는 다루지 않았습니다.

예를 들어 하루치 정산 배치가 세 번째 Chunk에서 멈췄다고 가정해 보겠습니다. 앞선 두 Chunk는 commit됐지만, 세 번째 Chunk를 처리하는 동안 외부 API timeout이 발생했고 일부 정산 데이터에도 오류가 있었습니다. 프로세스를 다시 실행하기만 해서는 같은 업무를 이어서 처리하는 것인지, 새로운 업무를 시작하는 것인지 구분하기 어렵습니다. 다시 읽을 위치도 알 수 없습니다.

그래서 Flowpath에 Instance, Run, Partition 메타데이터와 순차 실행 Runtime을 추가했습니다. 이어서 마지막으로 commit된 Cursor를 복원하는 Restart, Chunk를 다시 만드는 Retry, 잘못된 Item을 제외하는 Skip을 구현했습니다.

Spring Batch의 실행 이력과 재시작, 오류 정책을 참고해 Flowpath의 Instance, Run, typed Cursor와 Chunk 실행 규칙을 설계했습니다. 이 글에서는 앞의 정산 배치를 따라가며 각 기능이 실제로 어떻게 맞물리는지 살펴봅니다.

같은 작업과 한 번의 실행을 구분하기

먼저 상황을 조금 더 구체적으로 만들어 보겠습니다.

  • Pipeline 이름 : daily-settlement
  • 식별 파라미터 : business_date=2026-07-22, tenant=seoul
  • 1,000건씩 commit
  • 첫 번째, 두 번째 Chunk는 성공
  • 세 번째 Chunk를 처리하다 실행 실패

7월 23일에 배치를 다시 실행하더라도 처리 대상은 여전히 7월 22일 정산입니다. 정산 작업은 하나지만 실행 기록은 실패한 첫 시도와 재시작한 두 번째 시도로 나뉩니다.

Spring Batch는 Job과 identifying JobParameters가 같으면 하나의 논리적 작업인 JobInstance로 묶습니다. 이 작업을 실제로 시도할 때마다 JobExecution이 하나씩 생깁니다. Flowpath에서는 이를 InstanceRun으로 나눴습니다.

Instance는 논리적으로 같은 작업을 묶고, Run은 실행할 때마다 새로 생깁니다. Partition에는 한 Run 안에서 나눠 처리한 범위별 상태와 통계를 기록합니다. 데이터를 나누지 않고 실행해도 default Partition 하나를 만듭니다.

Instance 식별에는 Pipeline 이름과 identifying parameter만 사용합니다. Chunk 크기나 worker 수처럼 같은 업무를 어떻게 실행할지 정하는 값은 ExecutionOptions로 분리해 Run에 저장합니다.

구분예시Instance 식별에 참여
논리적 작업Pipeline 이름, 영업일, tenant참여
실행 전략Chunk 크기, worker 수, Retry 횟수참여하지 않음
pub struct ExecutionOptions {
    pub chunk_size: usize,
    pub worker_count: usize,
    pub retry_limit: u32,
    pub preserve_order: bool,
}

chunk_size를 바꿨다고 7월 22일 정산이 다른 업무가 되지는 않습니다. 반대로 숫자 7과 문자열 "7"을 같은 파라미터로 취급하거나 map 순서에 따라 식별자가 달라지면 이전 실행 이력을 잘못 찾을 수 있습니다.

Flowpath의 InstanceKey는 Pipeline 이름과 IdentifyingParameters를 canonical encoding한 뒤 SHA-256으로 계산합니다. map은 중첩된 위치에서도 key 순서에 영향을 받지 않고, 값에는 type tag와 length prefix가 들어갑니다. list의 순서는 식별에 참여하며 숫자와 문자열도 구분합니다.

pub struct InstanceKey {
    algorithm: InstanceKeyAlgorithm,
    canonicalization_version: u16,
    digest: [u8; 32],
}

let key = InstanceKey::derive(pipeline_name, &parameters);

digest와 함께 algorithm, canonicalization version도 저장합니다. 나중에 정규화 규칙이 바뀌었을 때 기존 key가 어느 규칙으로 만들어졌는지 확인할 수 있습니다.

실행 상태는 정해진 방향으로만 바뀝니다.

실행을 시작하면 Running이 됩니다. 모든 Partition이 끝나면 Completed, 하나라도 실패하면 Failed, 실행 도중 멈추면 Stopped로 기록합니다. 더 이상 재시작하지 않을 실행은 Abandoned로 닫습니다.

Restart할 때는 실패한 Run을 수정하지 않고 같은 Instance에 새 Run을 만듭니다. 새 Run에는 이전 Run의 ID를 남기고, 아직 끝나지 않은 Partition만 다시 처리합니다.

이미 성공한 작업을 다시 돌릴 때는 rerun으로 기록합니다. 실패한 실행을 이어가는 것이 아니므로 attempt는 다시 1부터 시작하고 restart_of는 비워 둡니다.

LocalRuntime으로 Pipeline 실행하기

메타데이터 모델을 만들고 나면 실제로 상태를 움직일 실행 주체가 필요합니다. 첫 구현은 단일 프로세스에서 Pipeline을 순차 실행하는 LocalRuntime입니다.

let pipeline = Pipeline::from(InMemoryCursorSource::new([1_u32, 2, 3]))
    .map(|value| value * 2)
    .chunks(2)?
    .commit_with(RecordingCommitter::new());

let report = LocalRuntime::new().run(pipeline).await?;

assert_eq!(report.counts().committed, 3);
assert_eq!(report.committed_chunks(), 2);

Runtime은 다음 순서로 한 Run을 실행합니다.

  1. Pipeline 이름과 identifying parameter로 Instance를 찾거나 만듭니다.
  2. 새 Run과 기본 Partition을 만들고 Running으로 전이합니다.
  3. 마지막으로 commit된 Cursor 다음에서 Source를 엽니다.
  4. 소비한 Source Item 수를 기준으로 Chunk를 만듭니다.
  5. output은 CommitBatch, Cursor와 통계는 CommitContext에 담습니다.
  6. Committer가 성공한 뒤에만 committed Cursor와 lifecycle metadata를 갱신합니다.

Source에서 읽은 수와 commit을 마친 수는 따로 계산합니다. 세 번째 Chunk의 변환을 마쳤더라도 commit이 실패하면 해당 Chunk의 Item, output, Cursor는 아직 확정되지 않은 값입니다. report와 checkpoint는 이전 commit point에 그대로 둡니다.

AtomicCommitter 내부에서 Writer 이후 checkpoint 저장이 실패하는 경우에도 같은 원칙을 적용합니다. transaction을 rollback하고 이전에 commit된 output과 Cursor만 유지합니다.

Chunk 크기는 output 수가 아니라 소비한 Source Item 수를 기준으로 계산합니다. transform에서 모든 Item이 filter되어 output이 비어 있더라도 Source progress가 있다면 빈 CommitBatch와 Cursor를 commit합니다. Source 자체가 비어 있으면 commit 없이 Run을 완료하고, 마지막 Chunk가 설정 크기보다 작으면 partial Chunk로 commit합니다.

현재 LocalRuntimeCursorSourceAtomicGuarantee를 제공하는 Committer만 실행할 수 있습니다. 이 보장은 같은 adapter transaction에 참여한 destination write, checkpoint, execution statistics가 함께 보이거나 보이지 않는다는 뜻입니다.

별도의 in-memory lifecycle metadata는 Committer가 성공한 다음 갱신하므로 같은 transaction에는 들어가지 않습니다. 여러 Runtime 인스턴스가 동시에 실행되는 상황과 외부 side effect는 이후 unique constraint, lease, fencing, reconciliation을 추가해 처리할 예정입니다.

Spring Batch 공식 문서도 처리 데이터와 JobRepository가 서로 다른 transaction manager를 사용하면, 데이터 처리 후 repository를 갱신하기 전에 실패해 중복이 생길 수 있다고 경고합니다. Flowpath에서는 현재 Runtime이 요구하는 commit 조건을 AtomicGuarantee로 표시했습니다.

정상 반환과 오류만 처리하면 실행 lifecycle에도 빈틈이 남습니다. cooperative cancellation이 commit 전에 관찰되면 현재 provisional Chunk를 폐기하고 Stopped로 전이합니다. commit이 이미 성공한 뒤라면 output과 Cursor, 통계를 반영한 다음 멈춥니다. 실행 Future가 drop되는 경우에도 ActiveRunGuardRunning 상태를 Stopped로 바꿉니다.

따라서 Runtime의 복구 상태를 판단할 때는 처리한 수가 아니라 commit된 수만 사실로 사용할 수 있습니다.

마지막으로 commit된 Cursor에서 재시작하기

Restart는 이전 Run에서 결과와 함께 commit한 Cursor를 복원하고, 그 다음 위치에서 새 Run을 시작합니다.

Flowpath의 checkpoint는 opaque Cursor byte와 함께 그 Cursor를 만든 Run과 Partition을 기록합니다.

pub struct EncodedCheckpoint {
    run_id: RunId,
    partition_id: PartitionId,
    cursor: Vec<u8>,
}

Cursor의 type identifier와 serialization version은 CursorCodec이 opaque byte에 넣고 검증합니다. 덕분에 Runtime은 Source마다 다른 Cursor 형식을 직접 알 필요가 없습니다.

현재 Restart API는 repository에서 checkpoint를 자동으로 찾지 않습니다. 호출자가 실패한 Run과 해당 checkpoint를 RestartRequest에 전달합니다.

let request = RestartRequest::from_checkpoint(
    previous_run_id,
    checkpoint,
);

let report = runtime
    .restart(pipeline, request, &cursor_codec)
    .await?;

새 Run을 만들기 전에 다음 조건을 검증합니다.

  • 선택한 Run이 현재 Pipeline과 같은 Instance에서 만들어졌는가
  • 이전 Run의 상태가 Failed 또는 Stopped인가
  • checkpoint가 선택한 Run에서 만들어졌는가
  • LocalRuntime이 복원할 수 있는 기본 Partition의 checkpoint인가
  • 현재 CursorCodec으로 type, version, payload를 decode할 수 있는가

provenance와 codec 검증을 모두 마친 뒤에만 새 Run을 만듭니다. 호환되지 않거나 손상된 checkpoint 때문에 실행할 수 없는데도 metadata에 비어 있는 restart Run이 남는 것을 막기 위한 순서입니다.

checkpoint가 없을 때 처음부터 시작하는 동작도 기본값으로 두지 않았습니다.

pub enum MissingCheckpointPolicy {
    Reject,
    StartFromBeginning,
}

처음부터 재처리해도 안전한지는 일반적인 Runtime이 알 수 없습니다. 그래서 기본 정책은 Reject이며, 호출자가 StartFromBeginning을 명시해야 처음부터 새 attempt를 시작합니다. 성공한 Run은 restart할 수 없고, 다른 Instance에 속한 Run도 거부합니다.

실제 계약 테스트에서는 [1, 2, 3, 4, 5, 6]을 두 건씩 처리하고 Item 5의 transform에서 실패를 주입합니다.

Run #1
  Chunk [1, 2] commit → Cursor 1
  Chunk [3, 4] commit → Cursor 3
  Chunk [5, 6] transform 실패
  output = [1, 2, 3, 4]
  state = Failed

Run #2, restart_of = Run #1
  SourceStart::After(Cursor 3)
  Chunk [5, 6] commit
  state = Completed

Run #1의 실패 이력은 그대로 남고, 같은 Instance에 attempt = 2인 Run #2가 추가됩니다. Run #2의 report에는 재시작 후 처리한 두 건만 집계됩니다. Run #1의 count는 더하지 않습니다.

checkpoint에는 읽기 위치뿐 아니라 어느 Run에서 commit됐는지도 남아야 합니다.

모든 실패를 같은 방식으로 복구하지 않기

Restart만으로 일시적인 오류까지 매번 새 Run으로 넘길 필요는 없습니다. 반대로 모든 오류를 같은 loop로 다시 시도하면 영원히 성공할 수 없는 데이터 오류도 반복하게 됩니다.

오류 성격예시현재 Flowpath의 판단
일시적 오류timeout, connection resetRetry 후보
데이터 오류파싱 실패, 도메인 검증 실패Skip 후보
치명적 오류schema 불일치, 권한 오류Fail

Flowpath는 실패 위치를 FailureKind로 먼저 구분합니다.

pub enum FailureKind {
    SourceOpen, // Source를 열거나 연결하는 과정에서 발생한 오류
    SourceRead, // Source에서 Item을 읽는 과정에서 발생한 오류
    Transform,  // Item을 변환하거나 검증하는 과정에서 발생한 오류
    Committer,  // output, checkpoint, 통계를 commit하는 과정에서 발생한 오류
}

정책 predicate는 FailureKind를 확인하거나 원래 typed error로 downcast해 실패 원인을 판별합니다. 원래 오류는 판별할 때만 참조하고 failure record에는 저장하지 않습니다.

마지막 committed Cursor에서 Chunk 다시 만들기

현재 Flowpath의 Retry 단위는 Chunk입니다. 실패한 입력을 Runtime 메모리에 계속 보관하지 않고, 매 attempt마다 마지막으로 commit된 Cursor에서 Source를 다시 열어 Chunk를 재구성합니다.

fn retryable(failure: FailureRef<'_>) -> bool {
    matches!(
        failure.kind(),
        FailureKind::SourceOpen
            | FailureKind::SourceRead
            | FailureKind::Committer
    )
}

let retry = RetryPolicy::exponential_between(
        Duration::from_millis(5),
        Duration::from_millis(20),
    )
    .max_attempts(3)
    .when(retryable);

max_attempts(3)은 최초 실행을 포함해 세 번까지 시도한다는 뜻입니다. exponential backoff는 5ms, 10ms처럼 늘어나다가 설정한 maximum에서 멈춥니다. RetryPolicy는 Pipeline에 설정하고, Source 재오픈과 Chunk 재실행, 대기는 Runtime이 맡습니다.

attempt 1
Cursor C200 → Source open → read → transform → commit 실패

attempt 2
Cursor C200 → Source reopen → read → transform → commit 실패

attempt 3
Cursor C200 → Source reopen → read → transform → commit 성공

Retry하는 동안 checkpoint와 committed count는 그대로 유지됩니다. run/partition/chunk-N 형식의 operation ID도 attempt가 바뀌어도 같습니다. Committer adapter는 이 값을 unique key나 idempotency key로 사용할 수 있습니다.

계약 테스트에서는 두 Item의 Chunk가 Committer에서 두 번 실패한 뒤 성공하도록 구성했습니다. transform은 2 items × 3 attempts = 6번 호출되고, delay는 5ms와 10ms, failure action은 Retry, Retry로 남습니다. 계속 실패하는 Source read는 Retry, Retry, Fail을 남기고 output, checkpoint, committed count를 변경하지 않습니다.

Source와 transform은 attempt마다 다시 실행됩니다. 따라서 side effect가 있거나 mutable state를 가진 transform은 재실행을 고려해 멱등하게 구성하거나 상태 복원 전략을 함께 가져야 합니다.

Spring Batch는 Item 단위로 retryable exception과 횟수를 판단합니다. processor나 writer에서 실패하면 transaction rollback과 stateful retry로 Chunk 안의 Item이 다시 처리될 수 있습니다. Flowpath는 마지막 committed Cursor부터 Chunk를 다시 만드는 방식으로 Retry 범위를 정했습니다.

Transform 오류만 제외하는 Skip

Retry해도 성공할 수 없는 일부 데이터는 전체 Run을 실패시키지 않고 제외할 수 있습니다.

fn skippable(failure: FailureRef<'_>) -> bool {
    failure.kind() == FailureKind::Transform
}

let skip = SkipPolicy::new()
    .max_items(100)
    .when(skippable);

현재 Skip은 transform failure에만 적용합니다. Source read나 Committer failure를 건너뛰면 어느 데이터가 빠졌고 어떤 결과가 저장됐는지 확인하기 어렵기 때문입니다. skip limit은 Chunk마다 초기화하지 않고 Run 전체의 committed skip count와 현재 pending Chunk를 합쳐 계산합니다.

[1, 2, 3] 중 Item 2의 transform이 실패하고 limit이 1이라면 Item 2를 제외한 [1, 3]을 commit합니다. Cursor는 Item 3까지 진행하고 count는 read = 3, committed = 2, skipped = 1이 됩니다.

두 번째 skippable Item이 나타나 limit을 넘으면 아직 확정하지 않은 현재 Chunk 전체를 commit하지 않고 Run을 실패시킵니다. Retry와 Skip 조건에 모두 맞는 transform 오류는 Retry를 먼저 적용합니다. 다만 Retry 횟수를 모두 사용한 오류를 자동으로 Skip으로 바꾸지는 않고 Fail로 끝냅니다.

Spring Batch는 read, process, write 단계의 Skip을 지원하고 Step 전체에서 limit을 계산합니다. 현재 Flowpath는 transform failure에 Run 전체 limit을 적용합니다.

Retry, Skip, Fail 판단 기록하기

오류 정책을 운영하려면 어떤 실패에 어떤 판단을 내렸는지 조회할 수 있어야 합니다. FailureRecord에는 Run과 Partition, 실패 위치, stable code, attempt, Retry | Skip | Fail action과 발생 시각을 기록합니다.

원래 오류의 Debug 문자열이나 Source payload는 기본으로 저장하지 않습니다. 오류와 입력 데이터에는 개인정보나 인증 정보가 포함될 수 있기 때문입니다.

pub enum PayloadPolicy {
    None,
    KeyOnly,
    Redacted,
    Full,
    External,
}

이 정책은 adapter가 제공할 수 있는 payload 노출 상한입니다. 현재 LocalRuntime은 Full을 선택하더라도 payload나 business key를 직접 만들어 채우지 않습니다.

FailureStore가 기록을 거부하면 해당 실패를 fatal로 처리합니다. Skip한 뒤 실행은 계속됐지만 감사 기록은 사라지는 상태를 허용하지 않기 위해서입니다. 다만 FailureStore 기록과 Chunk commit은 현재 하나의 transaction이 아니므로, 같은 logical Item의 occurrence가 attempt별로 여러 번 남을 수 있습니다.

실제 시간을 기다리는 코드도 교체할 수 있게 Sleeper capability로 분리했습니다. Runtime에는 ThreadSleeper를 사용하고 테스트에는 delay를 기록만 하는 fake sleeper를 주입해 backoff 순서를 실제 대기 없이 검증합니다.

재시도 횟수를 정하기 전에 어떤 상태를 다시 실행하고 어떤 실패를 제외할지부터 정의해야 합니다.

하나의 실행 흐름으로 다시 따라가기

처음의 정산 배치를 현재 Runtime 기준으로 다시 실행해 보겠습니다.

Instance
  daily-settlement / 2026-07-22 / seoul

Run #1
  Chunk 0 commit → checkpoint C100
  Chunk 1 commit → checkpoint C200
  Chunk 2 외부 API timeout
    attempt 1 실패
    attempt 2 실패
    attempt 3 실패
  state = Failed

Run #2, restart_of = Run #1
  checkpoint C200의 Run/Partition/codec 검증
  SourceStart::After(C200)
  잘못된 정산 Item 1건 → Skip 기록
  나머지 output + Cursor + stats commit
  state = Completed

Instance는 두 Run을 같은 논리적 정산으로 묶습니다. Run #1의 Retry는 같은 실행 안에서 일시적인 실패를 복구하려는 짧은 시도입니다. 모든 attempt가 실패하면 Run #1을 Failed로 닫습니다.

Restart는 Run #1을 되감지 않고 Run #2를 만듭니다. checkpoint의 provenance와 codec을 먼저 검증한 뒤 C200 다음에서 Source를 엽니다. transform에서 허용한 데이터 오류 한 건은 Skip record로 남고, 나머지 output과 다음 Cursor가 commit되면 Run #2를 완료합니다.

여기서 Retry와 Restart는 다음처럼 구분됩니다.

  • Retry는 같은 Run 안에서 일시적인 실패를 다시 시도합니다.
  • Restart는 실패하거나 중단된 Run 이후에 새 Run을 만들고 committed state에서 재개합니다.
  • Replay는 저장한 실패 데이터를 별도의 실행으로 다시 처리하는 기능이며 아직 구현하지 않았습니다.

코드만 보면 각각 enum과 loop에 가깝습니다. 하지만 실제 실행에서는 Instance identity와 commit boundary를 중심으로 모두 맞물립니다.

Spring Batch와 현재 Flowpath 비교

실행 이력과 복구를 중심으로 Spring Batch와 현재 Flowpath를 비교하면 다음과 같습니다.

관점Spring Batch현재 Flowpath
논리적 작업Job + identifying parameters의 JobInstancePipeline name + identifying parameters의 Instance
실행 시도JobExecutionRunrestart_of lineage
하위 실행 상태StepExecutionPartition이 실행 범위의 상태와 통계 일부 담당
복구 상태ExecutionContext, ItemStreamtyped Cursor, CursorCodec, EncodedCheckpoint
RetryItem 관점의 정책과 stateful retrycommitted Cursor 이후 Chunk 재구성
Skipread, process, write 단계 지원transform failure만 지원
메타데이터 저장JobRepositoryprocess-local InMemoryMetadataRepository

Spring Batch의 Step은 독립된 처리 단계이고 partitioning은 별도의 확장 기능입니다. Flowpath의 Partition은 Run 아래에서 처리 범위별 상태와 통계를 관리합니다. 이 역할은 StepExecution과 일부 겹치지만 계층 구조는 다릅니다.

Spring Batch에서는 ItemStream 구현이 commit 전에 Reader 상태를 ExecutionContext에 저장하고 restart할 때 복원합니다. Flowpath는 Source별 typed Cursor와 CursorCodec으로 읽기 위치를 저장하고 복원합니다.

Chunk 크기는 처리량뿐 아니라 transaction, rollback, 재처리 범위까지 결정합니다. 크게 잡으면 한 번의 commit으로 더 많은 데이터를 처리할 수 있지만, 실패했을 때 다시 실행할 데이터도 늘어납니다.

현재 구현 범위

현재 Flowpath Runtime은 실행 규칙을 검증하는 단일 프로세스 구현입니다.

  • metadata와 failure record는 process-local in-memory 저장소에 보관합니다.
  • 기본 Partition 하나를 순차 실행하며 병렬 또는 분산 실행은 없습니다.
  • 여러 Step으로 구성된 Job flow는 없습니다.
  • SQL metadata repository와 durable checkpoint discovery는 없습니다.
  • lease, heartbeat, fencing, optimistic CAS는 없습니다.
  • coordinated commit의 crash reconciliation은 없습니다.
  • Item 단위 stateful Retry와 read/write Skip은 없습니다.
  • 저장된 실패 데이터를 실행하는 Replay engine은 없습니다.

기본 ThreadSleeper도 async timer가 아니라 std::thread::sleep으로 실행 thread를 막습니다. local reference Runtime에서는 정책 경계를 확인하는 데 집중했지만, async scheduler에서 사용하려면 non-blocking sleeper가 필요합니다.

Core domain에는 여러 Partition과 monotonically increasing revision이 있습니다. 이후 distributed ownership을 구현할 때 이 모델에 stale revision 검증과 lease 규칙을 연결할 예정입니다.

다음 단계에서 durable metadata repository를 붙인다면 단순히 in-memory map을 SQL table로 옮기는 것으로 끝나지 않습니다. 동시에 같은 Instance를 시작하는 실행을 막는 제약, checkpoint compare-and-set, Run과 Partition revision 충돌, 중단된 worker를 판정하는 lease와 fencing을 함께 정의해야 합니다.

마치며

지난 글에서는 commit되지 않은 Chunk가 persistent state를 바꾸지 않도록 Core contract를 만들었습니다. 이번에는 이 결과를 실행 이력에 남기고, 실패 후 새 Run을 만들며, 실행 중에 Retry와 Skip을 판단하는 Runtime 규칙을 추가했습니다.

구현하면서 Spring Batch의 JobInstance, JobExecution, ExecutionContext, Retry, Skip이 실패 후 실행을 이어 가는 데 필요한 장치라는 점을 다시 확인했습니다. Flowpath에서는 Instance, Run, typed Cursor와 Chunk 재구성으로 같은 문제를 풀었습니다.

그리고 구현하면서 정한 규칙은 다음과 같습니다.

Checkpoint는 마지막으로 읽은 위치가 아니라, 결과와 함께 commit된 복구 상태여야 합니다.

Instance와 Run은 그 상태가 어느 실행의 것인지 기록합니다. Restart는 그 상태를 검증한 뒤 새 실행을 만들고, Retry는 그 경계 이후의 Chunk만 다시 만듭니다. Skip은 허용한 데이터 오류를 기록하고 제외하지만 현재 Chunk가 commit되기 전까지는 count와 Cursor를 확정하지 않습니다.

신뢰할 수 있는 배치 Runtime이라면 실패 후 어떤 상태가 남았는지, 다음 실행이 어디서부터 시작하는지 분명히 알 수 있어야 합니다. 이번 구현으로 실행 이력부터 Retry까지 하나의 commit boundary를 기준으로 처리할 수 있게 됐습니다.

참고 자료

profile
데이터베이스, 트랜잭션 구조 설계에 관심이 많은 백엔드 개발자입니다.

0개의 댓글