본문으로 건너뛰기

실행 저널

샌드박스 처리된 코딩 에이전트는 10분 동안 작업할 수 있습니다. 에이전트의 stdout 파이프를 유지하는 호스트 프로세스가 3분째에 사라지면 해당 파이프가 끊기고, 에이전트는 SIGPIPE을(를) 받으며 작업이 사라집니다. 에이전트 출력의 유일한 복사본이 전송 중이었기 때문에 읽을 수 있는 내용이 아무것도 남지 않습니다.

따라서 에이전트는 파이프에 쓰지 않습니다. stdout은 샌드박스 내부의 파일로 리디렉션되고, 호스트는 해당 파일을 tail합니다:

/tmp/tanstack-runs/<runId>.ndjson    every NDJSON event the agent emitted
/tmp/tanstack-runs/<runId>.err the agent's stderr, kept separate

파이프가 없으므로 사라짐으로 에이전트에 신호를 보낼 리더도 없습니다. 또한 저널은 파일이므로 리더는 원할 때마다 바이트 0부터 시작할 수 있습니다.

저널을 요청해야 합니다

저널링은 기본적으로 활성화되지 않으며, 이것이 가장 먼저 올바르게 처리해야 할 사항입니다. — 이 페이지의 다른 모든 내용은 파일이 존재한다고 가정합니다. 내장된 세 가지 하네스 어댑터(grokBuildText, claudeCodeText, codexText)는 실행이 영속적인 경우에만 실행을 저널링합니다. 즉, withSandbox둘 다 runsdurabilitywithSandbox(sandbox, { runs, durability: { adapter } })이(가) 전달된 경우입니다, 아래 스니펫이 하는 것처럼 동작합니다. 둘 다 전달하지 않으면(일반적인 withSandbox(sandbox)) 또는 하나만 전달하면 저널이 전혀 없습니다: journalOptionsForundefined에 답하고, spawnNdjson은 해당 원래 저널이 없는 경로를 사용하며, 에이전트의 stdout은 다시 정확히 파이프가 됩니다. 이 기능이 존재하기 전이었습니다. 아무런 경고도 표시되지 않으며, 절반만 구성된 앱은 아직 영속성을 요청했으므로 경고할 내용이 없습니다. 찾아보려 한다면 /tmp/tanstack-runs/<runId>.ndjson 일반적인 withSandbox(sandbox)를 실행한 후, 찾을 수 없으며, 그 이유는 provider가 아니라 바로 여기에 있습니다.

두 가지가 모두 필요한 이유는 다음과 같습니다. 이벤트 로그가 없는 record는 재생할 수 없고, record가 없는 로그는 찾거나, 소유권을 주장하거나, 회수할 수 없습니다. 유용한 절반은 존재하지 않습니다.

다시 계산할 수 있는 id를 모든 실행에 부여합니다

저널 경로는 runId만으로 파생되므로, 재현할 수 없는 runId는 아무도 찾을 수 없는 저널이 됩니다. 어댑터는 비영속적 실행에서 임의의 내부 id로 대체합니다. 영속적인 실행에는 대체 수단이 전혀 없습니다 — chatStream은 의도적으로 DurableRunIdRequiredError를 throw합니다. 어떤 후속 호스트도 다시 계산할 수 없는 id는 정상적으로 스트리밍되지만 조용히 복구할 수 없는 실행을 만들기 때문입니다.

따라서 요청에 이미 포함된 runId를 저널링을 활성화하는 두 저장소와 함께 전달합니다:

import {
chat,
chatParamsFromRequest,
memoryStream,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { claudeCodeText } from '@tanstack/ai-claude-code'
import { memoryPersistence } from '@tanstack/ai-persistence'
import { withSandbox } from '@tanstack/ai-sandbox'
// Your `defineSandbox(...)` result.
import { sandbox } from './sandbox'

// Single-process stand-ins, enough to see a journal on your laptop. A real
// deployment needs a store and a stream backend every replica can reach — see
// ./takeover for that wiring, plus the distributed lock it requires.
const persistence = memoryPersistence()
const { runs } = persistence.stores

export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request)
// One instance, handed to both the middleware and the transport, so the
// journal and the delivery log describe the same run. `memoryStream` reads the
// run id from the `X-Run-Id` header `@tanstack/ai-client` sends, so it needs
// no help on a POST route; `durableStream` does — see ./takeover.
const adapter = memoryStream(request)
const stream = chat({
adapter: claudeCodeText('claude-sonnet-4-6'),
messages,
threadId,
// Without this a durable run throws: the journal path and the deterministic
// message ids are both derived from it, so a later reader can only find the
// journal of a run whose `runId` it can recompute.
runId,
// BOTH options, or there is no journal to read.
middleware: [withSandbox(sandbox, { runs, durability: { adapter } })],
})
return toServerSentEventsResponse(stream, { durability: { adapter } })
}

