コンテンツにスキップ
getnextpdf.com

Pro エディション

Stream

Stream モジュールは、ドキュメントのバッチを永続的かつ並行してレンダリングし、単一ホストの永続ストアへのローカルでの exactly-once コミットを行います(クロスホストの exactly-once は Enterprise Stream の境界です)。作業を 2 つの明確に分離された責務に分割します。検証済みのマニフェストをバイト列に変換する(そしてそれ以外のことは行わない)レンダーエンジンと、それらのバイト列を安全に公開し、コミット済みの出力を再公開することなくクラッシュ後に実行を再開できるようにする一連の永続ストア(コミッター、チェックポイント、冪等性、デッドレター)です。

この機能は NextPDF Pronextpdf/pro)に同梱され、Pro ティアのライセンスエンベロープで有効化されます。そのエンタイトルメントを持たないデプロイメントでは、機能のクラスはロードされません。エディションを比較してライセンスを取得する

機能ごとの個別のライセンスフラグはありません。並行性(ワーカー数)、バッチサイズ、リトライ予算、ストアバックエンド(インメモリか永続ファイルシステムか)は、ライセンスのスイッチではなくランタイムのパラメーターです。

Terminal window
composer require nextpdf/pro:^3

コードは NextPDF\Pro\Stream 名前空間の下にあります。

Stream は凍結されたシーム — NextPDF\Pro\Stream\Engine\RenderEngineInterface — を中心に構成されており、これがスループットエンジンとストリームのセマンティクスを分離します。

  • レンダーエンジンは並行性と境界付きメモリを所有します。 事前検証済み・事前重複排除済みのマニフェストのウィンドウを renderBatch() を通じてレンダリングし、マニフェストごとに 1 つの EngineRenderResult を入力順で返します。重要なのは、エンジンが最終出力に対して副作用を持たない点です。レンダリングされたバイト列とその sha-256 ダイジェストを返すだけで、最終的なオブジェクトキーへは決して書き込みません。この純粋性こそが exactly-once 配信を可能にします。
  • ストリームのコラボレーターが配信を所有します。 コミッター、チェックポイントストア、冪等性(重複排除)ストア、デッドレターストアが、バイト列の到達先、実行の再開方法、どの作業がリプレイか、終端的な失敗をどう扱うかを決定します。

マニフェスト単位のレンダー失敗は、項目ごとの Failed(または Timeout)の結果として報告されます。それがバッチを中断させることは決してありません。バッチのエンベロープは常に成功し、項目ごとの結果を伴います。

  • InProcessRenderEngine は、同期的・単一プロセスの正しさのベースラインです。各マニフェストを、出荷されている RenderManifestValidator を通じてフェイルクローズドで検証してから、Core の SingleDocumentRenderer を通じてレンダリングします。これにより、不正なマニフェストはレンダラーに到達する代わりに項目ごとの失敗(エラーコード SPEC-MANIFEST-INVALID)になります。
  • ConcurrentRenderEngine は、バッチを RenderUnitExecutorInterface へファンアウトし、ユニットインデックスによって決定論的なバッチ順序を復元します。出力は、完了順序に関係なくシーケンシャルなレンダリングとバイト単位で同一です。欠落、重複、または不明な完了は、サイレントなドロップではなく必ずハードな失敗になります。
  • エグゼキューターは並行性のシームです。InlineRenderUnitExecutor は決定論的なベースラインです。ProcessPoolRenderUnitExecutor は、バッチを最大 N 個の php ワーカーサブプロセスに分散し、それらが並列でレンダリングしてから、結果を収集して整合性をチェックします。

OutputCommitterInterface::commit() は、レンダリングされたバイト列をその最終的な宛先へちょうど一度だけ公開します。アトミックに(部分的なオブジェクトが観測されることは決してありません)、冪等に(バイト単位で同一のコンテンツを再コミットしても書き込みは行われず、idempotentReuse = true を持つ CommitReceipt を返します。これは元のものではなく、新しいレシートです)、サイレントな上書きなしに(overwrite なしで使用中のキーに異なるバイト列が来た場合は競合を発生させます)、そして整合性をチェックしたうえで(コミッターは書き込み前にダイジェストを再計算します)行います。LocalFilesystemCommitter がこれをローカルファイルシステム向けに実装します。

