跳转到内容
getnextpdf.com

Pro 版本

Stream — 深度参考

本页记录 NextPDF\Pro\Stream 子系统在概览页之外的公共契约、类、方法与失败模式。下文每一个类型都属于已记录的 Pro 公开范围。

此能力随 NextPDF Pronextpdf/pro)发布,并通过一个 Pro 层级的许可信封激活。一个不具备该权限的部署不会加载此能力的类。比较版本并获取授权

不存在逐功能的许可标志;代码随 Pro 版本一同发布。worker 数量、批大小、重试预算,以及存储后端都是运行时参数。

NextPDF\Pro\Stream\Engine\RenderEngineInterface 是吞吐引擎与文档作业流处理器之间的契约。引擎实现它(掌管并发、worker 池生命周期、背压、有界内存);流处理器消费它(掌管带键状态、去重、重试、检查点,以及恰好一次提交)。引擎返回字节加 sha-256,绝不返回已提交位置——正是这种无副作用让处理器能够暂存、提交并恰好一次地建立检查点。

public function renderBatch(array $manifests, array $variablesByJobId = []): array; // list<EngineRenderResult>, input order
public function maxBatchSize(): int; // int<1, max> backpressure hint
public function isAvailable(): bool;

$manifests 是一个大小至多为 maxBatchSize()list<RenderManifest>$variablesByJobId 把 job id 映射到 array<string, scalar> 模板变量。单个清单的失败是单条目的 Failed/Timeout 结果,绝不会中止整个批次。

同步、单进程基线。在通过 Core 的 SingleDocumentRenderer 渲染之前,通过 RenderManifestValidator 对每个清单进行失败关闭校验(16 MiB 内联载荷上限、符合性/签名白名单、sha-256 内容哈希格式、BCP-47 区域设置语法)。一个阻断性校验错误会短路到 EngineRenderResult::failed(jobId, 'SPEC-MANIFEST-INVALID', ...);一个渲染异常会变成 'SPEC-RENDER-EXCEPTION'。构造函数:__construct(SingleDocumentRenderer $renderer, int $maxBatchSize = 64, ?RenderManifestValidator $validator = null)——maxBatchSize < 1 抛出 InvalidArgumentExceptionisAvailable() 始终为 true

NextPDF\Pro\Stream\Engine\ConcurrentRenderEngine

标题为“NextPDF\Pro\Stream\Engine\ConcurrentRenderEngine”的章节

final readonly__construct(RenderUnitExecutorInterface $executor)。把每个清单包装进一个带索引的 RenderUnit,通过执行器运行它们,并按索引重新排序完成项,因此输出与顺序渲染逐字节一致。一个落在 [0, count) 之外的完成索引抛出 RenderEngineException::unknownUnit();一个重复索引抛出 duplicateResult();一个缺失索引抛出 missingResult()maxBatchSize()isAvailable() 委托给执行器。

NextPDF\Pro\Stream\Engine\RenderUnitExecutorInterface

标题为“NextPDF\Pro\Stream\Engine\RenderUnitExecutorInterface”的章节
public function execute(array $units): iterable; // iterable<CompletedRenderUnit>, any order
public function maxBatchSize(): int;
public function isAvailable(): bool;

实现可以以任意顺序产出完成项;ConcurrentRenderEngine 按索引恢复顺序。

NextPDF\Pro\Stream\Engine\InlineRenderUnitExecutor

标题为“NextPDF\Pro\Stream\Engine\InlineRenderUnitExecutor”的章节

final readonly__construct(RenderEngineInterface $inner)。通过内层引擎按顺序渲染每个单元——一个并行执行器必须逐字节匹配的确定性正确性参照。无时间、进程、线程或随机性。

NextPDF\Pro\Stream\Engine\ProcessPoolRenderUnitExecutor

标题为“NextPDF\Pro\Stream\Engine\ProcessPoolRenderUnitExecutor”的章节

final readonly。把一个批次分发到至多 maxWorkers 个并行渲染的 php worker 子进程(每个一块);输出与内联基线逐字节一致。构造函数:

