Aller au contenu
getnextpdf.com

Pro édition

Stream

Le module Stream rend des lots de documents de façon durable et concurrente, avec un commit local exactement-une-fois vers des stores durables mono-hôte (l’exactement-une-fois inter-hôtes est la frontière de Stream Enterprise). Il scinde le travail en deux responsabilités proprement séparées : un moteur de rendu qui transforme des manifestes validés en octets (et rien d’autre), et un ensemble de stores durables — committer, jalon, idempotence, lettre morte — qui publient ces octets en toute sécurité et permettent à une exécution de reprendre après une panne sans re-publier la sortie déjà commitée.

Cette fonctionnalité est livrée dans NextPDF Pro (nextpdf/pro) et s’active avec une enveloppe de licence de palier Pro. Un déploiement sans ce droit ne charge pas les classes de la fonctionnalité. Compare les éditions et obtiens une licence.

Il n’existe pas d’indicateur de licence par fonctionnalité distinct. La concurrence (nombre de workers), la taille de lot, le budget de nouvelles tentatives et le backend de store (en mémoire ou système de fichiers durable) sont des paramètres au runtime, pas des bascules de licence.

Fenêtre de terminal
composer require nextpdf/pro:^3

Le code se trouve sous l’espace de noms NextPDF\Pro\Stream.

Stream s’organise autour d’une jointure figée — NextPDF\Pro\Stream\Engine\RenderEngineInterface — qui sépare le moteur de débit de la sémantique de flux :

  • Le moteur de rendu détient la concurrence et la mémoire bornée. Il rend une fenêtre de manifestes pré-validés et pré-dédupliqués via renderBatch() et renvoie un EngineRenderResult par manifeste, dans l’ordre d’entrée. Surtout, le moteur est sans effet de bord vis-à-vis de la sortie finale : il renvoie les octets rendus accompagnés de leur empreinte sha-256, sans jamais écrire vers une clé d’objet finale. C’est cette pureté qui rend possible la livraison exactement-une-fois.
  • Les collaborateurs de flux détiennent la livraison. Le committer, le store de jalons, le store d’idempotence (déduplication) et le store de lettres mortes décident où atterrissent les octets, comment une exécution reprend, quel travail est une rediffusion et ce qu’il advient des défaillances terminales.

Un échec de rendu par manifeste est rapporté comme un résultat Failed (ou Timeout) par élément ; il n’interrompt jamais le lot. L’enveloppe de lot réussit toujours, avec des issues par élément.

  • InProcessRenderEngine est la référence de correction synchrone, mono-processus. Il valide chaque manifeste de façon verrouillée via le RenderManifestValidator livré avant de le rendre via le SingleDocumentRenderer du Core, si bien qu’un manifeste invalide devient un échec par élément (code d’erreur SPEC-MANIFEST-INVALID) au lieu d’atteindre le renderer.
  • ConcurrentRenderEngine distribue un lot vers un RenderUnitExecutorInterface et rétablit l’ordre déterministe du lot par index d’unité. La sortie est identique octet pour octet à un rendu séquentiel, quel que soit l’ordre d’achèvement ; un achèvement manquant, dupliqué ou inconnu est un échec dur, jamais un abandon silencieux.
  • Les exécuteurs sont la jointure de concurrence. InlineRenderUnitExecutor est la référence déterministe ; ProcessPoolRenderUnitExecutor répartit un lot sur jusqu’à N sous-processus worker php qui rendent en parallèle, puis collecte et vérifie l’intégrité de leurs résultats.

OutputCommitterInterface::commit() publie les octets rendus vers leur destination finale exactement une fois : de façon atomique (aucun objet partiel n’est jamais observé), idempotente (re-commiter un contenu identique octet pour octet n’effectue aucune écriture et renvoie un CommitReceipt avec idempotentReuse = true — un reçu neuf, pas l’original), sans écrasement silencieux (des octets divergents vers une clé occupée sans overwrite lève un conflit) et vérifiée en intégrité (le committer recalcule l’empreinte avant d’écrire). Le LocalFilesystemCommitter met cela en œuvre pour le système de fichiers local.