@tanstack/ai-client은 모든 실행에 대해 새로운 runId를 발급하고 이를 AG-UI 요청 본문에 넣으며, 이것이 chatParamsFromRequest이 반환하는 내용입니다. 따라서 이 작업에서 client 측이 할 일은 전혀 없습니다. useChat은 이미 실행마다 고유한 id를 전송하며, 재연결 동작도 변경되지 않습니다.

import { fetchServerSentEvents } from '@tanstack/ai-client'
import { useChat } from '@tanstack/ai-react'

export function Chat() {
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
return (
<button onClick={() => sendMessage('Refactor the auth module')}>
Send ({messages.length} messages)
</button>
)
}

각 실행마다 runId는 고유해야 합니다

저널은 추가 전용이며, /tmp/tanstack-runs은(는) 단일 실행, 테스트 또는 프로세스보다 오래 유지되는 고정된 절대 경로입니다. 따라서 runId을(를) 재사용해도 새 저널이 시작되지 않습니다. 이전 실행의 종료 센티널 뒤에 기존 저널에 추가됩니다.

리더는 테일 윈도우에서 확인하는 마지막 일치 센티널에서 중지합니다. 따라서 ID를 재사용하면 두 번째 실행이 이벤트를 전혀 내보내지 않은 것처럼 보이거나, 이전 실행의 종료 코드로 실패한 것처럼 보입니다. 예외도 발생하지 않고 경고도 표시되지 않으므로, 조용히 비어 있는 실행을 얻게 됩니다.

이는 강제되지 않습니다. 추가를 거부하면 append-only 규칙이 존재하는 목적인 재생이 손상되기 때문입니다. 실행마다 고유한 값(UUID 또는 클라이언트 자체의 runId)에서 id를 도출하고 리터럴을 하드코딩하지 않습니다.

저널 읽기

readJournal는 한 번에 완전한 한 줄을 반환하며, 각 줄에는 개행 바로 다음의 절대 바이트 위치가 태그로 지정됩니다. 에이전트가 아직 작성 중인 부분적인 줄은 절대 반환되지 않으므로 위치는 항상 재개할 수 있는 바이트 오프셋입니다.

import { journalPaths, readJournal } from '@tanstack/ai-sandbox'
// A live `SandboxHandle`, e.g. from `provider.create(...)`.
import { handle } from './sandbox-handle'

async function tailRun(runId: string) {
for await (const { line, endPosition } of readJournal(handle, {
paths: journalPaths(runId),
})) {
console.log(endPosition, line)
}
}

5분 전에 다른 프로세스에서 시작된 실행에서도 해당 호출에 변경되는 사항은 없습니다. Resume는 별도의 코드 경로가 아닙니다. 모든 읽기는 일부 N에 대해 항상 tail -c +N이며, 새 실행은 단순히 N = 0입니다.

알아 두면 좋은 세부 사항은 다음과 같습니다:

  • 저널은 셸을 통해서만 접근되며, 절대 handle.fs.*을 통해 접근되지 않습니다. 로컬 프로세스 provider에서는 fs.write이(가) /tmp을(를) 확인합니다 샌드박스 루트 아래에서 확인하는 반면, 셸 리디렉션은 실제 호스트 /tmp에 적용됩니다. 두 부분이 정확히 서로 일치하는 이유는 어느 것도 fs을(를) 사용하지 않기 때문입니다. 작성하면 자체 저널링 도구를 사용한다면, handle.process에 보관합니다.
  • Stderr는 자체 사이드카 파일에 저장되므로, tail 진단 또는 대화가 많은 CLI 배너가 이벤트 바이트에 스스로 삽입되는 일은 절대 없습니다. 0이 아닌 종료 해당 사이드카의 나머지 부분을 오류 메시지에 포함합니다.

기능에 따라 선택하는 팔로우 또는 폴링