__construct(
int $maxWorkers = 4,
int $maxBatchSize = 64,
?string $phpBinary = null,
?string $workerScript = null,
?string $autoload = null,
?int $timeoutSeconds = 300, // null disables the wall-clock watchdog
)

健壮性契约:

  • 无死锁、Windows 安全。 单元载荷与结果通过临时文件而非管道传输;父进程轮询 proc_get_status(),且只有在一个 worker 退出之后才把管道抽干至 EOF,因此一个 worker 无法卡住父进程。
  • 有界等待。 timeoutSeconds 为整个并行渲染设上限;到期时每个仍在运行的 worker 都会被终止,并抛出一个 RenderEngineException
  • 资源卫生。 一个 finally 会关闭管道、对存活的 worker 做一次有界的终止并回收尝试(优雅终止 → 强制 kill → 回收;在有界宽限期内未被观察到停止的子进程会被放弃,而非冒着无限阻塞的风险),并在所有路径上 unlink 每个临时文件。
  • 可信关联。 每个 worker 必须恰好返回它被分配的索引集合(无缺失、重复或外来索引);每份渲染结果的字节都会被重新哈希,并与 worker 上报的 sha-256 比对,任何不是 rendered/failed 的状态都会硬失败。单个清单的渲染失败是单元级的 Failed 结果;只有基础设施层面的故障(非零退出、不可读/被搅乱的输出、超时)才会使执行器硬失败。

isAvailable() 要求 autoload 文件和 worker 脚本都存在。一个非正的界限或负的超时抛出 InvalidArgumentException

final readonly——int<0, max> $indexRenderManifest $manifestarray<string, scalar> $variables。关联是按 index,绝不按 job id(job id 不保证在一个批次内唯一)。

final readonly——int $index(不可信,由引擎校验)、EngineRenderResult $result

final readonly。字段:jobIdEngineRenderStatus $status?string $bytes?string $sha256int $pageCount?string $errorCode?string $errorMessagearray<non-empty-string, float> $timings。工厂方法:rendered(jobId, bytes, sha256, pageCount, timings = [])failed(jobId, errorCode, errorMessage)timedOut(jobId, errorMessage)(码 SPEC-ENGINE-TIMEOUT)。isRendered() 报告状态。一个 rendered 结果携带字节与一个摘要,绝不携带已提交位置。

字符串支撑的枚举:RenderedFailedTimeoutisRetryable() 仅对 Timeouttrue,因此调用方无需重新检视错误即可把一个超时归类为瞬时。

NextPDF\Pro\Stream\Commit\OutputCommitterInterface

标题为“NextPDF\Pro\Stream\Commit\OutputCommitterInterface”的章节
public function commit(
string $jobId,
OutputObjectKey $target,
string $bytes,
string $sha256,
bool $overwrite = false,
): CommitReceipt;

恰好一次发布:原子、幂等(逐字节相同的重新提交不执行写入,并返回一个 idempotentReuse = trueCommitReceipt——一张全新的回执,而非原始那张;其 committedAt 是当前时钟值)、不静默覆盖,以及经完整性校验(committer 重算摘要)。失败模式:CommitIntegrityException(声明的 sha-256 与字节不匹配)、OutputCommitConflictException(在 overwrite = false 下向已占用的键写入有差异的字节)、UnsupportedTargetException(不支持的目标方案)。

NextPDF\Pro\Stream\Commit\LocalFilesystemCommitter

标题为“NextPDF\Pro\Stream\Commit\LocalFilesystemCommitter”的章节

final readonly,实现 OutputCommitterInterface, DurableCapability__construct(string $rootDirectory, ?AtomicFileWriter $writer = null, ?ClockInterface $clock = null)。仅服务 file 方案;将每个目标解析到一个配置好的根目录之下,并通过一个原子 writer 写入(O_EXCL 临时文件 → fsync → 同卷重命名)。整个临界区(包括父目录创建)在一个保存在输出键空间之外的逐根锁文件上以排他 flock 运行,并且若锁无法打开或获取,提交会失败关闭。它拒绝符号链接的最终组件,以及任何含冒号的键(NTFS 备用数据流向量)。跨主机并发地向同一个键恰好一次写入需要持久的 Enterprise committer。一个本身是系统临时目录、或包含系统临时目录的根目录抛出 InvalidArgumentException

