ข้ามไปยังเนื้อหา
getnextpdf.com

Pro รุ่น

Stream

โมดูล Stream เรนเดอร์เอกสารทีละ batch อย่างคงทนและทำงานพร้อมกัน พร้อม การ commit ภายในเครื่องแบบ exactly-once ไปยังที่จัดเก็บคงทนบนโฮสต์เดียว (exactly-once ข้ามโฮสต์เป็นขอบเขตของ Enterprise Stream) มันแบ่งงานออกเป็นสองความรับผิดชอบที่แยกออกจากกันอย่างชัดเจน คือเอนจินเรนเดอร์ที่แปลงแมนิเฟสต์ที่ตรวจสอบแล้วให้เป็นไบต์ (และไม่ทำอย่างอื่น) กับชุดที่จัดเก็บแบบคงทน ได้แก่ committer, checkpoint, idempotency, dead-letter ซึ่งเผยแพร่ไบต์เหล่านั้นอย่างปลอดภัยและให้การรันดำเนินต่อได้หลังการล่มโดยไม่เผยแพร่เอาต์พุตที่ commit ไปแล้วซ้ำ

ความสามารถนี้มาพร้อมกับ NextPDF Pro (nextpdf/pro) และเปิดใช้งานด้วยซองสิทธิ์ใช้งานระดับ Pro การติดตั้งที่ไม่มีสิทธิ์ดังกล่าวจะไม่โหลดคลาสของความสามารถนี้ เปรียบเทียบรุ่นและขอรับสิทธิ์ใช้งาน

ไม่มีแฟล็กสิทธิ์ใช้งานแยกต่างหากรายฟีเจอร์ การทำงานพร้อมกัน (จำนวน worker) ขนาด batch งบประมาณการลองใหม่ และแบ็กเอนด์ที่จัดเก็บ (ในหน่วยความจำเทียบกับระบบไฟล์แบบคงทน) เป็นพารามิเตอร์ในขณะรันไทม์ ไม่ใช่สวิตช์สิทธิ์ใช้งาน

Terminal window
composer require nextpdf/pro:^3

โค้ดอยู่ภายใต้เนมสเปซ NextPDF\Pro\Stream

Stream จัดระเบียบรอบ ๆ รอยต่อที่ถูกตรึงไว้ — NextPDF\Pro\Stream\Engine\RenderEngineInterface — ซึ่งแยกเอนจินด้านปริมาณงานออกจากความหมายของสตรีม:

  • เอนจินเรนเดอร์เป็นเจ้าของการทำงานพร้อมกันและหน่วยความจำที่มีขอบเขต มันเรนเดอร์หน้าต่างของแมนิเฟสต์ที่ผ่านการตรวจสอบและกำจัดซ้ำมาก่อนแล้วผ่าน renderBatch() และคืนค่า EngineRenderResult หนึ่งรายการต่อแมนิเฟสต์ ตามลำดับของอินพุต ที่สำคัญคือเอนจิน ปราศจาก side-effect เมื่อเทียบกับเอาต์พุตสุดท้าย: มันคืนค่าไบต์ที่เรนเดอร์แล้วพร้อมไดเจสต์ sha-256 ของไบต์เหล่านั้น โดยไม่เขียนไปยังคีย์อ็อบเจกต์สุดท้ายเลย ความบริสุทธิ์นั้นเองคือสิ่งที่ทำให้การส่งมอบแบบ exactly-once เป็นไปได้
  • ผู้ทำงานร่วมของสตรีมเป็นเจ้าของการส่งมอบ ตัว committer, checkpoint store, idempotency (dedup) store และ dead-letter store เป็นผู้ตัดสินว่าไบต์จะลงที่ใด การรันจะดำเนินต่ออย่างไร งานใดเป็นการเล่นซ้ำ และเกิดอะไรขึ้นกับความล้มเหลวขั้นสิ้นสุด

