Lewati ke konten
getnextpdf.com

Pro edisi

Stream

Modul Stream me-render batch dokumen secara durable dan konkuren, dengan commit lokal exactly-once ke store durable satu-host (exactly-once lintas-host adalah batas Enterprise Stream). Modul ini membagi pekerjaan menjadi dua tanggung jawab yang terpisah dengan rapi: sebuah mesin render yang mengubah manifes tervalidasi menjadi byte (dan tidak lebih dari itu), serta sekumpulan store durable — committer, checkpoint, idempotensi, dead-letter — yang memublikasikan byte tersebut secara aman dan memungkinkan suatu run dilanjutkan setelah crash tanpa memublikasikan ulang keluaran yang sudah di-commit.

Kapabilitas ini disertakan dalam NextPDF Pro (nextpdf/pro) dan diaktifkan dengan envelope lisensi tier Pro. Deployment tanpa entitlement tersebut tidak memuat kelas-kelas kapabilitas ini. Bandingkan edisi dan dapatkan lisensi.

Tidak ada flag lisensi per-fitur yang terpisah. Konkurensi (jumlah worker), ukuran batch, anggaran retry, dan backend store (in-memory versus filesystem durable) adalah parameter runtime, bukan sakelar lisensi.

Terminal window
composer require nextpdf/pro:^3

Kode berada di bawah namespace NextPDF\Pro\Stream.

Stream ditata mengelilingi sebuah seam beku — NextPDF\Pro\Stream\Engine\RenderEngineInterface — yang memisahkan mesin throughput dari semantik stream:

  • Mesin render memegang konkurensi dan memori terbatas. Mesin ini me-render satu window manifes yang sudah pra-validasi dan pra-dedup melalui renderBatch() dan mengembalikan satu EngineRenderResult per manifes, dalam urutan input. Yang krusial, mesin ini bebas efek-samping terhadap keluaran final: ia mengembalikan byte hasil render ditambah digest sha-256-nya, tidak pernah menulis ke object key final. Kemurnian itulah yang membuat pengiriman exactly-once menjadi mungkin.
  • Kolaborator stream memegang pengiriman. Committer, checkpoint store, idempotensi (dedup) store, dan dead-letter store memutuskan di mana byte mendarat, bagaimana suatu run dilanjutkan, pekerjaan mana yang merupakan replay, dan apa yang terjadi pada kegagalan terminal.

Kegagalan render per-manifes dilaporkan sebagai hasil Failed (atau Timeout) per-item; hal itu tidak pernah membatalkan batch. Envelope batch selalu berhasil dengan hasil per-item.

  • InProcessRenderEngine adalah baseline kebenaran yang sinkron dan satu-proses. Ia memvalidasi setiap manifes secara fail-closed melalui RenderManifestValidator yang sudah disertakan sebelum me-render-nya melalui Core SingleDocumentRenderer, sehingga manifes yang buruk menjadi kegagalan per-item (kode galat SPEC-MANIFEST-INVALID) alih-alih mencapai renderer.
  • ConcurrentRenderEngine menyebar sebuah batch ke RenderUnitExecutorInterface dan memulihkan urutan batch deterministik berdasarkan indeks unit. Keluaran identik secara byte dengan render sekuensial terlepas dari urutan penyelesaian; penyelesaian yang hilang, duplikat, atau tidak dikenal adalah kegagalan keras, tidak pernah pembuangan diam-diam.
  • Executor adalah seam konkurensi. InlineRenderUnitExecutor adalah baseline deterministik; ProcessPoolRenderUnitExecutor mendistribusikan sebuah batch ke hingga N subproses worker php yang me-render secara paralel, lalu mengumpulkan dan memeriksa integritas hasilnya.

OutputCommitterInterface::commit() memublikasikan byte hasil render ke tujuan finalnya tepat sekali: secara atomik (objek parsial tidak pernah teramati), idempoten (commit ulang konten yang identik secara byte tidak melakukan penulisan dan mengembalikan CommitReceipt dengan idempotentReuse = true — sebuah receipt baru, bukan yang asli), tanpa clobber diam-diam (byte yang berbeda ke key yang sudah terisi tanpa overwrite memunculkan konflik), dan terperiksa integritasnya (committer menghitung ulang digest sebelum menulis). LocalFilesystemCommitter mengimplementasikan ini untuk filesystem lokal.