대부분의 provider는 tail -c +N -f을(를) spawn에서 실행할 수 있으며, 폴링 비용 없이 스트리밍됩니다. 생성된 프로세스를 중지할 수 없는 provider에는 대신 제한된 exec 호출의 폴링 루프가 제공되며, 각 호출은 자체적으로 종료되므로 샌드박스 내부에서는 중지할 수 없는 프로세스가 절대 시작되지 않습니다.

선택은 provider의 이름이 아니라 핸들의 capabilities에서 backgroundProcesses && killableProcesses을 읽으므로, 동일한 제한이 있는 BYOP(provider 직접 지정)도 같은 방식으로 처리됩니다. 어떤 provider가 무엇을 선언하는지는 killableProcesses을 참조하세요.

진단을 위해 어느 전략이든 강제로 지정할 수 있습니다:

import { journalPaths, journalReadStrategy, readJournal } from '@tanstack/ai-sandbox'
import { handle } from './sandbox-handle'

console.log(journalReadStrategy(handle)) // 'follow' | 'poll'

const lines = readJournal(handle, {
paths: journalPaths('run-2a7f'),
strategy: 'poll',
pollIntervalMs: 100,
})

종료 sentinel에는 nonce가 포함됩니다

에이전트가 종료한 후 셸이 실행에 추가하는 줄은 더 이상 예전의 단순한 {"__exit":N}이 아닙니다. 이제 두 번째 필드도 포함됩니다:

{"__exit":0,"__nonce":"3f9c1a7b..."}

에이전트의 stdout과 이 sentinel 줄은 둘 사이에 프레이밍 없이 동일한 프레임 없는 저널 파일로 리디렉션됩니다. nonce가 없으면 에이전트가 우연히 출력한 줄 중 {"__exit":N}과 같은 형식인 줄 — 에코된 fixture, cat된 진단 파일, 덤프된 config — 은 모두 유효한 sentinel이 됩니다. 리더는 tail window에서 일치하는 첫 번째 줄을 사용하므로, 에이전트가 이러한 줄을 초기에 출력하면 probeRunExit은 아직 실행 중인 실행을 'finished' 상태라고 보고했습니다. 그러면 reaper는 해당 실행을 terminal 상태로 전환하고, 실행 중인 에이전트가 사용 중인 샌드박스를 회수하게 됩니다 — 이는 이 전체 저널 메커니즘이 절대 하지 않겠다고 약속하는 유일한 일이었습니다.

__noncerunId에서 파생된 실행별 값입니다: sha256('tanstack-ai-sandbox/journal-exit-sentinel/v1:' + runId), 다음 길이로 잘립니다. 32개의 16진수 문자입니다. 의도적으로 무작위가 아니라 파생된 값입니다. reaper가 읽는 대상은 이미 종료된 프로세스가 작성한 저널이며, 남아 있는 것은 runId뿐이므로 후속 호스트가 원래 프로세스가 작성한 것과 동일한 nonce를 다시 계산할 수 있어야 합니다. parseJournalExit는 이제 해당 창을 에서부터 역방향으로 검사합니다(shell은 항상 모든 내용 뒤에 실제 sentinel을 작성합니다. 에이전트 자체의 출력)를 사용하며, __exit 값이 다음이 아닌 일치하는 줄은 거부합니다 0(으)로 강제 변환하지 않고 정수입니다.

두 변경 사항은 우발적 위조 유형을 완전히 제거하지만, 남은 위험은 정직하게 인정해야 합니다. nonce는 비밀이 아니라 파생되므로, 자체 runId를 알고 파생 과정을 다시 구현한 에이전트는 의도적으로 일치하는 줄을 여전히 출력할 수 있습니다. 이를 방지하려면 적법한 호스트만 읽을 수 있는 비밀을 실행 기록에 저장해야 합니다. 이는 RunStore 스키마 변경이 필요하며, 이 저널 구성 모듈만으로는 수행할 수 없습니다.

저널을 직접 시드하는 경우(테스트, 픽스처, 또는 journaledCommand를 거치지 않는 사용자 지정 하네스) JSON을 직접 작성하지 말고 exitSentinelLine(paths, exitCode)로 센티널을 기록해야 합니다. 그러면 셸의 printf가 생성할 정확한 바이트를 생성할 수 있습니다:

import { exitSentinelLine, journalPaths } from '@tanstack/ai-sandbox'

function seedSentinel(runId: string, exitCode: number): string {
const paths = journalPaths(runId)
return exitSentinelLine(paths, exitCode)
// '{"__exit":0,"__nonce":"..."}' — append this, not a hand-written object.
}