final readonly——jobIdOutputObjectKey $targetsha256int<0, max> $bytesWrittenbool $idempotentReuseDateTimeImmutable $committedAttoArray() / fromArray() 完全可往返(target 是结构化的,不是一个有损 URI);fromArray() 是严格的,在字段缺失或格式错误时抛出 InvalidArgumentException

NextPDF\Pro\Stream\Checkpoint\CheckpointStoreInterface

标题为“NextPDF\Pro\Stream\Checkpoint\CheckpointStoreInterface”的章节

load(string $runId): ?RunCheckpointsave(RunCheckpoint $checkpoint): void(持久且原子——读取方绝不会看到一个写到一半的检查点)。

final readonly——runIdint<0, max> $committedOffsetarray $keyedStateDateTimeImmutable $updatedAtSCHEMA_VERSION = '1.0'。工厂方法 start(runId, at)advancedTo(committedOffset, keyedState, at)toArray()/toJson()/fromArray()/fromJson() 对其进行序列化;fromArray() 要求一个非空的 run id 和一个有效的 updated_at,拒绝一个不兼容(非 1.x)的 schema_version,并通过在每一层丢弃任何不可 JSON 序列化的值来规范化带键状态,因此恢复出的状态始终可重新序列化。在恢复时,处理器快进越过 committedOffset 并恢复带键状态;在最后一道屏障之后被改变的状态会被向前重算,绝不是错误,因为持久的恰好一次来自 committer 的摘要去重。

NextPDF\Pro\Stream\Checkpoint\FilesystemCheckpointStore

标题为“NextPDF\Pro\Stream\Checkpoint\FilesystemCheckpointStore”的章节

final readonly,实现 CheckpointStoreInterface, DurableCapability。每次运行一个 JSON 文件,原子写入。run id 必须匹配 [A-Za-z0-9._-]+ 且不含 ..;一个不存在的目录抛出 InvalidArgumentException

NextPDF\Pro\Stream\Dedup\IdempotencyStoreInterface

标题为“NextPDF\Pro\Stream\Dedup\IdempotencyStoreInterface”的章节

isCommitted(IdempotencyKey $key): boolmarkCommitted(IdempotencyKey $key, CommitReceipt $receipt): voidreceiptFor(IdempotencyKey $key): ?CommitReceipt。在渲染一个重放之前短路的快速路径;committer 的摘要比对仍然是持久保证,因此一条丢失的记录最坏只会浪费一次会被 committer 去重的重新渲染。

  • InMemoryIdempotencyStore——单次运行/测试范围(崩溃时丢失)。
  • FilesystemIdempotencyStore——DurableCapability;每个已提交的键一个原子 JSON 文件(序列化后的回执),以键值的一个哈希命名。标记是幂等的;一次并发的重新标记会在一个文件上无害地竞争。一个不存在的目录抛出 InvalidArgumentException

final readonly——positive-int $maxAttemptspositive-int $baseDelayMspositive-int $maxDelayMs__construct(int $maxAttempts = 3, int $baseDelayMs = 100, int $maxDelayMs = 30000),不变量为 maxAttempts >= 11 <= baseDelayMs <= maxDelayMs <= 7 days(否则 InvalidArgumentException)。工厂方法 default()none()(单次尝试)。shouldRetry(int $attempt): booldelayMsForAttempt(int $attempt): int<0, max> 是确定性指数退避 baseDelayMs * 2^(attempt-1),封顶于 maxDelayMs(无内置抖动;请在调用处施加)。

NextPDF\Pro\Stream\Retry\DeadLetterStoreInterface

标题为“NextPDF\Pro\Stream\Retry\DeadLetterStoreInterface”的章节

