콘텐츠로 이동
getnextpdf.com

Pro 에디션

Stream

Stream 모듈은 문서 배치를 내구성 있게, 동시에, 단일 호스트 내구성 저장소에 대한 정확히 한 번 로컬 커밋으로 렌더링합니다(호스트 간 정확히 한 번은 Enterprise Stream의 경계입니다). 작업을 명확하게 분리된 두 가지 책임으로 나눕니다. 검증된 매니페스트를 바이트로 변환하는(그리고 그 외에는 아무것도 하지 않는) 렌더 엔진과, 그 바이트를 안전하게 발행하고 커밋된 출력을 다시 발행하지 않은 채로 크래시 후 실행을 재개하게 해 주는 일련의 내구성 저장소 — 커미터, 체크포인트, 멱등성, 데드레터 — 입니다.

이 기능은 NextPDF Pro(nextpdf/pro)에 포함되어 있으며 Pro 티어 라이선스 엔벨로프로 활성화됩니다. 해당 엔타이틀먼트가 없는 배포에서는 이 기능의 클래스가 로드되지 않습니다. 에디션을 비교하고 라이선스 받기.

별도의 기능별 라이선스 플래그는 없습니다. 동시성(워커 수), 배치 크기, 재시도 예산, 그리고 저장소 백엔드(인메모리 대 내구성 파일 시스템)는 라이선스 스위치가 아니라 런타임 매개변수입니다.

Terminal window
composer require nextpdf/pro:^3

코드는 NextPDF\Pro\Stream 네임스페이스 아래에 있습니다.

Stream은 고정된 이음새 — NextPDF\Pro\Stream\Engine\RenderEngineInterface — 를 중심으로 구성되며, 이 이음새는 처리량 엔진을 스트림 시맨틱으로부터 분리합니다.

  • 렌더 엔진은 동시성과 제한된 메모리를 소유합니다. 사전 검증되고 사전 중복 제거된 매니페스트의 윈도우를 renderBatch()를 통해 렌더링하고, 입력 순서대로 매니페스트당 하나의 EngineRenderResult를 반환합니다. 결정적으로 엔진은 최종 출력에 대해 부작용이 없습니다. 렌더링된 바이트와 그 sha-256 다이제스트를 반환할 뿐, 최종 객체 키에는 절대 쓰지 않습니다. 그 순수성이 바로 정확히 한 번 전달을 가능하게 합니다.
  • 스트림 협력자들은 전달을 소유합니다. 커미터, 체크포인트 저장소, 멱등성(중복 제거) 저장소, 데드레터 저장소가 바이트가 어디에 안착할지, 실행이 어떻게 재개되는지, 어떤 작업이 재생(replay)인지, 그리고 종단 실패에 무슨 일이 일어나는지를 결정합니다.

매니페스트당 렌더 실패는 항목별 Failed(또는 Timeout) 결과로 보고됩니다. 배치를 절대 중단시키지 않습니다. 배치 엔벨로프는 항상 항목별 결과와 함께 성공합니다.

  • InProcessRenderEngine은 동기식 단일 프로세스 정확성 기준선입니다. 각 매니페스트를 렌더링하기 전에 함께 제공되는 RenderManifestValidator를 통해 실패-차단으로 검증한 다음 Core의 SingleDocumentRenderer를 통해 렌더링하므로, 잘못된 매니페스트는 렌더러에 도달하는 대신 항목별 실패(오류 코드 SPEC-MANIFEST-INVALID)가 됩니다.
  • ConcurrentRenderEngine은 배치를 RenderUnitExecutorInterface로 분산시킨 뒤 유닛 인덱스로 결정적 배치 순서를 복원합니다. 완료 순서와 무관하게 출력은 순차 렌더와 바이트 단위로 동일합니다. 누락되거나 중복되거나 알 수 없는 완료는 조용한 누락이 아니라 하드 실패입니다.
  • 익스큐터는 동시성 이음새입니다. InlineRenderUnitExecutor는 결정적 기준선입니다. ProcessPoolRenderUnitExecutor는 배치를 최대 N개의 php 워커 하위 프로세스에 분산하여 병렬로 렌더링한 다음, 그 결과를 수집하고 무결성을 검사합니다.

OutputCommitterInterface::commit()은 렌더링된 바이트를 최종 목적지에 정확히 한 번 발행합니다. 원자적으로(부분 객체는 절대 관찰되지 않음), 멱등적으로(바이트 단위로 동일한 콘텐츠를 다시 커밋하면 쓰기가 수행되지 않고 idempotentReuse = trueCommitReceipt를 반환합니다 — 원본이 아니라 새 영수증입니다), 조용한 덮어쓰기 없이(overwrite 없이 점유된 키에 상이한 바이트를 쓰면 충돌을 발생시킴), 그리고 무결성 검사를 거쳐(커미터가 쓰기 전에 다이제스트를 다시 계산함) 발행합니다. LocalFilesystemCommitter가 이를 로컬 파일 시스템에 대해 구현합니다.