RunCheckpoint は、実行がコミットした項目数とキー付き状態のスナップショットを記録する永続的なバリアです。復旧時、プロセッサーはコミット済みのオフセットを早送りで通過し、キー付き状態を復元するため、実行の途中でクラッシュしてもコミット済みの出力を再公開することなく再開します。FilesystemCheckpointStore は各バリアをアトミックに永続化します。

冪等性ストアは、リプレイされたマニフェストをレンダリングする前にプロセッサーがショートカットできるようにするファストパスです。コミッターのダイジェスト比較が永続的な exactly-once 保証であり続けるため、重複排除レコードが失われても、最悪の場合に発生するのは無駄な再レンダリングであり、それはコミッターが重複排除します。RetryPolicy は、一時的な(タイムアウト)失敗に対して、境界付きで決定論的な指数バックオフを提供します。予算を使い果たしたジョブは、失われるのではなく DeadLetterStoreInterface に捕捉されます。各ストアは、インメモリのバリアント(単一実行/テストスコープ)と、永続的なファイルシステムのバリアントを出荷します。

プロセスの再起動を越えて状態が存続するストアは、DurableCapability マーカーを実装します。クラッシュに強い実行には、すべてのコラボレーターが永続的であることが求められます。そうすることで、インメモリストアが再起動を越えて維持できない exactly-once セマンティクスを約束する代わりに、すばやく失敗します。

1 つのマニフェストをレンダリングし、そのバイト列をちょうど一度だけコミットします。エンジンはバイト列とダイジェストを返し、コミッターがそれらを公開します。

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");
}
  • スループットが並行(プロセスプール)実行から恩恵を受ける、大量バッチレンダリング。
  • クラッシュを乗り越えて、出力を二重公開せずに再開する必要がある長時間実行。
  • レンダリングされた各ドキュメントをそのターゲットへ exactly-once で配信することを保証する必要があるパイプライン。

単発のアドホックなドキュメントには、Writer モジュールで直接レンダリングしてください。Stream の価値は、永続的・再開可能・並行なバッチにあります。

スループットは ProcessPoolRenderUnitExecutor のワーカー数に応じてスケールし(maxWorkersmaxBatchSize によって境界付けられます)、エンジンはレンダー出力をシーケンシャルなベースラインとバイト単位で同一に保ちます。実時間タイムアウトが各並列バッチに上限を設けるため、ハングしたワーカーが永久にブロックすることはありません。公表された固定のスループット値はありません。ドキュメントの複雑さとホストの並列度に依存します。代表的なドキュメントで測定してください。

マニフェストはレンダリング前にフェイルクローズドで検証されます。コミッターは、パストラバーサル、null バイト、ストリームラッパースキーム、シンボリックリンクされたターゲット、NTFS の代替データストリーム(コロン)ベクターを拒否し、すべてのキーを 1 つの構成済みルートの下で解決します。プロセス間のワーカー結果は再ハッシュされ、ワーカーが報告したダイジェストと照合されるため、文字化けしたワーカーが出力をサイレントに破損させることはありません。このモジュールはドキュメントの内容をログに記録しません。

ここでの Stream の永続ストアはファイルシステムベースかつ単一ホストです。同一キーに対するクロスホストの並行 exactly-once と、実行をまたぐ永続的な重複排除は、Enterprise のオブジェクトストレージコミッターおよびストアの役割です。これらのコラボレーターを駆動するドキュメントジョブのストリームプロセッサーは Enterprise の領域です。Pro は、エンジン、コントラクト、そしてローカルの永続実装を提供します。

Pro がない場合は、NextPDF Core のライターでドキュメントを 1 つずつレンダリングしてください。永続的なバッチストリーミング、並行実行、exactly-once コミットは Pro の追加機能です。/modules/writer/を参照してください。

このページは、外部から観測可能な動作とサポートされる公開 API サーフェスのみを記述します。内部の名前空間パス、ヘルパークラス、メカニズムの表、ランブックのファイル名、チケットのプレフィックスは対象外です。