ความล้มเหลวในการเรนเดอร์ต่อแมนิเฟสต์จะถูกรายงานเป็นผลลัพธ์ Failed (หรือ Timeout) รายรายการ มันไม่เคยยกเลิกทั้ง batch ซองหุ้ม batch จะสำเร็จเสมอพร้อมผลลัพธ์รายรายการ

  • InProcessRenderEngine คือเกณฑ์อ้างอิงความถูกต้องแบบซิงโครนัสกระบวนการเดียว มันตรวจสอบแต่ละแมนิเฟสต์แบบ fail-closed ผ่าน RenderManifestValidator ที่มาพร้อมกัน ก่อนเรนเดอร์ผ่าน SingleDocumentRenderer ของ Core ดังนั้นแมนิเฟสต์ที่ไม่ถูกต้องจะกลายเป็นความล้มเหลวรายรายการ (รหัสข้อผิดพลาด SPEC-MANIFEST-INVALID) แทนที่จะไปถึงตัวเรนเดอร์
  • ConcurrentRenderEngine กระจาย batch ออกไปยัง RenderUnitExecutorInterface และคืนลำดับ batch ที่กำหนดได้แน่นอนตาม unit index เอาต์พุตจะเหมือนกันในระดับไบต์กับการเรนเดอร์แบบลำดับ ไม่ว่าจะเสร็จในลำดับใด การเสร็จที่ขาดหาย ซ้ำ หรือไม่รู้จักจะเป็นความล้มเหลวขั้นเด็ดขาด ไม่ใช่การทิ้งแบบเงียบ ๆ
  • executor คือรอยต่อของการทำงานพร้อมกัน InlineRenderUnitExecutor คือเกณฑ์อ้างอิงที่กำหนดได้แน่นอน ส่วน ProcessPoolRenderUnitExecutor กระจาย batch ไปยัง subprocess ของ worker php ได้สูงสุด N ตัวที่เรนเดอร์ขนานกัน แล้วเก็บรวบรวมและตรวจสอบความสมบูรณ์ของผลลัพธ์

OutputCommitterInterface::commit() เผยแพร่ไบต์ที่เรนเดอร์แล้วไปยังปลายทางสุดท้ายเพียงครั้งเดียวเท่านั้น (exactly once): แบบ atomic (ไม่เคยมีการสังเกตเห็นอ็อบเจกต์ที่ไม่สมบูรณ์), แบบ idempotent (การ commit เนื้อหาที่เหมือนกันในระดับไบต์ซ้ำจะไม่ทำการเขียนใด ๆ และคืน CommitReceipt ที่มี idempotentReuse = true — ใบเสร็จใหม่ ไม่ใช่ใบเดิม), ไม่มีการทับซ้อนแบบเงียบ ๆ (ไบต์ที่ต่างกันไปยังคีย์ที่มีอยู่แล้วโดยไม่มี overwrite จะทำให้เกิดความขัดแย้ง) และตรวจสอบความสมบูรณ์ (committer คำนวณไดเจสต์ใหม่ก่อนเขียน) LocalFilesystemCommitter ทำสิ่งนี้สำหรับระบบไฟล์ภายใน

RunCheckpoint คือสิ่งกีดขวางแบบคงทนที่บันทึกว่าการรันได้ commit ไปกี่รายการ พร้อมกับ snapshot ของสถานะแบบมีคีย์ เมื่อกู้คืน processor จะเดินหน้าข้ามอย่างรวดเร็วผ่านออฟเซ็ตที่ commit แล้วและคืนสถานะแบบมีคีย์ ดังนั้นการล่มกลางการรันจะดำเนินต่อโดยไม่เผยแพร่เอาต์พุตที่ commit ไปแล้วซ้ำ FilesystemCheckpointStore คงค่าสิ่งกีดขวางแต่ละอันแบบ atomic

idempotency store คือ เส้นทางเร็ว ที่ให้ processor ลัดวงจรก่อนเรนเดอร์แมนิเฟสต์ที่ถูกเล่นซ้ำ ส่วนการเปรียบเทียบไดเจสต์ของ committer ยังคงเป็นหลักประกัน exactly-once แบบคงทน ดังนั้นบันทึก dedup ที่สูญหายอย่างเลวร้ายที่สุดทำให้เกิดการเรนเดอร์ซ้ำที่สูญเปล่าซึ่ง committer จะกำจัดซ้ำให้ RetryPolicy ให้การถอยกลับแบบเลขชี้กำลังที่มีขอบเขตและกำหนดได้แน่นอนสำหรับความล้มเหลวชั่วคราว (timeout) งานที่ใช้งบประมาณจนหมดจะถูกเก็บไว้ใน DeadLetterStoreInterface แทนที่จะสูญหาย ที่จัดเก็บแต่ละแห่งมาพร้อมรูปแบบในหน่วยความจำ (ขอบเขตการรันเดียว / การทดสอบ) และรูปแบบระบบไฟล์แบบคงทน

ที่จัดเก็บซึ่งสถานะอยู่รอดการรีสตาร์ทกระบวนการได้จะทำตามเครื่องหมาย DurableCapability การรันที่ปลอดภัยต่อการล่มต้องให้ผู้ทำงานร่วมทุกตัวเป็นแบบคงทน เพื่อให้มันล้มเหลวอย่างรวดเร็วแทนที่จะให้สัญญาความหมายแบบ exactly-once ที่ที่จัดเก็บในหน่วยความจำไม่สามารถรักษาไว้ได้ข้ามการรีสตาร์ท

