Przejdź do głównej zawartości
getnextpdf.com

Pro edycja

Stream

Moduł Stream renderuje partie dokumentów trwale i współbieżnie, z lokalnym zatwierdzaniem dokładnie raz do jednohostowych trwałych magazynów (dokładnie raz między hostami to granica Stream w edycji Enterprise). Dzieli pracę na dwie czysto rozdzielone odpowiedzialności: silnik renderowania, który zamienia zwalidowane manifesty w bajty (i nic więcej), oraz zestaw trwałych magazynów — committer, punkt kontrolny, idempotentność, dead-letter — które bezpiecznie publikują te bajty i pozwalają wznowić przebieg po awarii bez ponownego publikowania zatwierdzonego wyniku.

Ta funkcja jest dostarczana w NextPDF Pro (nextpdf/pro) i aktywuje się za pomocą koperty licencyjnej poziomu Pro. Wdrożenie bez tego uprawnienia nie ładuje klas tej funkcji. Porównaj edycje i uzyskaj licencję.

Nie istnieje osobna flaga licencji na poziomie pojedynczej funkcji. Współbieżność (liczba pracowników), rozmiar partii, budżet ponowień oraz backend magazynu (w pamięci kontra trwały system plików) to parametry czasu działania, a nie przełączniki licencji.

Okno terminala
composer require nextpdf/pro:^3

Kod znajduje się w przestrzeni nazw NextPDF\Pro\Stream.

Stream jest zorganizowany wokół zamrożonego szwu — NextPDF\Pro\Stream\Engine\RenderEngineInterface — który oddziela silnik przepustowości od semantyki strumienia:

  • Silnik renderowania zarządza współbieżnością i ograniczoną pamięcią. Renderuje okno wstępnie zwalidowanych, wstępnie zdeduplikowanych manifestów za pomocą renderBatch() i zwraca po jednym EngineRenderResult na manifest, w kolejności wejścia. Co kluczowe, silnik jest wolny od skutków ubocznych względem ostatecznego wyniku: zwraca wyrenderowane bajty wraz z ich skrótem sha-256, nigdy nie zapisując do ostatecznego klucza obiektu. To właśnie ta czystość umożliwia dostarczanie dokładnie raz.
  • Współpracownicy strumienia zarządzają dostarczaniem. Committer, magazyn punktów kontrolnych, magazyn idempotentności (deduplikacji) oraz magazyn dead-letter decydują, gdzie trafiają bajty, jak wznawia się przebieg, która praca jest powtórką i co dzieje się z błędami terminalnymi.

Niepowodzenie renderowania pojedynczego manifestu jest raportowane jako wynik Failed (lub Timeout) dla danego elementu; nigdy nie przerywa partii. Koperta partii zawsze kończy się powodzeniem z wynikami dla poszczególnych elementów.

  • InProcessRenderEngine to synchroniczny, jednoprocesowy punkt odniesienia poprawności. Waliduje każdy manifest w trybie fail-closed za pomocą dostarczanego RenderManifestValidator przed wyrenderowaniem go przez SingleDocumentRenderer z Core, więc błędny manifest staje się niepowodzeniem dla danego elementu (kod błędu SPEC-MANIFEST-INVALID) zamiast dotrzeć do renderera.
  • ConcurrentRenderEngine rozprasza partię do RenderUnitExecutorInterface i odtwarza deterministyczną kolejność partii według indeksu jednostki. Wynik jest bajtowo identyczny z renderowaniem sekwencyjnym niezależnie od kolejności ukończenia; brakujące, zduplikowane lub nieznane ukończenie to twarde niepowodzenie, nigdy ciche porzucenie.
  • Wykonawcy są szwem współbieżności. InlineRenderUnitExecutor to deterministyczny punkt odniesienia; ProcessPoolRenderUnitExecutor rozdziela partię na maksymalnie N podprocesów roboczych php, które renderują równolegle, a następnie zbiera ich wyniki i sprawdza ich integralność.

OutputCommitterInterface::commit() publikuje wyrenderowane bajty do ich ostatecznego miejsca docelowego dokładnie raz: atomowo (nigdy nie obserwuje się częściowego obiektu), idempotentnie (ponowne zatwierdzenie bajtowo identycznej treści nie wykonuje żadnego zapisu i zwraca CommitReceipt z idempotentReuse = true — nowe potwierdzenie, a nie oryginalne), bez cichego nadpisywania (rozbieżne bajty do zajętego klucza bez overwrite zgłaszają konflikt) oraz ze sprawdzeniem integralności (committer przelicza skrót przed zapisem). LocalFilesystemCommitter realizuje to dla lokalnego systemu plików.