RunCheckpoint는 실행이 커밋한 항목 수와 키 상태(keyed state)의 스냅샷을 기록하는 내구성 있는 배리어입니다. 복구 시 프로세서는 커밋된 오프셋을 지나 빨리 감기를 수행하고 키 상태를 복원하므로, 실행 도중의 크래시는 커밋된 출력을 다시 발행하지 않은 채로 재개됩니다. FilesystemCheckpointStore는 각 배리어를 원자적으로 영속화합니다.

멱등성 중복 제거, 재시도, 데드레터

섹션 제목: “멱등성 중복 제거, 재시도, 데드레터”

멱등성 저장소는 프로세서가 재생된 매니페스트를 렌더링하기 전에 단락(short-circuit)할 수 있게 하는 빠른 경로입니다. 커미터의 다이제스트 비교가 내구성 있는 정확히 한 번 보장으로 남아 있으므로, 중복 제거 레코드가 손실되더라도 최악의 경우 커미터가 중복 제거하는 낭비된 재렌더링만 야기합니다. RetryPolicy는 일시적(타임아웃) 실패에 대해 제한적이고 결정적인 지수 백오프를 제공합니다. 예산을 소진한 작업은 손실되는 대신 DeadLetterStoreInterface에 캡처됩니다. 각 저장소는 인메모리 변형(단일 실행 / 테스트 범위)과 내구성 있는 파일 시스템 변형을 함께 제공합니다.

프로세스 재시작에도 상태가 살아남는 저장소는 DurableCapability 마커를 구현합니다. 크래시에 안전한 실행은 모든 협력자가 내구성을 갖추기를 요구하므로, 인메모리 저장소가 재시작에 걸쳐 유지할 수 없는 정확히 한 번 시맨틱을 약속하는 대신 빠르게 실패합니다.

매니페스트 하나를 렌더링하고 그 바이트를 정확히 한 번 커밋합니다. 엔진은 바이트와 다이제스트를 반환하고, 커미터가 그것을 발행합니다.

stream-quickstart.php
<?php
declare(strict_types=1);
use NextPDF\Manifest\OutputObjectKey;
use NextPDF\Manifest\Render\SingleDocumentRenderer;
use NextPDF\Manifest\RenderManifestBuilder;
use NextPDF\Manifest\TemplateRef;
use NextPDF\Pro\Stream\Commit\LocalFilesystemCommitter;
use NextPDF\Pro\Stream\Engine\InProcessRenderEngine;
$outputRoot = __DIR__ . '/out';
\is_dir($outputRoot) || \mkdir($outputRoot, 0o775, true);
// The engine renders bytes only — it never writes the final object.
$engine = new InProcessRenderEngine(SingleDocumentRenderer::standalone());
$target = OutputObjectKey::file('out', 'invoices/1001.pdf');
$manifest = RenderManifestBuilder::create('invoice-1001')
->withInlineInput('<h1>Invoice 1001</h1><p>Amount due: 42.00</p>')
->withTemplate(TemplateRef::html())
->withOutputKey($target)
->build();
$result = $engine->renderBatch([$manifest])[0];
// A durable committer publishes the rendered bytes exactly once.
$committer = new LocalFilesystemCommitter($outputRoot);
if ($result->isRendered()) {
$receipt = $committer->commit($result->jobId, $target, $result->bytes, $result->sha256);
echo $receipt->target->toUri(), ' (', $receipt->bytesWritten, " bytes)\n";
}

배치를 렌더링하고, 타임아웃을 재시도 정책으로 라우팅하며, 종단 실패를 데드레터로 보냅니다. 커밋은 상이한 바이트로의 덮어쓰기를 거부하므로, 키 충돌은 손실되는 대신 포착되어 캡처됩니다.

