Ga naar inhoud
getnextpdf.com

Pro editie

Stream

De Stream-module rendert batches documenten duurzaam en concurrent, met exactly-once lokale commit naar duurzame stores op één host (cross-host exactly-once is de grens van Enterprise Stream). Het splitst het werk in twee netjes gescheiden verantwoordelijkheden: een renderengine die gevalideerde manifesten omzet in bytes (en niets anders), en een set duurzame stores — committer, checkpoint, idempotentie, dead-letter — die die bytes veilig publiceren en een run na een crash laten hervatten zonder gecommitte uitvoer opnieuw te publiceren.

Deze mogelijkheid wordt geleverd in NextPDF Pro (nextpdf/pro) en wordt geactiveerd met een Pro-tier-licentie-envelope. Een deployment zonder die entitlement laadt de klassen van de mogelijkheid niet. Vergelijk edities en verkrijg een licentie.

Er is geen aparte licentievlag per feature. Concurrency (aantal workers), batchgrootte, retry-budget en store-backend (in-memory versus duurzaam bestandssysteem) zijn runtimeparameters, geen licentieschakelaars.

Terminal window
composer require nextpdf/pro:^3

De code bevindt zich onder de NextPDF\Pro\Stream-namespace.

Stream is georganiseerd rond een bevroren naad — NextPDF\Pro\Stream\Engine\RenderEngineInterface — die de throughput-engine scheidt van de streamsemantiek:

  • De renderengine bezit concurrency en begrensd geheugen. Het rendert een venster van voorgevalideerde, voorgededupliceerde manifesten via renderBatch() en retourneert één EngineRenderResult per manifest, in invoervolgorde. Cruciaal is dat de engine side-effect-vrij is ten opzichte van de uiteindelijke uitvoer: het retourneert gerenderde bytes plus hun sha-256-digest en schrijft nooit naar een uiteindelijke objectsleutel. Die zuiverheid is wat exactly-once-aflevering mogelijk maakt.
  • De stream-collaborators bezitten de aflevering. De committer, de checkpoint store, de idempotentie- (dedup-) store en de dead-letter store bepalen waar bytes landen, hoe een run hervat, welk werk een replay is en wat er met terminale mislukkingen gebeurt.

Een renderfout per manifest wordt gerapporteerd als een Failed- (of Timeout-)resultaat per item; het breekt de batch nooit af. De batch-envelope slaagt altijd met uitkomsten per item.

  • InProcessRenderEngine is de synchrone, single-process-correctheidsbaseline. Het valideert elk manifest fail-closed via de meegeleverde RenderManifestValidator voordat het dit rendert via de Core-SingleDocumentRenderer, zodat een fout manifest een mislukking per item wordt (foutcode SPEC-MANIFEST-INVALID) in plaats van de renderer te bereiken.
  • ConcurrentRenderEngine waaiert een batch uit naar een RenderUnitExecutorInterface en herstelt de deterministische batchvolgorde op unit-index. De uitvoer is byte-identiek aan een sequentiële render, ongeacht de voltooiingsvolgorde; een ontbrekende, dubbele of onbekende voltooiing is een harde mislukking, nooit een stille drop.
  • Executors zijn de concurrency-naad. InlineRenderUnitExecutor is de deterministische baseline; ProcessPoolRenderUnitExecutor verdeelt een batch over maximaal N php-workersubprocessen die parallel renderen, en verzamelt en integriteitscontroleert vervolgens hun resultaten.

OutputCommitterInterface::commit() publiceert gerenderde bytes exactly once naar hun uiteindelijke bestemming: atomair (er wordt nooit een partieel object waargenomen), idempotent (het opnieuw committen van byte-identieke inhoud voert geen schrijfactie uit en retourneert een CommitReceipt met idempotentReuse = true — een verse receipt, niet de originele), zonder stille overschrijving (afwijkende bytes naar een bezette sleutel zonder overwrite veroorzaken een conflict) en integriteitsgecontroleerd (de committer herberekent de digest vóór het schrijven). De LocalFilesystemCommitter implementeert dit voor het lokale bestandssysteem.