add(DeadLetterRecord $record): voidall(): list<DeadLetterRecord>count(): int<0, max>

final readonly——jobIdidempotencyKeyValuepositive-int $attemptslastErrorCodelastErrorMessageDateTimeImmutable $failedAt、可选 ?string $runId、可选 int<1, max> $sourceOffset。当 runIdsourceOffset 两者皆知时,dedupKey()runId:sourceOffset,否则是幂等键值。fromArray()failed_at 严格解析为 ATOM(拒绝相对或非 ATOM 表达式),因此序列化/反序列化保持对称。

  • InMemoryDeadLetterStore——单次运行/测试范围。
  • FilesystemDeadLetterStore——DurableCapability;每条记录一个原子 JSON 文件,以去重键的一个 SHA-256 哈希命名(….dlq.json),因此在恢复时重新添加同一条目是幂等的。all() 以确定性(排序后)顺序读取记录,并通过抛出来暴露一条损坏记录;count() 是一次廉价的文件计数,而非有效性检查。

NextPDF\Pro\Stream\State\KeyedStateStoreInterface

标题为“NextPDF\Pro\Stream\State\KeyedStateStoreInterface”的章节

hasgetputremoveclear,外加用于检查点边界的 snapshot(): arrayrestore(array $snapshot): void。值必须可 JSON 序列化。对于默认的渲染并提交工作负载,不使用任何带键状态;它的存在是为聚合/窗口化扩展服务。InMemoryKeyedStateStore 是单次运行的实现;对于默认工作负载,恢复时丢失它在语义上是无操作,因为恰好一次来自 committer 的摘要去重。

final readonly__construct(string $tenantField = 'tenant_id', string $documentField = 'document_id')keyFor(RenderManifest $manifest): non-empty-string 从清单元数据派生分区键,形如 rawurlencode(tenant):rawurlencode(document)(这种编码阻止 ("a:b","c")("a","b:c") 碰撞),并在任一字段缺失时回退到 job id——因此每个清单都解析为一个稳定、非空的键。

NextPDF\Pro\Stream\DurableCapability 是一个标记接口,用于任何状态可在进程重启后留存的存储/committer。一次崩溃安全的运行要求每个协作者都实现它,因此它会快速失败,而不会承诺一个内存式存储无法兑现的恰好一次。

所有子系统异常都实现 NextPDF\Pro\Stream\Exception\StreamException(继承 Throwable),因此调用方可以统一地 catch (StreamException)

  • RenderEngineExceptionRuntimeException)——执行器违反了批次契约(未知、重复或缺失单元;worker 故障;超时)。
  • CommitIntegrityExceptionRuntimeException)——声明的 sha-256 与载荷不匹配;spec 码 SPEC-COMMIT-422
  • OutputCommitConflictExceptionRuntimeException)——在 overwrite 关闭下向已占用的键写入有差异的字节;spec 码 SPEC-COMMIT-409(通过 specCode() 暴露)。
  • UnsupportedTargetExceptionInvalidArgumentException)——committer 无法服务的目标方案。

引擎依据 Core 清单模型校验清单,并产出确定性的字节加 sha-256 摘要;committer 强制执行原子、经完整性校验的恰好一次写入。本模块除 sha-256 内容摘要外不执行任何密码学操作,也不定义任何 FIPS 特定行为。

  • renderBatch() 绝不会因单个清单失败而中止;请检视每一个 EngineRenderResult
  • ProcessPoolRenderUnitExecutor 严格按索引关联并重新哈希 worker 字节;一个有缺陷的 worker 会硬失败,而非损坏输出。
  • LocalFilesystemCommitter 是单主机的;跨主机恰好一次需要持久的 Enterprise committer。
  • 崩溃安全的运行必须自始至终使用 DurableCapability(文件系统)存储,而非内存式变体。

本页仅记录外部可观察的行为与受支持的公共 API 范围。内部命名空间路径、辅助类、机制表、runbook 文件名,以及工单前缀均不在范围之内。