stream-production.php
<?php
declare(strict_types=1);
use DateTimeImmutable;
use NextPDF\Manifest\OutputObjectKey;
use NextPDF\Manifest\Render\SingleDocumentRenderer;
use NextPDF\Manifest\RenderManifest;
use NextPDF\Manifest\RenderManifestBuilder;
use NextPDF\Manifest\TemplateRef;
use NextPDF\Pro\Stream\Commit\LocalFilesystemCommitter;
use NextPDF\Pro\Stream\Engine\EngineRenderStatus;
use NextPDF\Pro\Stream\Engine\InProcessRenderEngine;
use NextPDF\Pro\Stream\Exception\OutputCommitConflictException;
use NextPDF\Pro\Stream\Retry\DeadLetterRecord;
use NextPDF\Pro\Stream\Retry\InMemoryDeadLetterStore;
use NextPDF\Pro\Stream\Retry\RetryPolicy;
$outputRoot = __DIR__ . '/out';
\is_dir($outputRoot) || \mkdir($outputRoot, 0o775, true);
$engine = new InProcessRenderEngine(SingleDocumentRenderer::standalone(), maxBatchSize: 64);
$committer = new LocalFilesystemCommitter($outputRoot);
$deadLetter = new InMemoryDeadLetterStore();
$retry = RetryPolicy::default(); // 3 attempts, 100ms base, 30s cap.
/**
* Build one manifest and remember its output target for the commit stage.
*
* @return array{RenderManifest, OutputObjectKey}
*/
$makeJob = static function (string $jobId, string $html): array {
$target = OutputObjectKey::file('out', 'invoices/' . $jobId . '.pdf');
$manifest = RenderManifestBuilder::create($jobId)
->withInlineInput($html)
->withTemplate(TemplateRef::html())
->withOutputKey($target)
->build();
return [$manifest, $target];
};
/** @var array<non-empty-string, OutputObjectKey> $targets */
$targets = [];
$manifests = [];
foreach (['inv-2001' => '<h1>2001</h1>', 'inv-2002' => '<h1>2002</h1>'] as $id => $html) {
[$manifest, $target] = $makeJob($id, $html);
$manifests[] = $manifest;
$targets[$id] = $target;
}
foreach ($engine->renderBatch($manifests) as $result) {
// A timeout is transient — the policy decides whether to re-enqueue it.
if ($result->status === EngineRenderStatus::Timeout && $retry->shouldRetry(1)) {
// Re-enqueue on the caller's work queue after delayMsForAttempt(1) ms.
continue;
}
if (!$result->isRendered()) {
$deadLetter->add(new DeadLetterRecord(
jobId: $result->jobId,
idempotencyKeyValue: $result->jobId,
attempts: $retry->maxAttempts,
lastErrorCode: $result->errorCode ?? 'SPEC-RENDER-EXCEPTION',
lastErrorMessage: $result->errorMessage ?? '',
failedAt: new DateTimeImmutable(),
));
continue;
}
try {
// overwrite=false: identical bytes are an idempotent no-op; divergent
// bytes to an occupied key raise SPEC-COMMIT-409 instead of clobbering.
$receipt = $committer->commit(
$result->jobId,
$targets[$result->jobId],
$result->bytes,
$result->sha256,
);
} catch (OutputCommitConflictException $e) {
$deadLetter->add(new DeadLetterRecord(
jobId: $result->jobId,
idempotencyKeyValue: $result->jobId,
attempts: 1,
lastErrorCode: $e->specCode(),
lastErrorMessage: $e->getMessage(),
failedAt: new DateTimeImmutable(),
));
continue;
}
echo $receipt->idempotentReuse
? "reused {$receipt->target->toUri()}\n"
: "committed {$receipt->target->toUri()}\n";
}
if ($deadLetter->count() > 0) {
\fwrite(\STDERR, $deadLetter->count() . " job(s) dead-lettered\n");
}
  • 동시(프로세스 풀) 실행으로 처리량이 향상되는 대용량 배치 렌더링.
  • 크래시를 견디고 출력을 이중 발행하지 않은 채로 재개해야 하는 장기 실행 작업.
  • 렌더링된 각 문서를 대상에 정확히 한 번 전달하도록 보장해야 하는 파이프라인.

단일 임시 문서의 경우 Writer 모듈로 직접 렌더링하십시오. Stream의 가치는 내구성 있고 재개 가능하며 동시적인 배치에 있습니다.

처리량은 ProcessPoolRenderUnitExecutor의 워커 수에 따라 확장되며(maxWorkersmaxBatchSize로 제한됨), 엔진은 렌더 출력을 순차 기준선과 바이트 단위로 동일하게 유지합니다. 실제 시간 타임아웃이 각 병렬 배치를 제한하므로 멈춘 워커가 영원히 차단할 수 없습니다. 발행된 고정 처리량 수치는 없습니다. 문서 복잡도와 호스트 병렬성에 따라 달라집니다. 대표적인 문서로 측정하십시오.

매니페스트는 렌더링 전에 실패-차단으로 검증됩니다. 커미터는 경로 순회, 널 바이트, 스트림 래퍼 스킴, 심볼릭 링크된 대상, 그리고 NTFS 대체 데이터 스트림(콜론) 벡터를 거부하고, 모든 키를 하나의 구성된 루트 아래에서 해석합니다. 프로세스 간 워커 결과는 다시 해싱되어 워커가 보고한 다이제스트와 대조되므로, 손상된 워커가 출력을 조용히 망가뜨릴 수 없습니다. 이 모듈은 문서 콘텐츠를 로깅하지 않습니다.

여기서 Stream의 내구성 저장소는 파일 시스템 기반이며 단일 호스트입니다. 동일한 키에 대한 호스트 간 동시 정확히 한 번, 그리고 실행 간 내구성 있는 중복 제거는 Enterprise 객체 스토리지 커미터와 저장소의 몫입니다. 이러한 협력자를 구동하는 문서-작업 스트림 프로세서는 Enterprise의 관심사입니다. Pro는 엔진, 계약, 그리고 로컬 내구성 구현을 제공합니다.

Pro 없이는 NextPDF Core의 writer로 문서를 한 번에 하나씩 렌더링하십시오. 내구성 있는 배치 스트리밍, 동시 실행, 그리고 정확히 한 번 커밋은 Pro 추가 기능입니다. /modules/writer/를 참조하십시오.

이 페이지는 외부에서 관찰 가능한 동작과 지원되는 공개 API 표면만 문서화합니다. 내부 네임스페이스 경로, 헬퍼 클래스, 메커니즘 표, 런북 파일명, 그리고 티켓 접두어는 범위를 벗어납니다.