Ir al contenido
getnextpdf.com

Pro edición

Stream

El módulo Stream renderiza lotes de documentos de forma duradera y concurrente, con confirmación local exactamente una vez en almacenes duraderos de un solo host (la concurrencia entre hosts exactamente una vez es el límite de Stream de Enterprise). Divide el trabajo en dos responsabilidades claramente separadas: un motor de renderizado que convierte manifiestos validados en bytes (y nada más), y un conjunto de almacenes duraderos —confirmador, punto de control, idempotencia, mensajes muertos— que publican esos bytes de forma segura y permiten que una ejecución se reanude tras una caída sin volver a publicar la salida ya confirmada.

Esta funcionalidad se incluye en NextPDF Pro (nextpdf/pro) y se activa con un sobre de licencia de nivel Pro. Un despliegue sin ese derecho no carga las clases de la funcionalidad. Compare ediciones y obtenga una licencia.

No hay ningún indicador de licencia por característica independiente. La concurrencia (número de workers), el tamaño de lote, el presupuesto de reintentos y el backend de almacén (en memoria frente a sistema de archivos duradero) son parámetros en tiempo de ejecución, no interruptores de licencia.

Ventana de terminal
composer require nextpdf/pro:^3

El código reside bajo el espacio de nombres NextPDF\Pro\Stream.

Stream se organiza en torno a una costura congelada —NextPDF\Pro\Stream\Engine\RenderEngineInterface— que separa el motor de rendimiento de la semántica del flujo:

  • El motor de renderizado posee la concurrencia y la memoria acotada. Renderiza una ventana de manifiestos previamente validados y deduplicados mediante renderBatch() y devuelve un EngineRenderResult por manifiesto, en el orden de entrada. Lo crucial es que el motor no tiene efectos secundarios respecto a la salida final: devuelve los bytes renderizados más su resumen sha-256, y nunca escribe en una clave de objeto final. Esa pureza es lo que hace posible la entrega exactamente una vez.
  • Los colaboradores del flujo poseen la entrega. El confirmador, el almacén de puntos de control, el almacén de idempotencia (deduplicación) y el almacén de mensajes muertos deciden dónde acaban los bytes, cómo se reanuda una ejecución, qué trabajo es una repetición y qué ocurre con los fallos terminales.

Un fallo de renderizado por manifiesto se informa como un resultado Failed (o Timeout) por elemento; nunca aborta el lote. El sobre del lote siempre tiene éxito, con resultados por elemento.

  • InProcessRenderEngine es la línea de base de corrección síncrona, de un solo proceso. Valida cada manifiesto de fallo cerrado mediante el RenderManifestValidator incluido antes de renderizarlo a través del SingleDocumentRenderer de Core, de modo que un manifiesto incorrecto se convierte en un fallo por elemento (código de error SPEC-MANIFEST-INVALID) en lugar de alcanzar el renderizador.
  • ConcurrentRenderEngine reparte un lote a un RenderUnitExecutorInterface y restaura el orden determinista del lote por índice de unidad. La salida es idéntica byte a byte a un renderizado secuencial, con independencia del orden de finalización; una finalización ausente, duplicada o desconocida es un fallo grave, nunca un descarte silencioso.
  • Los ejecutores son la costura de la concurrencia. InlineRenderUnitExecutor es la línea de base determinista; ProcessPoolRenderUnitExecutor distribuye un lote entre hasta N subprocesos worker php que renderizan en paralelo, y luego recopila y comprueba la integridad de sus resultados.

Confirmación duradera y sin efectos secundarios

Sección titulada «Confirmación duradera y sin efectos secundarios»

OutputCommitterInterface::commit() publica los bytes renderizados en su destino final exactamente una vez: de forma atómica (nunca se observa un objeto parcial), idempotente (volver a confirmar contenido idéntico byte a byte no realiza ninguna escritura y devuelve un CommitReceipt con idempotentReuse = true —un recibo nuevo, no el original—), sin sobrescritura silenciosa (escribir bytes divergentes en una clave ocupada sin overwrite provoca un conflicto) y con integridad comprobada (el confirmador recalcula el resumen antes de escribir). LocalFilesystemCommitter lo implementa para el sistema de archivos local.

