Перейти к содержимому
getnextpdf.com

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. Устойчивый к сбоям прогон требует, чтобы каждый коллаборатор был долговечным, поэтому он падает быстро, а не обещает семантику ровно один раз, которую хранилище в памяти не может сохранить через перезапуск.

Отрисуйте один манифест и зафиксируйте его байты ровно один раз. Движок возвращает байты плюс дайджест; фиксатор публикует их.

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

Отрисуйте пакет, направьте тайм-ауты в политику повторов и отправьте терминальные сбои в недоставленные сообщения. Фиксация отказывается перезаписать расходящиеся байты, поэтому коллизия ключа перехватывается и захватывается, а не теряется.

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");
}
  • Высокообъёмная пакетная отрисовка, где пропускная способность выигрывает от конкурентного (через пул процессов) выполнения.
  • Долгие прогоны, которые должны пережить сбой и возобновиться без двойной публикации вывода.
  • Конвейеры, которые должны гарантировать доставку каждого отрисованного документа в его цель ровно один раз.

Для одного разового документа отрисовывайте напрямую модулем Writer; ценность Stream — в долговечных, возобновляемых, конкурентных пакетах.

Пропускная способность масштабируется с числом воркеров в ProcessPoolRenderUnitExecutor (ограничена maxWorkers и maxBatchSize), при этом движок сохраняет вывод отрисовки побайтно идентичным последовательному эталону. Тайм-аут по реальному времени ограничивает каждый параллельный пакет, поэтому зависший воркер не может блокировать процесс навсегда. Опубликованного фиксированного показателя пропускной способности нет; он зависит от сложности документа и параллелизма хоста. Измеряйте на репрезентативных документах.

Манифесты проверяются с отказом в закрытое состояние перед отрисовкой. Фиксатор отклоняет обход пути, нулевые байты, схемы потоковых обёрток, цели по символическим ссылкам и векторы альтернативных потоков данных NTFS (двоеточие), а каждый ключ разрешает в пределах одного настроенного корня. Результаты межпроцессных воркеров перехэшируются и сверяются с дайджестом, сообщённым воркером, поэтому искажённый воркер не может тихо испортить вывод. Этот модуль не журналирует содержимое документа.

Долговечные хранилища Stream здесь основаны на файловой системе и привязаны к одному хосту. Межхостовая конкурентная фиксация ровно один раз на тот же ключ и долговечная дедупликация между прогонами — это работа фиксаторов и хранилищ Enterprise на объектном хранилище; процессор потока заданий-документов, управляющий этими коллабораторами, относится к компетенции Enterprise. Pro предоставляет движок, контракты и локальные долговечные реализации.

Без Pro отрисовывайте документы по одному с помощью writer-а NextPDF Core; долговечный пакетный потоковый вывод, конкурентное выполнение и фиксация ровно один раз — это дополнения Pro. См. /modules/writer/.

Эта страница документирует только внешне наблюдаемое поведение и поддерживаемую публичную поверхность API. Внутренние пути пространств имён, вспомогательные классы, таблицы механизмов, имена файлов runbook и префиксы тикетов — вне области рассмотрения.