읽기가 실패할 수 있습니다: 'journal-stalled'

readJournal는 아무것도 생성하지 않을 저널을 기다리며 영원히 멈출 수 없습니다. follow 전략과 poll 전략 모두 저널의 첫 번째 바이트를 기다리는 시간을 DEFAULT_ATTACH_JOURNAL_WAIT_MS(기본값은 10초)로 제한합니다. 이 시간 동안 아무것도 수신하지 못한 읽기는 대기 상태로 남는 대신 JournalAttachUnavailableErrorreason: 'journal-stalled'와 함께 발생시킵니다.

해당 상한이 존재하는 이유는 journalFollowCommand의 첫 번째 동작이 : >> file이기 때문입니다 — 저널을 tail하기 전에 먼저 저널을 생성해야 합니다. 따라서 저널이 존재한 적이 없는 runId에 대한 attach는 빈 파일을 만들어 영원히 이를 따라가곤 했으며, 해당 파일이 존재하게 되면 이후의 모든 존재 확인(test -f)도 성공하므로 동일한 runId가 이후의 모든 attach에서도 멈추게 됩니다. 이 상한은 첫 번째 바이트에만 적용됩니다. 저널이 생성을 시작한 후 에이전트가 줄 사이에서 얼마나 오래 기다린다고 생각하는지는 에이전트의 문제이며, 여기의 어떤 데드라인도 정상적인 실행을 중간에 종료시키지 않습니다.

import {
JournalAttachUnavailableError,
journalPaths,
readJournal,
} from '@tanstack/ai-sandbox'
import { handle } from './sandbox-handle'

async function tailWithStallGuard(runId: string) {
try {
for await (const { line } of readJournal(handle, {
paths: journalPaths(runId),
runId,
})) {
console.log(line)
}
} catch (error) {
if (error instanceof JournalAttachUnavailableError && error.reason === 'journal-stalled') {
// The journal exists but nothing is appending to it and no sentinel can
// arrive — the run's shell was killed before it wrote one, or the read
// itself just created an empty file for a runId with no journal.
} else {
throw error
}
}
}

호출별 대기 시간은 firstByteTimeoutMs로 재정의하거나, 0를 전달하여 완전히 비활성화할 수 있습니다 — 단, 읽기를 이미 다른 데드라인이 적용하는 경우에만 사용해야 합니다. 빈 저널에 대한 무제한 읽기는 반환되지 않기 때문입니다.

읽기가 시작되기 전에 attach 게이팅하기

readJournal에는 이를 조회할 수 있는 RunStorerunId도 없으므로 알 수 없거나 종료된 실행과 단순히 아직 첫 번째 줄을 작성하지 않은 실행 중 어느 것인지 구분할 수 없습니다 — 모든 경우가 동일하게 보이므로 동일한 제한 대기를 적용합니다. 실제로 store를 보유한 호출자는 먼저 더 정확한 질문을 awaitAttachableJournal로 수행할 수 있습니다. 이 방법은 실행 레코드를 확인한 후 동일한 제한 대기로 대체하므로, 오래되었거나 잘못 입력된 runId 은 전체 타임아웃이 끝날 때까지 기다리는 대신 'unknown-run' 또는 'terminal-run'로 빠르게 실패합니다.

import { awaitAttachableJournal, journalPaths } from '@tanstack/ai-sandbox'
import { memoryPersistence } from '@tanstack/ai-persistence'
import { handle } from './sandbox-handle'

// The same `RunStore` `withSandbox` was given.
const { runs } = memoryPersistence().stores

async function gateAttach(runId: string) {
await awaitAttachableJournal(handle, {
paths: journalPaths(runId),
runId,
runs,
})
// Only after this resolves is it worth calling `readJournal`.
}

이미 전달한 로그에 저널 다시 재생하기

한 문장으로 정리하면 다음과 같습니다. 클라이언트에는 로그가 우선하고, 드라이버의 재개 위치에는 저널이 우선합니다. 이 섹션의 모든 내용은 이 규칙을 기계적으로 적용한 것입니다.

저널 바이트 0부터 1000까지를 번역하고 그 결과 청크를 resumable-stream 로그에 추가한 뒤 사라진 호스트는 클라이언트에 해당 청크를 남겼습니다. 바이트 0부터 같은 저널을 읽는 호스트는 이를 다시 도출합니다. 이를 두 번째로 추가하면 클라이언트가 손상됩니다. 중복 제거는 durability 어댑터가 생성한 오프셋을 기준으로 하므로, 다시 추가된 청크가 새 항목처럼 보이고 스트림 프로세서는 텍스트 델타와 도구 호출 인수를 무조건 연결하기 때문입니다. 문장이 중복되고 {"a":1}{"a":1} 인수가 중복됩니다.

