Pular para o conteúdo
getnextpdf.com

Pro edição

Stream

O módulo Stream renderiza lotes de documentos de forma durável e concorrente, com commit local exatamente-uma-vez em stores duráveis de host único (o exatamente-uma-vez entre hosts é o limite do Enterprise Stream). Ele divide o trabalho em duas responsabilidades nitidamente separadas: um motor de renderização que transforma manifestos validados em bytes (e nada além disso) e um conjunto de stores duráveis — committer, checkpoint, idempotência, dead-letter — que publicam esses bytes com segurança e permitem que uma execução seja retomada após uma falha sem republicar a saída já confirmada.

Este recurso é fornecido no NextPDF Pro (nextpdf/pro) e é ativado com um envelope de licença de nível Pro. Uma implantação sem essa titularidade não carrega as classes do recurso. Compare edições e obtenha uma licença.

Não há um sinalizador de licença separado por recurso. A concorrência (número de workers), o tamanho do lote, o orçamento de retentativas e o backend dos stores (em memória versus sistema de arquivos durável) são parâmetros de runtime, não chaves de licença.

Terminal window
composer require nextpdf/pro:^3

O código está sob o namespace NextPDF\Pro\Stream.

O Stream é organizado em torno de uma costura congelada — NextPDF\Pro\Stream\Engine\RenderEngineInterface — que separa o motor de throughput da semântica de stream:

  • O motor de renderização é dono da concorrência e da memória limitada. Ele renderiza uma janela de manifestos pré-validados e pré-deduplicados por meio de renderBatch() e retorna um EngineRenderResult por manifesto, na ordem de entrada. Crucialmente, o motor é sem efeitos colaterais no que diz respeito à saída final: ele retorna os bytes renderizados mais o digest sha-256 deles, nunca escrevendo em uma chave de objeto final. É essa pureza que torna a entrega exatamente-uma-vez possível.
  • Os colaboradores do stream são donos da entrega. O committer, o store de checkpoint, o store de idempotência (deduplicação) e o store de dead-letter decidem onde os bytes pousam, como uma execução é retomada, qual trabalho é uma reprodução (replay) e o que acontece com as falhas terminais.

Uma falha de renderização por manifesto é informada como um resultado Failed (ou Timeout) por item; ela nunca aborta o lote. O envelope do lote sempre tem sucesso, com resultados por item.

  • InProcessRenderEngine é a linha de base de correção síncrona e de processo único. Ele valida cada manifesto de forma fail-closed por meio do RenderManifestValidator fornecido antes de renderizá-lo por meio do SingleDocumentRenderer do Core, de modo que um manifesto inválido se torna uma falha por item (código de erro SPEC-MANIFEST-INVALID) em vez de chegar ao renderizador.
  • ConcurrentRenderEngine distribui um lote para um RenderUnitExecutorInterface e restaura a ordem determinística do lote pelo índice da unidade. A saída é idêntica byte a byte a uma renderização sequencial, independentemente da ordem de conclusão; uma conclusão ausente, duplicada ou desconhecida é uma falha grave, nunca um descarte silencioso.
  • Os executores são a costura de concorrência. InlineRenderUnitExecutor é a linha de base determinística; ProcessPoolRenderUnitExecutor distribui um lote entre até N subprocessos worker php que renderizam em paralelo e, em seguida, coleta e verifica a integridade dos seus resultados.

OutputCommitterInterface::commit() publica os bytes renderizados em seu destino final exatamente uma vez: de forma atômica (nenhum objeto parcial é jamais observado), idempotente (reconfirmar conteúdo idêntico byte a byte não realiza nenhuma escrita e retorna um CommitReceipt com idempotentReuse = true — um recibo novo, não o original), sem sobrescrita silenciosa (bytes divergentes em uma chave ocupada sem overwrite levantam um conflito) e com verificação de integridade (o committer recalcula o digest antes de escrever). O LocalFilesystemCommitter implementa isso para o sistema de arquivos local.