Een RunCheckpoint is een duurzame barrière die vastlegt hoeveel items een run heeft gecommit, plus een snapshot van de keyed state. Bij herstel spoelt de processor vooruit voorbij de gecommitte offset en herstelt het de keyed state, zodat een crash midden in een run hervat zonder gecommitte uitvoer opnieuw te publiceren. FilesystemCheckpointStore persisteert elke barrière atomair.

De idempotentie-store is het snelle pad dat de processor laat kortsluiten voordat een gereplayd manifest wordt gerenderd; de digest-vergelijking van de committer blijft de duurzame exactly-once-garantie, zodat een verloren dedup-record in het slechtste geval een verspilde herrender veroorzaakt die de committer dedupliceert. RetryPolicy biedt begrensde, deterministische exponentiële backoff voor transiënte (timeout-)mislukkingen; een job die zijn budget uitput, wordt vastgelegd in een DeadLetterStoreInterface in plaats van verloren te gaan. Elke store wordt geleverd met een in-memory-variant (single-run / testscope) en een duurzame bestandssysteemvariant.

Stores waarvan de state een procesherstart overleeft, implementeren de DurableCapability-marker. Een crashveilige run vereist dat elke collaborator duurzaam is, zodat hij fail-fast werkt in plaats van exactly-once-semantiek te beloven die een in-memory-store niet over een herstart heen kan nakomen.

Render één manifest en commit zijn bytes exactly once. De engine retourneert bytes plus een digest; de committer publiceert ze.

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 een batch, route timeouts naar de retry policy en dead-letter terminale mislukkingen. Commit weigert afwijkende bytes te overschrijven, dus een sleutelbotsing wordt opgevangen en vastgelegd in plaats van verloren te gaan.

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");
}
  • Batchrendering met hoog volume waarbij de doorvoer profiteert van concurrente (process-pool-)uitvoering.
  • Langlopende runs die een crash moeten overleven en hervatten zonder uitvoer dubbel te publiceren.
  • Pijplijnen die exactly-once-aflevering van elk gerenderd document naar zijn doel moeten garanderen.

Voor een enkel ad-hocdocument render je rechtstreeks met de Writer-module; de waarde van Stream zit in duurzame, hervatbare, concurrente batches.

De doorvoer schaalt met het aantal workers in ProcessPoolRenderUnitExecutor (begrensd door maxWorkers en maxBatchSize), terwijl de engine de renderuitvoer byte-identiek houdt aan de sequentiële baseline. Een wandtijd-timeout begrenst elke parallelle batch zodat een vastgelopen worker niet voor altijd kan blokkeren. Er is geen gepubliceerd vast doorvoercijfer; het hangt af van de documentcomplexiteit en de host-parallelliteit. Meet met representatieve documenten.

Manifesten worden fail-closed gevalideerd vóór rendering. De committer wijst path traversal, null bytes, stream-wrapper-schema’s, symlinked targets en NTFS alternate-data-stream-(dubbelepunt-)vectoren af, en lost elke sleutel op onder één geconfigureerde root. Worker-resultaten tussen processen worden opnieuw gehasht en gematcht tegen de door de worker gerapporteerde digest, zodat een verminkte worker de uitvoer niet stilzwijgend kan corrumperen. Deze module logt geen documentinhoud.

De duurzame stores van Stream hier zijn bestandssysteem-backed en single-host. Cross-host concurrente exactly-once naar dezelfde sleutel, en duurzame dedup over runs heen, zijn de taak van de Enterprise object-storage-committers en -stores; de document-job-streamprocessor die deze collaborators aandrijft, is een Enterprise-aangelegenheid. Pro levert de engine, de contracten en de lokale duurzame implementaties.

Zonder Pro render je documenten één voor één met de writer van NextPDF Core; duurzame batchstreaming, concurrente uitvoering en exactly-once commit zijn Pro-toevoegingen. Zie /modules/writer/.

Deze pagina documenteert uitsluitend extern waarneembaar gedrag en het ondersteunde publieke API-oppervlak. Interne namespace-paden, helperklassen, mechanismetabellen, runbook-bestandsnamen en ticketprefixen vallen buiten de scope.