RunCheckpoint to trwała bariera rejestrująca, ile elementów zatwierdził przebieg, wraz z migawką stanu z kluczami. Przy odtwarzaniu procesor przewija do przodu poza zatwierdzone przesunięcie i odtwarza stan z kluczami, więc awaria w trakcie przebiegu wznawia się bez ponownego publikowania zatwierdzonego wyniku. FilesystemCheckpointStore utrwala każdą barierę atomowo.

Deduplikacja idempotentności, ponawianie i dead-letter

Dział zatytułowany „Deduplikacja idempotentności, ponawianie i dead-letter”

Magazyn idempotentności to szybka ścieżka, która pozwala procesorowi zwarciowo pominąć renderowanie powtórzonego manifestu; porównanie skrótów w committerze pozostaje trwałą gwarancją dokładnie raz, więc utracony rekord deduplikacji w najgorszym razie powoduje zmarnowane ponowne renderowanie, które committer zdeduplikuje. RetryPolicy zapewnia ograniczone, deterministyczne wykładnicze odczekiwanie (backoff) dla przejściowych (timeout) niepowodzeń; zadanie, które wyczerpie swój budżet, jest przechwytywane w DeadLetterStoreInterface, a nie tracone. Każdy magazyn dostarcza wariant w pamięci (zakres pojedynczego przebiegu / testu) oraz trwały wariant na systemie plików.

Magazyny, których stan przetrwa restart procesu, implementują znacznik DurableCapability. Przebieg odporny na awarie wymaga, aby każdy współpracownik był trwały, więc zawodzi szybko, zamiast obiecywać semantykę dokładnie raz, której magazyn w pamięci nie jest w stanie utrzymać po restarcie.

Wyrenderuj jeden manifest i zatwierdź jego bajty dokładnie raz. Silnik zwraca bajty wraz ze skrótem; committer je publikuje.

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

Wyrenderuj partię, przekieruj timeouty do polityki ponowień i skieruj błędy terminalne do dead-letter. Zatwierdzanie odmawia nadpisania rozbieżnych bajtów, więc kolizja kluczy jest wychwytywana i przechwytywana, a nie tracona.

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");
}
  • Renderowanie wsadowe o dużej objętości, gdzie przepustowość zyskuje na współbieżnym (process-pool) wykonaniu.
  • Długotrwałe przebiegi, które muszą przetrwać awarię i wznowić się bez podwójnego publikowania wyniku.
  • Potoki, które muszą gwarantować dostarczanie dokładnie raz każdego wyrenderowanego dokumentu do jego celu.

W przypadku pojedynczego dokumentu ad-hoc renderuj bezpośrednio modułem Writer; wartość Stream tkwi w trwałych, wznawialnych, współbieżnych partiach.

Przepustowość skaluje się wraz z liczbą pracowników w ProcessPoolRenderUnitExecutor (ograniczona przez maxWorkers i maxBatchSize), podczas gdy silnik utrzymuje wynik renderowania bajtowo identyczny z sekwencyjnym punktem odniesienia. Limit czasu rzeczywistego ogranicza każdą równoległą partię, więc zawieszony pracownik nie może blokować w nieskończoność. Nie ma opublikowanej stałej liczby przepustowości; zależy ona od złożoności dokumentu i równoległości hosta. Mierz na reprezentatywnych dokumentach.

Manifesty są walidowane w trybie fail-closed przed renderowaniem. Committer odrzuca przejścia po ścieżce (path traversal), bajty null, schematy stream-wrapper, cele będące dowiązaniami symbolicznymi oraz wektory alternatywnego strumienia danych NTFS (dwukropek) i rozwiązuje każdy klucz pod jednym skonfigurowanym katalogiem głównym. Wyniki pracowników z różnych procesów są ponownie hashowane i porównywane ze skrótem zgłoszonym przez pracownika, więc uszkodzony pracownik nie może po cichu zepsuć wyniku. Ten moduł nie loguje żadnej treści dokumentu.

Trwałe magazyny Stream są tutaj oparte na systemie plików i jednohostowe. Międzyhostowe współbieżne dostarczanie dokładnie raz do tego samego klucza oraz trwała deduplikacja między przebiegami to zadanie committerów i magazynów obiektowych Enterprise; procesor strumienia zadań dokumentowych, który napędza tych współpracowników, jest sprawą Enterprise. Pro dostarcza silnik, kontrakty oraz lokalne trwałe implementacje.

Bez Pro renderuj dokumenty pojedynczo za pomocą pisarza NextPDF Core; trwałe strumieniowanie wsadowe, wykonanie współbieżne oraz zatwierdzanie dokładnie raz to dodatki Pro. Zobacz /modules/writer/.

Ta strona dokumentuje wyłącznie zachowanie obserwowalne z zewnątrz oraz obsługiwaną publiczną powierzchnię API. Wewnętrzne ścieżki przestrzeni nazw, klasy pomocnicze, tabele mechanizmów, nazwy plików runbook oraz prefiksy zgłoszeń są poza zakresem.