alignToStoredLog가 이를 방지하는 기본 요소입니다. 저장된 접두사를 snapshot()로 한 번 읽고, 재생 결과가 이를 재현하는지 확인한 다음 접두사를 억제하고 나머지만 생성합니다:

import { memoryStream } from '@tanstack/ai'
import { alignToStoredLog } from '@tanstack/ai-sandbox'
import type { StreamChunk } from '@tanstack/ai'

async function forwardRemainder(
request: Request,
replayed: AsyncIterable<StreamChunk>,
) {
const durability = memoryStream(request)
for await (const chunk of alignToStoredLog(replayed, { durability })) {
await durability.append([chunk])
}
}

일반 append이며, 어댑터가 생성한 오프셋을 사용해 추가 순서대로 처리됩니다. 호출자가 선택한 오프셋은 사용되지 않으므로 durableStreammemoryStream 모두에서 작동합니다. 오프셋 소유권을 참고하세요.

ID는 결정적이므로 재생을 비교할 수 있습니다

정렬은 재번역 결과가 저장된 청크와 동일할 때만 저장된 청크를 인식할 수 있으며, Date.now()Math.random()로 만든 메시지 ID는 결코 동일해지지 않습니다. 따라서 저널링된 경로에서는 실행 범위 카운터에서 대신 ID를 생성합니다.

눈에 보이는 결과로 메시지 ID는 <runId>-0, <runId>-1 등과 같은 형태가 되며, <provider>-<timestamp>-<random>와 같은 형태가 아닙니다. ID는 실행마다 고유하고 불투명하게 유지됩니다. 이를 provider 이름이나 타임스탬프로 파싱하고 있었다면 중단하세요.

비교에서 timestamp는 제외됩니다. 이는 벽시계 시간이므로 재현할 수 없습니다. 명시적으로 undefined인 값을 가진 필드와 중첩된 도구 호출 인수를 포함해 다른 모든 필드가 비교에 참여합니다.

발산 시 예외가 발생합니다

재생 결과가 해당 인덱스에서 로그에 저장된 청크와 다르면 alignToStoredLog가 인덱스와 두 지문을 포함한 JournalReplayDivergedError를 발생시킵니다. 불일치한 결과를 전달하지 않습니다.

import { JournalReplayDivergedError } from '@tanstack/ai-sandbox'

function describe(error: unknown): string {
if (error instanceof JournalReplayDivergedError) {
return `chunk ${error.index}: stored ${error.stored}, replayed ${error.replayed}`
}
throw error
}

여기서는 큰 소리로 실패하는 것이 올바른 절충입니다. 대안은 성능이 저하된 전달이 아니라 손상된 전달이기 때문입니다. 클라이언트에는 오프셋 중복 제거 아래에 안전망이 없으므로 중복을 견딜 수 없습니다. 발산은 번역이 더 이상 결정적이지 않다는 의미이며, 복구해야 할 조건이 아니라 수정해야 할 버그입니다.

결정성이 다루지 않는 내용

보장은 번역기 수준에서 제공됩니다. 동일한 journal 바이트를 두 번 번역하면 동일한 청크 시퀀스가 생성됩니다.

@tanstack/ai-codex@tanstack/ai-claude-code에서는 번역된 스트림이 라이브 도구 실행에서 발생한 host-tool-bridge 이벤트를 전달하는 두 번째 스트림과 병합됩니다. 이러한 이벤트는 재생 시에는 발생하지 않으며, 어떤 경우든 번역된 청크와의 인터리빙은 타이밍에 따라 달라집니다. 따라서 브리지된 도구를 사용한 실행은 여전히 달라질 수 있으며, 손상된 데이터를 전달하는 대신 그렇게 되었음을 알립니다.

grokBuildText의 git-diff 청크도 번역 범위에 포함되지 않습니다. 번역된 스트림이 끝난 후 셸 아웃되므로 재현할 수 없습니다. 완료되는 실행의 마지막 부분에서만 방출됩니다.

journal 수명

