Pro редакция
Stream
Модуль Stream отрисовывает пакеты документов долговечно и конкурентно, с локальной фиксацией ровно один раз в долговечные хранилища одного хоста (межхостовая фиксация ровно один раз — это граница Enterprise Stream). Он разбивает работу на две чётко разделённые обязанности: движок отрисовки, который превращает проверенные манифесты в байты (и больше ничего), и набор долговечных хранилищ — фиксатор, контрольная точка, идемпотентность, недоставленные сообщения, — которые безопасно публикуют эти байты и позволяют прогону возобновиться после сбоя без повторной публикации уже зафиксированного вывода.
Доступность и лицензирование
Заголовок раздела «Доступность и лицензирование»Эта возможность поставляется в NextPDF Pro (nextpdf/pro) и активируется лицензионным конвертом уровня Pro. Развёртывание без этого права доступа не загружает классы возможности. Сравнить редакции и получить лицензию.
Отдельного пофункционального лицензионного флага нет. Конкурентность (число воркеров), размер пакета, бюджет повторов и бэкенд хранилища (в памяти либо долговечная файловая система) — это параметры времени выполнения, а не лицензионные переключатели.
Установка
Заголовок раздела «Установка»composer require nextpdf/pro:^3Код находится в пространстве имён NextPDF\Pro\Stream.
Концептуальный обзор
Заголовок раздела «Концептуальный обзор»Stream организован вокруг замороженного шва — NextPDF\Pro\Stream\Engine\RenderEngineInterface — который отделяет движок пропускной способности от семантики потока:
- Движок отрисовки владеет конкурентностью и ограниченной памятью. Он отрисовывает окно предварительно проверенных и предварительно дедуплицированных манифестов через
renderBatch()и возвращает одинEngineRenderResultна манифест, в порядке поступления. Принципиально важно, что движок не имеет побочных эффектов в отношении итогового вывода: он возвращает отрисованные байты плюс их sha-256-дайджест и никогда не пишет в финальный объектный ключ. Именно эта чистота делает возможной доставку ровно один раз. - Коллабораторы потока владеют доставкой. Фиксатор, хранилище контрольных точек, хранилище идемпотентности (дедупликации) и хранилище недоставленных сообщений решают, где оседают байты, как возобновляется прогон, какая работа является повтором и что происходит с терминальными сбоями.
Сбой отрисовки отдельного манифеста сообщается как результат Failed (или Timeout) по конкретному элементу; он никогда не прерывает пакет. Конверт пакета всегда завершается успешно с поэлементными исходами.
Ключевые концепции
Заголовок раздела «Ключевые концепции»Движки отрисовки и исполнители
Заголовок раздела «Движки отрисовки и исполнители»InProcessRenderEngine— синхронный, однопроцессный эталон корректности. Он проверяет каждый манифест с отказом в закрытое состояние через поставляемыйRenderManifestValidator, прежде чем отрисовать его черезSingleDocumentRendererиз Core, поэтому неверный манифест становится поэлементным сбоем (код ошибкиSPEC-MANIFEST-INVALID) вместо того, чтобы достичь отрисовщика.ConcurrentRenderEngineраспределяет пакет наRenderUnitExecutorInterfaceи восстанавливает детерминированный порядок пакета по индексу единицы. Вывод побайтно идентичен последовательной отрисовке независимо от порядка завершения; пропущенное, дублирующееся или неизвестное завершение — это жёсткий сбой, а не тихая потеря.- Исполнители — это шов конкурентности.
InlineRenderUnitExecutor— детерминированный эталон;ProcessPoolRenderUnitExecutorраспределяет пакет между не более чем N подпроцессами-воркерамиphp, которые отрисовывают параллельно, затем собирает и проверяет целостность их результатов.
Долговечная фиксация без побочных эффектов
Заголовок раздела «Долговечная фиксация без побочных эффектов»OutputCommitterInterface::commit() публикует отрисованные байты в их финальный пункт назначения ровно один раз: атомарно (частичный объект никогда не наблюдается), идемпотентно (повторная фиксация побайтно идентичного содержимого не выполняет записи и возвращает CommitReceipt с idempotentReuse = true — свежую квитанцию, а не исходную), без тихой перезаписи (расходящиеся байты на занятый ключ без overwrite вызывают конфликт) и с проверкой целостности (фиксатор пересчитывает дайджест перед записью). LocalFilesystemCommitter реализует это для локальной файловой системы.
Восстановление из контрольных точек
Заголовок раздела «Восстановление из контрольных точек»RunCheckpoint — это долговечный барьер, фиксирующий, сколько элементов прогон зафиксировал, плюс снимок состояния с ключами. При восстановлении процессор перематывает вперёд за зафиксированное смещение и восстанавливает состояние с ключами, поэтому сбой в середине прогона возобновляется без повторной публикации зафиксированного вывода. FilesystemCheckpointStore сохраняет каждый барьер атомарно.
Дедупликация по идемпотентности, повторы и недоставленные сообщения
Заголовок раздела «Дедупликация по идемпотентности, повторы и недоставленные сообщения»Хранилище идемпотентности — это быстрый путь, который позволяет процессору сократить путь до отрисовки повторно поступившего манифеста; сравнение дайджестов в фиксаторе остаётся долговечной гарантией ровно один раз, поэтому потерянная запись о дедупликации в худшем случае приводит к напрасной повторной отрисовке, которую фиксатор дедуплицирует. RetryPolicy обеспечивает ограниченную, детерминированную экспоненциальную задержку для временных сбоев (тайм-аутов); задание, исчерпавшее свой бюджет, захватывается в DeadLetterStoreInterface, а не теряется. Каждое хранилище поставляется в варианте в памяти (область одного прогона / тестов) и в долговечном варианте для файловой системы.
Маркер долговечности
Заголовок раздела «Маркер долговечности»Хранилища, состояние которых переживает перезапуск процесса, реализуют маркер DurableCapability. Устойчивый к сбоям прогон требует, чтобы каждый коллаборатор был долговечным, поэтому он падает быстро, а не обещает семантику ровно один раз, которую хранилище в памяти не может сохранить через перезапуск.
Пример кода — Быстрый старт
Заголовок раздела «Пример кода — Быстрый старт»Отрисуйте один манифест и зафиксируйте его байты ровно один раз. Движок возвращает байты плюс дайджест; фиксатор публикует их.
<?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";}Пример кода — Продакшн
Заголовок раздела «Пример кода — Продакшн»Отрисуйте пакет, направьте тайм-ауты в политику повторов и отправьте терминальные сбои в недоставленные сообщения. Фиксация отказывается перезаписать расходящиеся байты, поэтому коллизия ключа перехватывается и захватывается, а не теряется.
<?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");}Когда использовать
Заголовок раздела «Когда использовать»- Высокообъёмная пакетная отрисовка, где пропускная способность выигрывает от конкурентного (через пул процессов) выполнения.
- Долгие прогоны, которые должны пережить сбой и возобновиться без двойной публикации вывода.
- Конвейеры, которые должны гарантировать доставку каждого отрисованного документа в его цель ровно один раз.
Для одного разового документа отрисовывайте напрямую модулем Writer; ценность Stream — в долговечных, возобновляемых, конкурентных пакетах.
Производительность
Заголовок раздела «Производительность»Пропускная способность масштабируется с числом воркеров в ProcessPoolRenderUnitExecutor (ограничена maxWorkers и maxBatchSize), при этом движок сохраняет вывод отрисовки побайтно идентичным последовательному эталону. Тайм-аут по реальному времени ограничивает каждый параллельный пакет, поэтому зависший воркер не может блокировать процесс навсегда. Опубликованного фиксированного показателя пропускной способности нет; он зависит от сложности документа и параллелизма хоста. Измеряйте на репрезентативных документах.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»Манифесты проверяются с отказом в закрытое состояние перед отрисовкой. Фиксатор отклоняет обход пути, нулевые байты, схемы потоковых обёрток, цели по символическим ссылкам и векторы альтернативных потоков данных NTFS (двоеточие), а каждый ключ разрешает в пределах одного настроенного корня. Результаты межпроцессных воркеров перехэшируются и сверяются с дайджестом, сообщённым воркером, поэтому искажённый воркер не может тихо испортить вывод. Этот модуль не журналирует содержимое документа.
Замечание о границе с Enterprise
Заголовок раздела «Замечание о границе с Enterprise»Долговечные хранилища Stream здесь основаны на файловой системе и привязаны к одному хосту. Межхостовая конкурентная фиксация ровно один раз на тот же ключ и долговечная дедупликация между прогонами — это работа фиксаторов и хранилищ Enterprise на объектном хранилище; процессор потока заданий-документов, управляющий этими коллабораторами, относится к компетенции Enterprise. Pro предоставляет движок, контракты и локальные долговечные реализации.
Резервный вариант / альтернатива в Core
Заголовок раздела «Резервный вариант / альтернатива в Core»Без Pro отрисовывайте документы по одному с помощью writer-а NextPDF Core; долговечный пакетный потоковый вывод, конкурентное выполнение и фиксация ровно один раз — это дополнения Pro. См. /modules/writer/.
Граница публикации
Заголовок раздела «Граница публикации»Эта страница документирует только внешне наблюдаемое поведение и поддерживаемую публичную поверхность API. Внутренние пути пространств имён, вспомогательные классы, таблицы механизмов, имена файлов runbook и префиксы тикетов — вне области рассмотрения.