Um RunCheckpoint é uma barreira durável que registra quantos itens uma execução confirmou, além de um snapshot do estado com chave. Na recuperação, o processador avança rapidamente além do offset confirmado e restaura o estado com chave, de modo que uma falha no meio da execução é retomada sem republicar a saída já confirmada. O FilesystemCheckpointStore persiste cada barreira de forma atômica.

Deduplicação por idempotência, retentativa e dead-letters

Seção intitulada “Deduplicação por idempotência, retentativa e dead-letters”

O store de idempotência é o caminho rápido que permite ao processador fazer um curto-circuito antes de renderizar um manifesto reproduzido; a comparação de digest do committer continua sendo a garantia durável de exatamente-uma-vez, de modo que um registro de deduplicação perdido, no pior caso, causa uma re-renderização desperdiçada que o committer deduplica. RetryPolicy fornece backoff exponencial determinístico e limitado para falhas transitórias (timeout); um job que esgota seu orçamento é capturado em um DeadLetterStoreInterface em vez de ser perdido. Cada store é fornecido com uma variante em memória (escopo de execução única / de teste) e uma variante durável de sistema de arquivos.

Os stores cujo estado sobrevive a um reinício de processo implementam o marcador DurableCapability. Uma execução segura contra falhas requer que cada colaborador seja durável, de modo que ela falhe rapidamente em vez de prometer uma semântica exatamente-uma-vez que um store em memória não consegue manter ao longo de um reinício.

Renderize um manifesto e confirme seus bytes exatamente uma vez. O motor retorna os bytes mais um digest; o committer os 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";
}

Renderize um lote, encaminhe timeouts para a política de retentativas e envie falhas terminais para o dead-letter. O commit se recusa a sobrescrever bytes divergentes, de modo que uma colisão de chave é detectada e capturada em vez de perdida.

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");
}
  • Renderização de lotes de alto volume em que o throughput se beneficia de execução concorrente (process-pool).
  • Execuções de longa duração que precisam sobreviver a uma falha e retomar sem publicar a saída em duplicidade.
  • Pipelines que precisam garantir a entrega exatamente-uma-vez de cada documento renderizado ao seu destino.

Para um único documento ad-hoc, renderize diretamente com o módulo Writer; o valor do Stream está em lotes duráveis, retomáveis e concorrentes.

O throughput escala com o número de workers no ProcessPoolRenderUnitExecutor (limitado por maxWorkers e maxBatchSize), enquanto o motor mantém a saída de renderização idêntica byte a byte à linha de base sequencial. Um timeout de tempo de parede limita cada lote paralelo, de modo que um worker travado não possa bloquear para sempre. Não há um valor de throughput fixo publicado; ele depende da complexidade do documento e do paralelismo do host. Meça com documentos representativos.

Os manifestos são validados de forma fail-closed antes da renderização. O committer rejeita travessia de caminho (path traversal), bytes nulos, esquemas de stream-wrapper, destinos com link simbólico e vetores de fluxo de dados alternativo NTFS (dois-pontos), e resolve cada chave sob uma única raiz configurada. Os resultados de workers entre processos são re-hasheados e comparados com o digest informado pelo worker, de modo que um worker corrompido não possa corromper a saída silenciosamente. Este módulo não registra nenhum conteúdo de documento.

Os stores duráveis do Stream aqui são baseados em sistema de arquivos e de host único. O exatamente-uma-vez concorrente entre hosts para a mesma chave, e a deduplicação durável entre execuções, são tarefa dos committers e stores de armazenamento de objetos do Enterprise; o processador de stream de jobs de documentos que conduz esses colaboradores é uma preocupação do Enterprise. O Pro fornece o motor, os contratos e as implementações duráveis locais.

Sem o Pro, renderize documentos um de cada vez com o writer do NextPDF Core; o streaming de lotes durável, a execução concorrente e o commit exatamente-uma-vez são adições do Pro. Consulte /modules/writer/.

Esta página documenta apenas o comportamento observável externamente e a superfície de API pública suportada. Caminhos de namespace internos, classes auxiliares, tabelas de mecanismos, nomes de arquivos de runbook e prefixos de tickets estão fora de escopo.