เรนเดอร์แมนิเฟสต์หนึ่งรายการและ commit ไบต์ของมันเพียงครั้งเดียวเท่านั้น เอนจินคืนค่าไบต์พร้อมไดเจสต์ committer เผยแพร่ไบต์เหล่านั้น

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

เรนเดอร์ batch หนึ่ง กำหนดเส้นทาง timeout ไปยังนโยบายการลองใหม่ และส่งความล้มเหลวขั้นสิ้นสุดไป dead-letter การ commit ปฏิเสธที่จะทับไบต์ที่ต่างกัน ดังนั้นการชนกันของคีย์จะถูกจับและเก็บไว้แทนที่จะสูญหาย

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");
}
  • การเรนเดอร์ batch ปริมาณสูงที่ปริมาณงานได้ประโยชน์จากการดำเนินการแบบพร้อมกัน (process-pool)
  • การรันที่ใช้เวลานานซึ่งต้องอยู่รอดการล่มและดำเนินต่อโดยไม่เผยแพร่เอาต์พุตซ้ำซ้อน
  • ไปป์ไลน์ที่ต้องรับประกันการส่งมอบเอกสารที่เรนเดอร์แล้วแต่ละชิ้นไปยังเป้าหมายเพียงครั้งเดียวเท่านั้น

สำหรับเอกสารเดี่ยวแบบเฉพาะกิจ ให้เรนเดอร์โดยตรงด้วยโมดูล Writer คุณค่าของ Stream อยู่ที่ batch ที่คงทน ดำเนินต่อได้ และทำงานพร้อมกัน

ปริมาณงานขยายตามจำนวน worker ใน ProcessPoolRenderUnitExecutor (มีขอบเขตจาก maxWorkers และ maxBatchSize) ขณะที่เอนจินรักษาเอาต์พุตการเรนเดอร์ให้เหมือนกันในระดับไบต์กับเกณฑ์อ้างอิงแบบลำดับ การหมดเวลา wall-clock จำกัด batch ขนานแต่ละชุด เพื่อไม่ให้ worker ที่ค้างบล็อกได้ตลอดไป ไม่มีตัวเลขปริมาณงานคงที่ที่เผยแพร่ มันขึ้นอยู่กับความซับซ้อนของเอกสารและความขนานของโฮสต์ วัดผลด้วยเอกสารที่เป็นตัวแทน

แมนิเฟสต์ถูกตรวจสอบแบบ fail-closed ก่อนเรนเดอร์ committer ปฏิเสธการ traversal พาธ ไบต์ null สคีมแบบ stream-wrapper เป้าหมายที่เป็น symlink และเวกเตอร์ NTFS alternate-data-stream (โคลอน) และแก้ทุกคีย์ให้อยู่ภายใต้รากที่กำหนดค่าไว้หนึ่งราก ผลลัพธ์ของ worker ข้ามกระบวนการถูกแฮชใหม่และจับคู่กับไดเจสต์ที่ worker รายงาน เพื่อไม่ให้ worker ที่เพี้ยนทำให้เอาต์พุตเสียหายแบบเงียบ ๆ โมดูลนี้ไม่บันทึกเนื้อหาเอกสาร

ที่จัดเก็บแบบคงทนของ Stream ที่นี่ใช้ระบบไฟล์รองรับและเป็นแบบโฮสต์เดียว ส่วน exactly-once พร้อมกันข้ามโฮสต์ไปยังคีย์เดียวกัน และการกำจัดซ้ำแบบคงทนข้ามการรัน เป็นหน้าที่ของ committer และที่จัดเก็บแบบ object-storage ของ Enterprise ส่วน document-job stream processor ที่ขับเคลื่อนผู้ทำงานร่วมเหล่านี้เป็นเรื่องของ Enterprise Pro ให้เอนจิน สัญญา และการนำไปใช้แบบคงทนภายในเครื่อง

หากไม่มี Pro ให้เรนเดอร์เอกสารทีละชิ้นด้วย writer ของ NextPDF Core ส่วนการสตรีม batch แบบคงทน การดำเนินการแบบพร้อมกัน และการ commit แบบ exactly-once เป็นส่วนเพิ่มของ Pro ดู /modules/writer/

หน้านี้บันทึกเฉพาะพฤติกรรมที่สังเกตได้จากภายนอกและพื้นผิว public API ที่รองรับเท่านั้น พาธเนมสเปซภายใน คลาสตัวช่วย ตารางกลไก ชื่อไฟล์ runbook และคำนำหน้าตั๋วอยู่นอกขอบเขต