RunCheckpoint adalah barrier durable yang merekam berapa banyak item yang sudah di-commit oleh suatu run ditambah snapshot state berkunci. Pada pemulihan, prosesor mempercepat-maju melewati offset yang sudah di-commit dan memulihkan state berkunci, sehingga crash di tengah run dilanjutkan tanpa memublikasikan ulang keluaran yang sudah di-commit. FilesystemCheckpointStore menyimpan setiap barrier secara atomik.

Idempotensi store adalah jalur cepat yang memungkinkan prosesor melakukan short-circuit sebelum me-render manifes yang di-replay; perbandingan digest milik committer tetap menjadi jaminan exactly-once yang durable, sehingga rekaman dedup yang hilang paling buruk hanya menyebabkan render ulang yang sia-sia yang akan didedup oleh committer. RetryPolicy menyediakan exponential backoff yang terbatas dan deterministik untuk kegagalan transien (timeout); job yang menghabiskan anggarannya ditangkap di sebuah DeadLetterStoreInterface alih-alih hilang. Setiap store menyertakan varian in-memory (cakupan single-run / pengujian) dan varian filesystem durable.

Store yang state-nya bertahan melewati restart proses mengimplementasikan marker DurableCapability. Run yang crash-safe mensyaratkan setiap kolaborator bersifat durable agar gagal cepat alih-alih menjanjikan semantik exactly-once yang tidak dapat dipertahankan oleh store in-memory melewati sebuah restart.

Render satu manifes dan commit byte-nya tepat sekali. Mesin mengembalikan byte ditambah sebuah digest; committer memublikasikannya.

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";
}

Render sebuah batch, arahkan timeout ke retry policy, dan dead-letter-kan kegagalan terminal. Commit menolak meng-clobber byte yang berbeda, sehingga tabrakan key tertangkap dan disimpan alih-alih hilang.

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");
}
  • Rendering batch bervolume tinggi di mana throughput diuntungkan oleh eksekusi konkuren (process-pool).
  • Run berdurasi panjang yang harus bertahan dari crash dan dilanjutkan tanpa memublikasikan keluaran dua kali.
  • Pipeline yang harus menjamin pengiriman exactly-once setiap dokumen hasil render ke targetnya.

Untuk satu dokumen ad-hoc, render langsung dengan modul Writer; nilai Stream terletak pada batch yang durable, resumable, dan konkuren.

Throughput menskala bersama jumlah worker di ProcessPoolRenderUnitExecutor (dibatasi oleh maxWorkers dan maxBatchSize), sementara mesin menjaga keluaran render identik secara byte dengan baseline sekuensial. Timeout wall-clock membatasi setiap batch paralel sehingga worker yang macet tidak dapat memblokir selamanya. Tidak ada angka throughput tetap yang dipublikasikan; angka itu bergantung pada kompleksitas dokumen dan paralelisme host. Ukur dengan dokumen yang representatif.

Manifes divalidasi secara fail-closed sebelum render. Committer menolak path traversal, null byte, skema stream-wrapper, target ber-symlink, dan vektor NTFS alternate-data-stream (titik dua), serta meresolusi setiap key di bawah satu root yang dikonfigurasi. Hasil worker lintas-proses di-hash ulang dan dicocokkan terhadap digest yang dilaporkan worker sehingga worker yang rusak tidak dapat merusak keluaran secara diam-diam. Modul ini tidak mencatat isi dokumen.

Store durable Stream di sini berbasis filesystem dan satu-host. Exactly-once konkuren lintas-host ke key yang sama, serta dedup durable lintas run, adalah tugas committer dan store penyimpanan-objek Enterprise; prosesor stream document-job yang menggerakkan kolaborator ini merupakan perkara Enterprise. Pro menyediakan mesin, kontrak, dan implementasi durable lokal.

Tanpa Pro, render dokumen satu per satu dengan writer NextPDF Core; batch streaming durable, eksekusi konkuren, dan commit exactly-once adalah tambahan Pro. Lihat /modules/writer/.

Halaman ini hanya mendokumentasikan perilaku yang teramati secara eksternal dan permukaan API publik yang didukung. Path namespace internal, kelas helper, tabel mekanisme, nama file runbook, dan prefiks tiket berada di luar cakupan.