Un RunCheckpoint est une barrière durable qui enregistre combien d’éléments une exécution a commités, plus un instantané d’état clavé. À la reprise, le processor avance rapidement au-delà de l’offset commité et restaure l’état clavé, si bien qu’une panne en cours d’exécution reprend sans re-publier la sortie déjà commitée. FilesystemCheckpointStore persiste chaque barrière de façon atomique.

Déduplication par idempotence, nouvelles tentatives et lettres mortes

Section intitulée « Déduplication par idempotence, nouvelles tentatives et lettres mortes »

Le store d’idempotence est le chemin rapide qui permet au processor de court-circuiter avant de rendre un manifeste rediffusé ; la comparaison d’empreinte du committer reste la garantie durable exactement-une-fois, si bien qu’un enregistrement de déduplication perdu provoque au pire un re-rendu gaspillé que le committer déduplique. RetryPolicy fournit un backoff exponentiel borné et déterministe pour les défaillances transitoires (timeout) ; un job qui épuise son budget est capturé dans un DeadLetterStoreInterface plutôt que perdu. Chaque store livre une variante en mémoire (portée mono-exécution / test) et une variante système de fichiers durable.

Les stores dont l’état survit à un redémarrage de processus implémentent le marqueur DurableCapability. Une exécution résistante aux pannes exige que chaque collaborateur soit durable, de sorte qu’elle échoue rapidement plutôt que de promettre une sémantique exactement-une-fois qu’un store en mémoire ne peut tenir à travers un redémarrage.

Rends un manifeste et commite ses octets exactement une fois. Le moteur renvoie les octets accompagnés d’une empreinte ; le committer les publie.

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

Rends un lot, route les timeouts vers la politique de nouvelles tentatives et met en lettre morte les défaillances terminales. Le commit refuse d’écraser des octets divergents, si bien qu’une collision de clé est détectée et capturée plutôt que perdue.

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");
}
  • Rendu de lots à fort volume où le débit profite d’une exécution concurrente (pool de processus).
  • Exécutions de longue durée qui doivent survivre à une panne et reprendre sans double publication de la sortie.
  • Pipelines qui doivent garantir la livraison exactement-une-fois de chaque document rendu vers sa cible.

Pour un document ponctuel isolé, rends directement avec le module Writer ; la valeur de Stream réside dans les lots durables, reprenables et concurrents.

Le débit évolue avec le nombre de workers dans ProcessPoolRenderUnitExecutor (borné par maxWorkers et maxBatchSize), tandis que le moteur garde une sortie de rendu identique octet pour octet à la référence séquentielle. Un délai d’expiration en temps d’horloge plafonne chaque lot parallèle, de sorte qu’un worker bloqué ne puisse pas bloquer indéfiniment. Il n’existe pas de chiffre de débit fixe publié ; il dépend de la complexité des documents et du parallélisme de l’hôte. Mesure avec des documents représentatifs.

Les manifestes sont validés de façon verrouillée avant le rendu. Le committer rejette la traversée de chemin, les octets nuls, les schémas de stream-wrapper, les cibles en lien symbolique et les vecteurs de flux de données alternatifs NTFS (deux-points), et résout chaque clé sous une unique racine configurée. Les résultats de worker inter-processus sont re-hachés et comparés à l’empreinte rapportée par le worker, de sorte qu’un worker corrompu ne puisse pas altérer silencieusement la sortie. Ce module ne journalise aucun contenu de document.

Les stores durables de Stream sont ici adossés au système de fichiers et mono-hôte. Le commit exactement-une-fois concurrent inter-hôtes vers la même clé, ainsi que la déduplication durable entre exécutions, relèvent des committers et stores Enterprise sur stockage objet ; le processor de flux de jobs documentaires qui pilote ces collaborateurs est une affaire Enterprise. Pro fournit le moteur, les contrats et les implémentations durables locales.

Sans Pro, rends les documents un par un avec le writer de NextPDF Core ; le streaming de lots durable, l’exécution concurrente et le commit exactement-une-fois sont des ajouts de Pro. Voir /modules/writer/.

Cette page documente uniquement le comportement observable de l’extérieur et la surface d’API publique prise en charge. Les chemins d’espaces de noms internes, les classes utilitaires, les tables de mécanismes, les noms de fichiers de runbook et les préfixes de tickets sont hors périmètre.