리더가 종료 센티널을 관찰하면 두 파일이 모두 삭제됩니다. 종료 코드가 0인지 0이 아닌지와 관계없이 동일합니다. 센티널 없이 읽기가 끝나면(소비자가 중지했거나 클라이언트가 사라진 경우) 아무것도 삭제되지 않습니다. 실행이 아직 진행 중일 수 있으며 모든 바이트가 여전히 필요할 수 있기 때문입니다.

한 가지 경우에는 정리가 필요합니다. journal을 읽는 사람이 없는 상태에서 실행이 센티널에 도달하면 센티널을 관찰할 리더가 없으므로 아무것도 정리되지 않으며, 해당 journal은 sandbox가 사라질 때까지 남게 됩니다. pruneJournals@tanstack/ai-sandbox에서 이를 제한합니다. 즉, journal 디렉터리를 훑어 RunStore이 터미널 상태라고 보고한 실행의 journal만 삭제하고 나머지는 모두 유지합니다. 실행 리퍼와 마찬가지로 이는 애플리케이션이 예약하는 함수입니다. Reaping & Retention을 참조하세요.

이 문장에서 "sandbox가 사라질 때까지"라는 표현은 실제로 중요한 역할을 합니다. 기능이 durableFilesystem: false를 선언한 provider(Cloudflare가 기본 제공되는 provider입니다)에서는 journal을 보관하는 파일 시스템이 컨테이너 인스턴스와 정확히 같은 기간 동안만 존재하므로 journal의 내구성은 실행이 아니라 컨테이너의 수명에 의해 제한됩니다. 이것이 Durable Runs Explained에서 설명하는 계층 경계입니다. journal만 사용하는 내구성은 sandbox 수명과 같으며, sandbox보다 오래 유지하려면 log-first 계층이 필요합니다.

지금 이를 기반으로 구축할 수 있는 것

  • 호스트가 종료되어도 실행은 계속됩니다. 에이전트에는 잃을 파이프가 없으며 출력은 sandbox 내부의 디스크에 저장됩니다.
  • sandbox 핸들과 runId를 가진 모든 프로세스는 readJournal을 사용하여 바이트 0부터 전체 실행을 다시 읽을 수 있습니다.
  • 이미 전달된 로그에 대한 재생은 정확합니다. alignToStoredLog을 통해 불일치를 숨기지 않고 표시합니다.

또한 라이브 실행을 한 호스트에서 다른 호스트로 자동 인계하는 기능도 연결되어 있습니다. sandboxRunDriver이 정렬 프리미티브를 대신 처리합니다. 후속 호스트가 lease를 통해 실행을 확보하고, 실행 레코드의 fencing epoch를 증가시켜 선행 호스트가 더 이상 추가할 수 없게 한 다음, 저장된 로그의 증가가 멈출 때까지 기다립니다. 이후 바이트 0부터 alignToStoredLog까지 journal을 재생하고 나머지만 추가합니다.

이 오케스트레이션을 직접 구현하지 마세요. alignToStoredLog은 하나의 writer가 자신의 접두사를 다시 추가하는 것만 방지하며, writer에 대해서는 아무것도 하지 않습니다. 두 호스트가 하나의 로그에 추가하는 것이 바로 위에서 설명한 중복 전달 손상이 다시 발생하는 방식이며, 여기에 대체된 호스트가 후속 호스트가 정상적으로 스트리밍 중인 실행에 터미널 상태를 기록하는 문제까지 더해집니다. lease, 로그와 실행 레코드 양쪽의 epoch fence, 그리고 정지 대기가 이러한 문제를 해결하며, 이것이 driver가 제공하는 기능입니다. Takeover & Detached Runs을 참조하세요.

함께 보기

  • Durable Runs Explained: 출력이 애초에 파일로 저장되는 이유를 일상적인 언어로, 코드 없이 설명합니다.
  • Takeover & Detached Runs: sandboxRunDriver, 연결 해제 시 분리, 그리고 단일 writer 펜싱—이 페이지의 모든 기능을 구동하는 연결 방식을 설명합니다.
  • Providers: killableProcesses 기능과 이것이 선택하는 읽기 전략을 설명합니다.
  • Harnesses: 어떤 어댑터가 journal을 기록하며, 어떻게 runId를 도출하는지 설명합니다.
  • Resumable Streams (Advanced): 오프셋 소유권과 snapshot() 읽기 정렬이 의존하는 요소를 설명합니다.
  • Custom Durability Adapter: 다음을 포함한 5개 메서드 계약과 snapshot()를 설명합니다.