Pro Edition
Stream
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“Das Stream-Modul rendert Batches von Dokumenten dauerhaft und nebenläufig, mit Exactly-once-Commit lokal in Single-Host-taugliche dauerhafte Stores (Cross-Host-Exactly-once ist die Grenze von Enterprise Stream). Es teilt die Arbeit in zwei sauber getrennte Verantwortlichkeiten auf: eine Render-Engine, die validierte Manifeste in Bytes verwandelt (und nichts anderes), sowie einen Satz dauerhafter Stores — Committer, Checkpoint, Idempotenz, Dead-Letter —, die diese Bytes sicher veröffentlichen und einen Lauf nach einem Absturz fortsetzen lassen, ohne bereits committete Ausgabe erneut zu veröffentlichen.
Verfügbarkeit und Lizenzierung
Abschnitt betitelt „Verfügbarkeit und Lizenzierung“Diese Funktion ist in NextPDF Pro (nextpdf/pro) enthalten und wird mit einem Lizenz-Envelope der Pro-Stufe aktiviert. Eine Bereitstellung ohne diese Berechtigung lädt die Klassen der Funktion nicht. Editionen vergleichen und Lizenz erwerben.
Es gibt kein separates per-Feature-Lizenz-Flag. Nebenläufigkeit (Worker-Anzahl), Batch-Größe, Wiederholungsbudget und Store-Backend (in-memory versus dauerhaftes Dateisystem) sind Laufzeitparameter, keine Lizenzschalter.
Installation
Abschnitt betitelt „Installation“composer require nextpdf/pro:^3Der Code liegt unter dem Namespace NextPDF\Pro\Stream.
Konzeptioneller Überblick
Abschnitt betitelt „Konzeptioneller Überblick“Stream ist um eine eingefrorene Naht herum organisiert — NextPDF\Pro\Stream\Engine\RenderEngineInterface —, die die Durchsatz-Engine von der Stream-Semantik trennt:
- Die Render-Engine besitzt Nebenläufigkeit und beschränkten Speicher. Sie rendert ein Fenster vorvalidierter, vorab deduplizierter Manifeste über
renderBatch()und gibt einEngineRenderResultpro Manifest in Eingabereihenfolge zurück. Entscheidend ist, dass die Engine nebenwirkungsfrei in Bezug auf die endgültige Ausgabe ist: Sie gibt gerenderte Bytes plus deren sha-256-Digest zurück und schreibt niemals in einen endgültigen Objektschlüssel. Genau diese Reinheit macht Exactly-once-Delivery möglich. - Die Stream-Kollaborateure besitzen die Delivery. Der Committer, der Checkpoint-Store, der Idempotenz-Store (Dedup) und der Dead-Letter-Store entscheiden, wo Bytes landen, wie ein Lauf fortgesetzt wird, welche Arbeit ein Replay ist und was mit terminalen Fehlern geschieht.
Ein Render-Fehler pro Manifest wird als Failed- (oder Timeout-)Ergebnis pro Item gemeldet; er bricht den Batch niemals ab. Der Batch-Envelope ist immer erfolgreich, mit Ergebnissen pro Item.
Schlüsselkonzepte
Abschnitt betitelt „Schlüsselkonzepte“Render-Engines und Executors
Abschnitt betitelt „Render-Engines und Executors“InProcessRenderEngineist die synchrone Single-Process-Korrektheitsbasis. Sie validiert jedes Manifest fail-closed über den mitgeliefertenRenderManifestValidator, bevor sie es über den Core-SingleDocumentRendererrendert, sodass ein fehlerhaftes Manifest zu einem Fehler pro Item wird (FehlercodeSPEC-MANIFEST-INVALID), statt den Renderer zu erreichen.ConcurrentRenderEnginefächert einen Batch an einRenderUnitExecutorInterfaceauf und stellt die deterministische Batch-Reihenfolge anhand des Unit-Index wieder her. Die Ausgabe ist byte-gleich zu einem sequentiellen Render, unabhängig von der Abschlussreihenfolge; ein fehlender, doppelter oder unbekannter Abschluss ist ein harter Fehler, niemals ein stiller Verlust.- Executors sind die Nebenläufigkeitsnaht.
InlineRenderUnitExecutorist die deterministische Basis;ProcessPoolRenderUnitExecutorverteilt einen Batch auf bis zu Nphp-Worker-Subprozesse, die parallel rendern, und sammelt anschließend deren Ergebnisse und prüft sie auf Integrität.
Dauerhafter, nebenwirkungsfreier Commit
Abschnitt betitelt „Dauerhafter, nebenwirkungsfreier Commit“OutputCommitterInterface::commit() veröffentlicht gerenderte Bytes exactly-once an ihr endgültiges Ziel: atomar (es wird niemals ein partielles Objekt beobachtet), idempotent (ein erneutes Committen byte-gleichen Inhalts führt keinen Schreibvorgang durch und gibt ein CommitReceipt mit idempotentReuse = true zurück — eine frische Quittung, nicht das Original), ohne stilles Überschreiben (abweichende Bytes auf einen belegten Schlüssel ohne overwrite lösen einen Konflikt aus) und integritätsgeprüft (der Committer berechnet den Digest vor dem Schreiben neu). Der LocalFilesystemCommitter implementiert dies für das lokale Dateisystem.
Checkpoint-Wiederherstellung
Abschnitt betitelt „Checkpoint-Wiederherstellung“Ein RunCheckpoint ist eine dauerhafte Barriere, die festhält, wie viele Items ein Lauf committet hat, plus einen Snapshot des schlüsselbasierten Zustands. Bei der Wiederherstellung spult der Prozessor über das committete Offset hinaus vor und stellt den schlüsselbasierten Zustand wieder her, sodass ein Absturz mitten im Lauf fortgesetzt wird, ohne committete Ausgabe erneut zu veröffentlichen. FilesystemCheckpointStore persistiert jede Barriere atomar.
Idempotenz-Deduplizierung, Wiederholung und Dead-Letter
Abschnitt betitelt „Idempotenz-Deduplizierung, Wiederholung und Dead-Letter“Der Idempotenz-Store ist der schnelle Pfad, der es dem Prozessor erlaubt, vor dem Rendern eines erneut abgespielten Manifests einen Kurzschluss zu nehmen; der Digest-Vergleich des Committers bleibt die dauerhafte Exactly-once-Garantie, sodass ein verlorener Dedup-Eintrag schlimmstenfalls ein vergebliches erneutes Rendern verursacht, das der Committer dedupliziert. RetryPolicy bietet ein beschränktes, deterministisches exponentielles Backoff für transiente (Timeout-)Fehler; ein Job, der sein Budget erschöpft, wird in einem DeadLetterStoreInterface erfasst, statt verloren zu gehen. Jeder Store liefert eine in-memory-Variante (Single-Run-/Testumfang) und eine dauerhafte Dateisystem-Variante.
Dauerhaftigkeits-Marker
Abschnitt betitelt „Dauerhaftigkeits-Marker“Stores, deren Zustand einen Prozess-Neustart überlebt, implementieren den Marker DurableCapability. Ein absturzsicherer Lauf erfordert, dass jeder Kollaborateur dauerhaft ist, sodass er schnell scheitert, statt eine Exactly-once-Semantik zu versprechen, die ein in-memory-Store über einen Neustart hinweg nicht halten kann.
Codebeispiel — Schnellstart
Abschnitt betitelt „Codebeispiel — Schnellstart“Rendern Sie ein Manifest und committen Sie seine Bytes exactly-once. Die Engine gibt Bytes plus einen Digest zurück; der Committer veröffentlicht sie.
<?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";}Codebeispiel — Produktion
Abschnitt betitelt „Codebeispiel — Produktion“Rendern Sie einen Batch, leiten Sie Timeouts an die Retry-Policy und schreiben Sie terminale Fehler ins Dead-Letter. Der Commit weigert sich, abweichende Bytes zu überschreiben, sodass eine Schlüsselkollision abgefangen und erfasst statt verloren wird.
<?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");}Wann zu verwenden
Abschnitt betitelt „Wann zu verwenden“- Batch-Rendering mit hohem Volumen, bei dem der Durchsatz von nebenläufiger (Process-Pool-)Ausführung profitiert.
- Lang laufende Läufe, die einen Absturz überleben und fortgesetzt werden müssen, ohne Ausgabe doppelt zu veröffentlichen.
- Pipelines, die eine Exactly-once-Delivery jedes gerenderten Dokuments an sein Ziel garantieren müssen.
Für ein einzelnes Ad-hoc-Dokument rendern Sie direkt mit dem Writer-Modul; der Wert von Stream liegt in dauerhaften, fortsetzbaren, nebenläufigen Batches.
Performance
Abschnitt betitelt „Performance“Der Durchsatz skaliert mit der Worker-Anzahl im ProcessPoolRenderUnitExecutor (begrenzt durch maxWorkers und maxBatchSize), während die Engine die Render-Ausgabe byte-gleich zur sequentiellen Basis hält. Ein Wall-Clock-Timeout begrenzt jeden parallelen Batch, sodass ein hängender Worker nicht für immer blockieren kann. Es gibt keine veröffentlichte feste Durchsatzangabe; sie hängt von der Dokumentkomplexität und der Host-Parallelität ab. Messen Sie mit repräsentativen Dokumenten.
Sicherheitshinweise
Abschnitt betitelt „Sicherheitshinweise“Manifeste werden vor dem Rendern fail-closed validiert. Der Committer weist Path-Traversal, Null-Bytes, Stream-Wrapper-Schemata, symlink-Ziele und NTFS-Alternate-Data-Stream-(Doppelpunkt-)Vektoren zurück und löst jeden Schlüssel unter einer konfigurierten Wurzel auf. Cross-Process-Worker-Ergebnisse werden neu gehasht und gegen den vom Worker gemeldeten Digest abgeglichen, sodass ein fehlerhafter Worker die Ausgabe nicht stillschweigend beschädigen kann. Dieses Modul protokolliert keinen Dokumentinhalt.
Hinweis zur Enterprise-Grenze
Abschnitt betitelt „Hinweis zur Enterprise-Grenze“Die dauerhaften Stores von Stream sind hier dateisystembasiert und Single-Host. Cross-Host-nebenläufiges Exactly-once auf denselben Schlüssel sowie dauerhafte Deduplizierung über Läufe hinweg sind Aufgabe der Enterprise-Objektspeicher-Committer und -Stores; der Document-Job-Stream-Prozessor, der diese Kollaborateure ansteuert, ist eine Enterprise-Angelegenheit. Pro stellt die Engine, die Verträge und die lokalen dauerhaften Implementierungen bereit.
Core-Fallback / Alternative
Abschnitt betitelt „Core-Fallback / Alternative“Ohne Pro rendern Sie Dokumente einzeln mit dem Writer von NextPDF Core; dauerhaftes Batch-Streaming, nebenläufige Ausführung und Exactly-once-Commit sind Pro-Ergänzungen. Siehe /modules/writer/.
Veröffentlichungsgrenze
Abschnitt betitelt „Veröffentlichungsgrenze“Diese Seite dokumentiert ausschließlich extern beobachtbares Verhalten und die unterstützte öffentliche API-Oberfläche. Interne Namespace-Pfade, Hilfsklassen, Mechanismustabellen, Runbook-Dateinamen und Ticket-Präfixe liegen außerhalb des Umfangs.