Un RunCheckpoint es una barrera duradera que registra cuántos elementos ha confirmado una ejecución más una instantánea del estado con clave. En la recuperación, el procesador avanza rápidamente más allá del desplazamiento confirmado y restaura el estado con clave, de modo que una caída a mitad de ejecución se reanuda sin volver a publicar la salida confirmada. FilesystemCheckpointStore persiste cada barrera de forma atómica.

Deduplicación por idempotencia, reintentos y mensajes muertos

Sección titulada «Deduplicación por idempotencia, reintentos y mensajes muertos»

El almacén de idempotencia es la ruta rápida que permite al procesador hacer un cortocircuito antes de renderizar un manifiesto repetido; la comparación de resúmenes del confirmador sigue siendo la garantía duradera de exactamente una vez, de modo que un registro de deduplicación perdido provoca, en el peor de los casos, un re-renderizado malgastado que el confirmador deduplica. RetryPolicy proporciona retroceso exponencial acotado y determinista para los fallos transitorios (timeout); un trabajo que agota su presupuesto se captura en un DeadLetterStoreInterface en lugar de perderse. Cada almacén incluye una variante en memoria (alcance de una sola ejecución / pruebas) y una variante duradera en sistema de archivos.

Los almacenes cuyo estado sobrevive al reinicio de un proceso implementan el marcador DurableCapability. Una ejecución a prueba de caídas requiere que todos los colaboradores sean duraderos, de modo que falle rápidamente en lugar de prometer una semántica de exactamente una vez que un almacén en memoria no puede mantener tras un reinicio.

Renderice un manifiesto y confirme sus bytes exactamente una vez. El motor devuelve los bytes más un resumen; el confirmador los publica.

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

Renderice un lote, dirija los timeouts a la política de reintentos y envíe a mensajes muertos los fallos terminales. La confirmación se niega a sobrescribir bytes divergentes, de modo que una colisión de claves se detecta y captura en lugar de perderse.

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");
}
  • Renderizado por lotes de alto volumen donde el rendimiento se beneficia de la ejecución concurrente (con grupo de procesos).
  • Ejecuciones de larga duración que deben sobrevivir a una caída y reanudarse sin publicar la salida por duplicado.
  • Pipelines que deben garantizar la entrega exactamente una vez de cada documento renderizado a su destino.

Para un único documento ad hoc, renderice directamente con el módulo Writer; el valor de Stream está en los lotes duraderos, reanudables y concurrentes.

El rendimiento escala con el número de workers en ProcessPoolRenderUnitExecutor (acotado por maxWorkers y maxBatchSize), mientras el motor mantiene la salida del renderizado idéntica byte a byte a la línea de base secuencial. Un tiempo de espera de reloj de pared limita cada lote paralelo para que un worker colgado no pueda bloquear para siempre. No hay ninguna cifra de rendimiento fija publicada; depende de la complejidad del documento y del paralelismo del host. Mida con documentos representativos.

Los manifiestos se validan de fallo cerrado antes de renderizar. El confirmador rechaza el cruce de rutas, los bytes nulos, los esquemas de envoltorio de flujo, los destinos con enlaces simbólicos y los vectores de flujo de datos alternativo de NTFS (dos puntos), y resuelve cada clave bajo una única raíz configurada. Los resultados de los workers entre procesos se vuelven a hashear y se cotejan con el resumen informado por el worker, de modo que un worker corrupto no pueda dañar la salida en silencio. Este módulo no registra ningún contenido del documento.

Los almacenes duraderos de Stream aquí están respaldados por el sistema de archivos y son de un solo host. La concurrencia entre hosts exactamente una vez a la misma clave, y la deduplicación duradera entre ejecuciones, son competencia de los confirmadores y almacenes de almacenamiento de objetos de Enterprise; el procesador de flujo de trabajos de documento que dirige a estos colaboradores es un asunto de Enterprise. Pro proporciona el motor, los contratos y las implementaciones duraderas locales.

Sin Pro, renderice documentos uno a uno con el writer de NextPDF Core; el streaming por lotes duradero, la ejecución concurrente y la confirmación exactamente una vez son adiciones de Pro. Consulte /modules/writer/.

Esta página documenta únicamente el comportamiento observable externamente y la superficie pública de API compatible. Las rutas de espacios de nombres internas, las clases auxiliares, las tablas de mecanismos, los nombres de archivo de runbooks y los prefijos de tickets quedan fuera de alcance.