Pro edizione
Stream
In sintesi
Sezione intitolata “In sintesi”Il modulo Stream esegue il rendering di batch di documenti in modo durevole e concorrente, con commit locale esattamente-una-volta verso store durevoli su host singolo (l’esattamente-una-volta cross-host è il confine di Enterprise Stream). Suddivide il lavoro in due responsabilità nettamente separate: un motore di rendering che trasforma i manifest convalidati in byte (e nient’altro) e un insieme di store durevoli — committer, checkpoint, idempotenza, dead-letter — che pubblicano quei byte in modo sicuro e consentono a un’esecuzione di riprendere dopo un crash senza ripubblicare l’output già committato.
Disponibilità e licenze
Sezione intitolata “Disponibilità e licenze”Questa funzionalità è inclusa in NextPDF Pro (nextpdf/pro) e si attiva con un envelope di licenza di livello Pro. Un deployment privo di tale entitlement non carica le classi della funzionalità. Confronta le edizioni e ottieni una licenza.
Non esiste un flag di licenza separato per singola funzionalità. La concorrenza (numero di worker), la dimensione del batch, il budget dei retry e il backend di store (in memoria rispetto a filesystem durevole) sono parametri di runtime, non interruttori di licenza.
Installazione
Sezione intitolata “Installazione”composer require nextpdf/pro:^3Il codice risiede sotto il namespace NextPDF\Pro\Stream.
Panoramica concettuale
Sezione intitolata “Panoramica concettuale”Stream è organizzato attorno a una giuntura congelata — NextPDF\Pro\Stream\Engine\RenderEngineInterface — che separa il motore di throughput dalla semantica dello stream:
- Il motore di rendering possiede la concorrenza e la memoria limitata. Esegue il rendering di una finestra di manifest pre-convalidati e pre-deduplicati tramite
renderBatch()e restituisce unEngineRenderResultper ogni manifest, nell’ordine di input. Fondamentale: il motore è privo di effetti collaterali rispetto all’output finale: restituisce i byte renderizzati più il loro digest sha-256, senza mai scrivere su una chiave oggetto finale. È questa purezza a rendere possibile il recapito esattamente-una-volta. - I collaboratori dello stream possiedono il recapito. Il committer, lo store di checkpoint, lo store di idempotenza (deduplica) e lo store di dead-letter decidono dove approdano i byte, come riprende un’esecuzione, quale lavoro è un replay e cosa accade ai fallimenti terminali.
Un fallimento di rendering per singolo manifest viene riportato come risultato Failed (o Timeout) per singolo elemento; non interrompe mai il batch. L’involucro del batch ha sempre esito positivo, con risultati per singolo elemento.
Concetti chiave
Sezione intitolata “Concetti chiave”Motori di rendering ed esecutori
Sezione intitolata “Motori di rendering ed esecutori”InProcessRenderEngineè la baseline di correttezza sincrona e a processo singolo. Convalida ogni manifest fail-closed tramite ilRenderManifestValidatorincluso prima di eseguirne il rendering attraverso ilSingleDocumentRendererdel Core, così un manifest non valido diventa un fallimento per singolo elemento (codice di erroreSPEC-MANIFEST-INVALID) anziché raggiungere il renderer.ConcurrentRenderEnginedistribuisce un batch a unRenderUnitExecutorInterfacee ripristina l’ordine deterministico del batch tramite l’indice di unità. L’output è identico byte per byte a un rendering sequenziale, a prescindere dall’ordine di completamento; un completamento mancante, duplicato o sconosciuto è un fallimento netto, mai uno scarto silenzioso.- Gli esecutori sono la giuntura di concorrenza.
InlineRenderUnitExecutorè la baseline deterministica;ProcessPoolRenderUnitExecutordistribuisce un batch su un massimo di N sottoprocessi workerphpche eseguono il rendering in parallelo, quindi raccoglie e verifica l’integrità dei loro risultati.
Commit durevole e privo di effetti collaterali
Sezione intitolata “Commit durevole e privo di effetti collaterali”OutputCommitterInterface::commit() pubblica i byte renderizzati nella loro destinazione finale esattamente una volta: in modo atomico (non viene mai osservato alcun oggetto parziale), idempotente (ri-committare un contenuto identico byte per byte non esegue alcuna scrittura e restituisce un CommitReceipt con idempotentReuse = true — una ricevuta nuova, non quella originale), senza sovrascritture silenziose (byte divergenti verso una chiave occupata senza overwrite solleva un conflitto) e con verifica di integrità (il committer ricalcola il digest prima di scrivere). LocalFilesystemCommitter lo implementa per il filesystem locale.
Ripristino da checkpoint
Sezione intitolata “Ripristino da checkpoint”Un RunCheckpoint è una barriera durevole che registra quanti elementi un’esecuzione ha committato più uno snapshot dello stato con chiave. Al ripristino il processore avanza rapidamente oltre l’offset committato e ripristina lo stato con chiave, così un crash a metà esecuzione riprende senza ripubblicare l’output già committato. FilesystemCheckpointStore persiste ogni barriera in modo atomico.
Deduplica per idempotenza, retry e dead-letter
Sezione intitolata “Deduplica per idempotenza, retry e dead-letter”Lo store di idempotenza è il percorso rapido che consente al processore di prendere una scorciatoia prima di eseguire il rendering di un manifest replayed; il confronto del digest da parte del committer rimane la garanzia durevole di esattamente-una-volta, quindi un record di deduplica perso causa nel peggiore dei casi un re-rendering sprecato che il committer deduplica. RetryPolicy fornisce un backoff esponenziale limitato e deterministico per i fallimenti transitori (timeout); un job che esaurisce il proprio budget viene catturato in un DeadLetterStoreInterface anziché andare perduto. Ogni store distribuisce una variante in memoria (ambito di esecuzione singola / test) e una variante durevole su filesystem.
Marcatore di durabilità
Sezione intitolata “Marcatore di durabilità”Gli store il cui stato sopravvive al riavvio di un processo implementano il marcatore DurableCapability. Un’esecuzione a prova di crash richiede che ogni collaboratore sia durevole, così fallisce rapidamente anziché promettere una semantica di esattamente-una-volta che uno store in memoria non può mantenere attraverso un riavvio.
Esempio di codice — Avvio rapido
Sezione intitolata “Esempio di codice — Avvio rapido”Esegue il rendering di un manifest e ne committa i byte esattamente una volta. Il motore restituisce i byte più un digest; il committer li pubblica.
<?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";}Esempio di codice — Produzione
Sezione intitolata “Esempio di codice — Produzione”Esegue il rendering di un batch, instrada i timeout verso la retry policy e invia i fallimenti terminali in dead-letter. Il commit rifiuta di sovrascrivere byte divergenti, così una collisione di chiave viene intercettata e catturata anziché andare perduta.
<?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");}Quando usarlo
Sezione intitolata “Quando usarlo”- Rendering batch ad alto volume in cui il throughput trae vantaggio dall’esecuzione concorrente (process pool).
- Esecuzioni di lunga durata che devono sopravvivere a un crash e riprendere senza ripubblicare l’output.
- Pipeline che devono garantire il recapito esattamente-una-volta di ogni documento renderizzato verso la sua destinazione.
Per un singolo documento ad-hoc, eseguire il rendering direttamente con il modulo Writer; il valore di Stream sta nei batch durevoli, ripristinabili e concorrenti.
Prestazioni
Sezione intitolata “Prestazioni”Il throughput scala con il numero di worker in ProcessPoolRenderUnitExecutor (limitato da maxWorkers e maxBatchSize), mentre il motore mantiene l’output di rendering identico byte per byte alla baseline sequenziale. Un timeout di tempo reale limita ogni batch parallelo, così un worker bloccato non può bloccare per sempre. Non esiste una cifra di throughput fissa pubblicata; dipende dalla complessità del documento e dal parallelismo dell’host. Misurare con documenti rappresentativi.
Note sulla sicurezza
Sezione intitolata “Note sulla sicurezza”I manifest vengono convalidati fail-closed prima del rendering. Il committer rifiuta il path traversal, i byte null, gli schemi di stream-wrapper, le destinazioni con symlink e i vettori di NTFS alternate-data-stream (i due punti), e risolve ogni chiave sotto un’unica radice configurata. I risultati dei worker tra processi vengono ri-hashati e confrontati con il digest riportato dal worker, così un worker corrotto non può alterare l’output silenziosamente. Questo modulo non registra alcun contenuto del documento.
Nota sul confine Enterprise
Sezione intitolata “Nota sul confine Enterprise”Gli store durevoli di Stream qui sono basati su filesystem e a host singolo. L’esattamente-una-volta concorrente cross-host verso la stessa chiave e la deduplica durevole tra le esecuzioni sono compito dei committer e degli store di object storage di Enterprise; il processore di stream per i job documentali che pilota questi collaboratori è una competenza Enterprise. Pro fornisce il motore, i contratti e le implementazioni durevoli locali.
Fallback / alternativa di Core
Sezione intitolata “Fallback / alternativa di Core”Senza Pro, eseguire il rendering dei documenti uno alla volta con il writer di NextPDF Core; lo streaming batch durevole, l’esecuzione concorrente e il commit esattamente-una-volta sono aggiunte di Pro. Vedere /modules/writer/.
Confine di pubblicazione
Sezione intitolata “Confine di pubblicazione”Questa pagina documenta soltanto il comportamento osservabile esternamente e la superficie di API pubblica supportata. I percorsi di namespace interni, le classi helper, le tabelle dei meccanismi, i nomi dei file di runbook e i prefissi dei ticket sono fuori ambito.