การสร้าง PDF แบบกลุ่มผ่าน Connect พร้อมการติดตามความคืบหน้า
ภาพรวมโดยสังเขป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสังเขป”ใช้กระบวนการไคลเอนต์เพียงกระบวนการเดียวรันรายการเอกสารจนเสร็จสมบูรณ์ผ่าน NextPDF Connect ซึ่งเป็นบริการ HTTP แบบสแตนด์อโลนของเอนจิน สูตรนี้ส่งคำขอเรนเดอร์แต่ละรายการไปยังจุดปลายงานอะซิงโครนัส POST /api/v1/jobs สำรวจแต่ละงานด้วย GET /api/v1/jobs/{id} จนกว่าจะเข้าสู่สถานะปลายทาง อ่านฟิลด์ status และ progress ที่เซิร์ฟเวอร์รายงานสำหรับแต่ละงาน แล้วดาวน์โหลด PDF ที่เสร็จสมบูรณ์ทุกไฟล์จาก GET /api/v1/jobs/{id}/result
วงจรชีวิตของงานมีรูปแบบตายตัวและมีไม่กี่สถานะ งานหนึ่งจะอยู่ในสถานะ pending จากนั้น running แล้วจึงเข้าสู่สถานะปลายทางเพียงหนึ่งสถานะ ได้แก่ completed failed หรือ cancelled การตอบกลับสถานะจะมีค่า progress เป็นจำนวนเต็มตั้งแต่ 0 ถึง 100 เมื่อเซิร์ฟเวอร์ติดตามค่านี้ และในการสำรวจทุกครั้งที่ยังไม่เข้าสู่สถานะปลายทางจะมีส่วนหัว Retry-After เพื่อบอกว่าควรส่งคำขอถัดไปเมื่อใด กำหนดคีย์ให้การส่งแต่ละครั้งด้วย Idempotency-Key เพื่อให้การส่งซ้ำคืนงานเดิมแทนที่จะเริ่มเรนเดอร์ครั้งที่สอง
สูตรนี้ทำงานที่ระดับ wire โดยเรียกพื้นผิว REST โดยตรงและไม่สันนิษฐานว่ามีชุดพัฒนาซอฟต์แวร์ (SDK) เฉพาะภาษา ดังนั้นจึงพอร์ตขั้นตอนเดียวกันไปยัง HTTP client ใดก็ได้
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”ฝั่งเซิร์ฟเวอร์ใช้ดิสทริบิวชัน Connect มาตรฐาน:
composer require nextpdf/serverPHP client ในตัวอย่างระดับการใช้งานจริงด้านล่างใช้ Hypertext Transfer Protocol (HTTP) client และ factory สำหรับสร้างข้อความที่เป็นไปตาม PSR-18 และ PSR-17 ติดตั้งการนำไปใช้งานที่โครงการของคุณใช้เป็นมาตรฐานอยู่แล้ว ตัวอย่างเช่น:
composer require psr/http-client psr/http-factoryภาพรวมเชิงแนวคิด
หัวข้อที่มีชื่อว่า “ภาพรวมเชิงแนวคิด”พื้นผิวงานอะซิงโครนัสแยก การส่ง ออกจาก การเรียกผลลัพธ์ แทนที่จะคงการเชื่อมต่อ HTTP ระยะยาวไว้หนึ่งการเชื่อมต่อต่อเอกสารหนึ่งฉบับ ไคลเอนต์จะส่งงาน รับตัวระบุ แล้วสำรวจจุดปลายสถานะที่มีต้นทุนต่ำจนกว่างานจะเสร็จสิ้น รูปแบบนี้ทำให้การประมวลผลแบบกลุ่มจัดการได้ง่ายขึ้น: ไคลเอนต์ติดตามงานอิสระ N งานพร้อมกันได้โดยไม่มีการเชื่อมต่อที่ถูกบล็อก N การเชื่อมต่อ
ขั้นตอนนี้ขับเคลื่อนด้วยจุดปลายสามจุด:
POST /api/v1/jobsรับเนื้อหาคำขอเรนเดอร์แบบเดียวกับจุดปลายซิงโครนัส/api/v1/renderได้แก่page_sizeorientationและอาร์เรย์operationsที่เรียงลำดับ จุดปลายนี้คืน201 Createdสำหรับงานใหม่ หรือ200 OKเมื่อIdempotency-Keyตรงกับงานที่เคยส่งไปแล้วGET /api/v1/jobs/{id}คืนเรกคอร์ดงานปัจจุบัน สำหรับงานที่ยังไม่เข้าสู่สถานะปลายทาง จุดปลายนี้ยังตั้งค่าส่วนหัวRetry-After(เซิร์ฟเวอร์ใช้ช่วงเวลา 2 วินาที) และฟิลด์poll_urlให้ใช้ค่าจากส่วนหัวแทนการสำรวจในลูปแบบกระชั้นGET /api/v1/jobs/{id}/resultสตรีม PDF ที่เสร็จสมบูรณ์เป็นapplication/pdfจุดปลายนี้คืน409 Conflictหากงานยังไม่เข้าสู่สถานะcompletedดังนั้นจึงควรเรียกก็ต่อเมื่อการสำรวจสถานะยืนยันสถานะปลายทางแล้วเท่านั้น
การตอบกลับที่สำเร็จทุกครั้งใช้ envelope รูปแบบเดียวกัน: ออบเจ็กต์ data ที่มีฟิลด์ของงาน และออบเจ็กต์ meta ที่มี request_id timestamp duration_ms และ api_version ฟิลด์ของงานที่ต้องอ่านจะอยู่ภายใต้ data ได้แก่ data.status data.progress data.job_id และในงานที่เสร็จสมบูรณ์จะมี data.result_url ด้วย
ข้อควรระวังสำหรับรุ่นปัจจุบันมีอยู่หนึ่งข้อ: เซิร์ฟเวอร์ประมวลผลงานที่ส่งแบบ inline ก่อนที่จะตอบ POST ในทางปฏิบัติ การตอบกลับการส่งอาจมีสถานะปลายทาง status อยู่แล้ว และผลลัพธ์อาจพร้อมตั้งแต่การสำรวจครั้งแรก สัญญาการสำรวจและความคืบหน้าที่บันทึกไว้ที่นี่เป็นรูปแบบ Application Programming Interface (API) ที่เสถียร เซิร์ฟเวอร์คงรูปแบบนี้ไว้ไม่เปลี่ยนแปลงระหว่างที่แบ็กเอนด์ประมวลผลย้ายไปใช้ worker pool แบบคิว ดังนั้นไคลเอนต์ที่ใช้การสำรวจจึงถูกต้องในวันนี้และจะยังถูกต้องหลังการเปลี่ยนแปลงนั้น เขียนลูปการสำรวจ อย่าสันนิษฐานว่าการตอบกลับครั้งแรกยังไม่ถึงสถานะปลายทาง และก็ไม่ควรสันนิษฐานว่าถึงสถานะปลายทางแล้ว
พื้นผิว API
หัวข้อที่มีชื่อว่า “พื้นผิว API”เอกสาร OpenAPI ของเซิร์ฟเวอร์และการกำหนดเส้นทาง JobHandler กำหนดพื้นผิว REST แบบงานอะซิงโครนัสของ Connect:
POST /api/v1/jobs: ส่งงานเรนเดอร์ ส่วนหัวคำขอIdempotency-Keyเป็นตัวเลือก เนื้อหาคือคำขอเรนเดอร์ (operationsจำเป็นต้องมีและต้องมีอย่างน้อยหนึ่งการดำเนินการ) การตอบกลับ:201งานใหม่200การเล่นซ้ำแบบ idempotent422เนื้อหาไม่ถูกต้อง409ความขัดแย้งด้าน idempotency429ถูกจำกัดอัตราคำขอGET /api/v1/jobs/{id}: สำรวจสถานะ การตอบกลับ200พร้อมเรกคอร์ดงาน มีส่วนหัวRetry-Afterขณะที่ยังไม่ถึงปลายทาง404หากงานไม่มีอยู่หรือเป็นของไคลเอนต์รายอื่นGET /api/v1/jobs/{id}/result: ดาวน์โหลด PDF200application/pdfเมื่อcompleted409เมื่อยังไม่เสร็จสมบูรณ์404หากไม่รู้จักงานนั้นDELETE /api/v1/jobs/{id}: ยกเลิกงานที่อยู่ในสถานะpendingหรือrunningหรือลบงานที่อยู่ในสถานะcompleted(204)
เรกคอร์ดงานภายใต้ data มีฟิลด์ต่อไปนี้ตามที่เซิร์ฟเวอร์ serialize ออกมา
job_id: ตัวระบุงาน (คำนำหน้าjob_และอักขระเลขฐานสิบหก 24 ตัว)status: หนึ่งในpendingrunningcompletedfailedcancelledสองสถานะแรกยังไม่ถึงปลายทาง สามสถานะสุดท้ายเป็นสถานะปลายทางcreated_atและเมื่อกำหนดค่าแล้วจะมีstarted_atและcompleted_at: timestamp แบบ ISO-8601progress: จำนวนเต็ม 0 ถึง 100 มีอยู่เฉพาะเมื่อเซิร์ฟเวอร์ติดตามค่านี้สำหรับงานนั้น มิฉะนั้นจะไม่มี (ให้ถือว่าไม่ทราบ)error: สตริงข้อความ มีอยู่เฉพาะในงานที่อยู่ในสถานะfailedเท่านั้นresult_url: มีอยู่เฉพาะในงานที่อยู่ในสถานะcompletedเป็นเส้นทางสำหรับดาวน์โหลดผลลัพธ์poll_url: มีอยู่เฉพาะขณะที่งานยังไม่ถึงปลายทาง
การยืนยันตัวตนใช้ bearer token ในส่วนหัว Authorization: Authorization: Bearer npk_live_{kid}_{secret} ในรูปแบบนี้
ตัวอย่างโค้ด — เริ่มต้นอย่างรวดเร็ว
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — เริ่มต้นอย่างรวดเร็ว”ตัวอย่างนี้รันงานหนึ่งตั้งแต่ต้นจนจบที่ระดับ wire เพื่อให้เห็นการเรียกทั้งสามและฟิลด์ที่การเรียกเหล่านั้นคืนมา โดยส่งงาน สำรวจหนึ่งครั้ง แล้วดาวน์โหลด ตัวอย่างระดับการใช้งานจริงด้านล่างเพิ่มลูปการประมวลผลแบบกลุ่ม การรอ Retry-After และการจัดการข้อผิดพลาดอย่างครบถ้วน
# 1. Submit an async render job. Capture the job_id from data.job_id.curl -sS -X POST "$NEXTPDF_CONNECT_URL/api/v1/jobs" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN" \ -H 'Content-Type: application/json' \ -H "Idempotency-Key: invoice-2026-04-0001" \ -d '{"page_size":"A4","orientation":"portrait","operations":[{"type":"add_text","text":"Invoice 0001"}]}'
# 2. Poll status. Read data.status and data.progress; honour Retry-After.curl -sS "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_0123456789abcdef01234567" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"
# 3. Once data.status is "completed", download the PDF binary.curl -sS "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_0123456789abcdef01234567/result" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN" \ -o invoice-0001.pdfตัวอย่างโค้ด — ระดับการใช้งานจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — ระดับการใช้งานจริง”ไคลเอนต์ที่ทำงานได้ในตัวเองนี้ส่งคำขอเรนเดอร์เป็นกลุ่ม จำกัดจำนวนงานที่กำลังดำเนินการพร้อมกัน สำรวจแต่ละงานตามจังหวะที่เซิร์ฟเวอร์กำหนดผ่าน Retry-After รายงานค่า progress ที่เซิร์ฟเวอร์คืนมา ดาวน์โหลด PDF ที่เสร็จสมบูรณ์ทุกไฟล์ และบันทึกกรณีล้มเหลว ไคลเอนต์นี้ใช้ HTTP client ที่เป็นไปตาม PSR-18 และ factory ที่เป็นไปตาม PSR-17 ซึ่งเป็นสัญญาการขนส่งที่สูตร Connect กำหนดเป็นมาตรฐาน นอกจากนี้ยังจับ exception ที่เฉพาะเจาะจงที่สุดซึ่งแต่ละการเรียกอาจเกิดขึ้น: Psr\Http\Client\ClientExceptionInterface สำหรับความล้มเหลวในการขนส่ง และ BatchJobException ที่มีชนิดกำหนดไว้สำหรับการตอบกลับของเซิร์ฟเวอร์ที่ทำให้การประมวลผลแบบกลุ่มไม่สามารถดำเนินต่อได้ ไม่มีบล็อก catch ใดที่ว่างเปล่า แต่ละบล็อกจะบันทึกลงล็อกแล้วส่งต่อ exception อีกครั้ง หรือบันทึกผลลัพธ์ที่กำหนดไว้
แทนที่รายการ $documents ในโค้ดด้วยข้อมูลนำเข้าของคุณเอง และส่ง HTTP client กับ factory จริงของโครงการในตำแหน่งที่ constructor คาดหวังอินเทอร์เฟซ PSR
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Psr\Http\Client\ClientExceptionInterface;use Psr\Http\Client\ClientInterface;use Psr\Http\Message\RequestFactoryInterface;use Psr\Http\Message\StreamFactoryInterface;
/** * Raised when a Connect job response prevents the batch from proceeding. * * Distinct from the PSR-18 transport exception: this means the request was * delivered and the server answered, but the answer is one the batch * cannot act on (a non-success status code, or a job that ended in a * terminal failure). */final class BatchJobException extends RuntimeException{}
/** * Drives a batch of async render jobs over the NextPDF Connect REST surface. * * The client submits each render request, polls every job on the cadence * the server requests through Retry-After, and downloads each completed * PDF. It enforces bounded concurrency so a large batch never opens more * in-flight jobs than the host should track at once. */final readonly class ConnectBatchRunner{ /** * @param non-empty-string $baseUrl Connect base URL, no trailing slash * @param non-empty-string $bearerToken Connect API key (npk_live_...) * @param positive-int $maxInFlight Concurrent jobs cap * @param positive-int $maxPolls Per-job poll attempts before giving up */ public function __construct( private ClientInterface $httpClient, private RequestFactoryInterface $requestFactory, private StreamFactoryInterface $streamFactory, private string $baseUrl, private string $bearerToken, private int $maxInFlight = 8, private int $maxPolls = 150, ) {}
/** * Render every document in the batch and write each completed PDF. * * @param array<non-empty-string, array<string, mixed>> $documents * Map of stable document key to render request body. The key * doubles as the Idempotency-Key, so a re-run of the same batch * does not duplicate server-side work. * @param non-empty-string $outputDir Directory for the written PDFs * * @throws BatchJobException When the batch cannot proceed at all * @throws ClientExceptionInterface When the transport cannot send a request * * @return array<non-empty-string, string> Map of document key to a * human-readable outcome line */ public function run(array $documents, string $outputDir): array { $this->assertWritableDir($outputDir);
$outcomes = [];
// Process in bounded windows so the in-flight job count never // exceeds the configured cap, regardless of batch size. foreach (array_chunk($documents, $this->maxInFlight, preserve_keys: true) as $window) { $jobIds = [];
foreach ($window as $key => $body) { $jobIds[$key] = $this->submit($key, $body); }
foreach ($jobIds as $key => $jobId) { $record = $this->pollToTerminal($jobId); $outcomes[$key] = $this->finish($key, $record, $outputDir); } }
return $outcomes; }
/** * Submit one render job and return its identifier. * * @param non-empty-string $idempotencyKey Stable per-document key * @param array<string, mixed> $body Render request body * * @throws BatchJobException * @throws ClientExceptionInterface * * @return non-empty-string The job_id from data.job_id */ private function submit(string $idempotencyKey, array $body): string { $request = $this->requestFactory ->createRequest('POST', $this->baseUrl . '/api/v1/jobs') ->withHeader('Authorization', 'Bearer ' . $this->bearerToken) ->withHeader('Content-Type', 'application/json') ->withHeader('Idempotency-Key', $idempotencyKey) ->withBody($this->streamFactory->createStream($this->encode($body)));
$response = $this->httpClient->sendRequest($request); $status = $response->getStatusCode();
// 201 new job; 200 idempotent replay. Anything else stops the batch. if ($status !== 201 && $status !== 200) { throw new BatchJobException( sprintf('Submit for "%s" returned HTTP %d.', $idempotencyKey, $status), ); }
$data = $this->decodeData($response->getBody()->__toString()); $jobId = $data['job_id'] ?? null;
if (!is_string($jobId) || $jobId === '') { throw new BatchJobException( sprintf('Submit for "%s" returned no job_id.', $idempotencyKey), ); }
return $jobId; }
/** * Poll one job until it reaches a terminal state. * * Honours the Retry-After header on every non-terminal poll. Gives up * after maxPolls attempts and reports the wait as a failure so the * batch records it rather than blocking forever. * * @param non-empty-string $jobId * * @throws BatchJobException * @throws ClientExceptionInterface * * @return array<string, mixed> The terminal job record (data object) */ private function pollToTerminal(string $jobId): array { $url = $this->baseUrl . '/api/v1/jobs/' . rawurlencode($jobId);
for ($attempt = 0; $attempt < $this->maxPolls; $attempt++) { $request = $this->requestFactory ->createRequest('GET', $url) ->withHeader('Authorization', 'Bearer ' . $this->bearerToken);
$response = $this->httpClient->sendRequest($request); $status = $response->getStatusCode();
if ($status !== 200) { throw new BatchJobException( sprintf('Poll for job "%s" returned HTTP %d.', $jobId, $status), ); }
$data = $this->decodeData($response->getBody()->__toString()); $jobStatus = is_string($data['status'] ?? null) ? $data['status'] : 'unknown'; $progress = is_int($data['progress'] ?? null) ? $data['progress'] : null;
$this->logProgress($jobId, $jobStatus, $progress);
// Terminal states: completed, failed, cancelled. if (in_array($jobStatus, ['completed', 'failed', 'cancelled'], strict: true)) { return $data; }
// Non-terminal: wait the interval the server asked for. $this->waitRetryAfter($response->getHeaderLine('Retry-After')); }
throw new BatchJobException( sprintf('Job "%s" did not finish within %d polls.', $jobId, $this->maxPolls), ); }
/** * Act on a terminal job record: download a completed PDF, or report. * * @param non-empty-string $key Document key * @param array<string, mixed> $record Terminal job record (data object) * @param non-empty-string $outputDir Where to write the PDF * * @throws BatchJobException * @throws ClientExceptionInterface * * @return string A human-readable outcome line */ private function finish(string $key, array $record, string $outputDir): string { $jobStatus = is_string($record['status'] ?? null) ? $record['status'] : 'unknown'; $jobId = is_string($record['job_id'] ?? null) ? $record['job_id'] : '';
if ($jobStatus !== 'completed') { // A failed job carries an error message; surface it, do not swallow. $error = is_string($record['error'] ?? null) ? $record['error'] : 'no detail';
return sprintf('%s -> %s (%s)', $key, $jobStatus, $error); }
$path = rtrim($outputDir, '/\\') . DIRECTORY_SEPARATOR . $key . '.pdf'; $this->download($jobId, $path);
return sprintf('%s -> completed, written to %s', $key, $path); }
/** * Download a completed job result and write it to a server-derived path. * * @param non-empty-string $jobId * @param non-empty-string $path Caller-controlled output path * * @throws BatchJobException * @throws ClientExceptionInterface */ private function download(string $jobId, string $path): void { $request = $this->requestFactory ->createRequest('GET', $this->baseUrl . '/api/v1/jobs/' . rawurlencode($jobId) . '/result') ->withHeader('Authorization', 'Bearer ' . $this->bearerToken);
$response = $this->httpClient->sendRequest($request);
if ($response->getStatusCode() !== 200) { throw new BatchJobException( sprintf('Result download for job "%s" returned HTTP %d.', $jobId, $response->getStatusCode()), ); }
$bytes = $response->getBody()->__toString();
if (!str_starts_with($bytes, '%PDF')) { throw new BatchJobException( sprintf('Result for job "%s" is not a PDF.', $jobId), ); }
if (file_put_contents($path, $bytes) === false) { throw new BatchJobException(sprintf('Could not write result to "%s".', $path)); } }
/** * Sleep for the server-requested interval, with a safe floor and ceiling. */ private function waitRetryAfter(string $retryAfter): void { $seconds = ctype_digit($retryAfter) ? (int) $retryAfter : 2; // Clamp to a sane band so a hostile header cannot stall or busy-loop. $seconds = max(1, min(30, $seconds)); sleep($seconds); }
/** * Emit a progress line. Replace with your logger. */ private function logProgress(string $jobId, string $jobStatus, ?int $progress): void { $pct = $progress === null ? 'n/a' : $progress . '%'; fwrite(STDERR, sprintf("[%s] status=%s progress=%s\n", $jobId, $jobStatus, $pct)); }
/** * Decode a response envelope and return its data object. * * @throws BatchJobException When the body is not the expected envelope * * @return array<string, mixed> */ private function decodeData(string $json): array { try { /** @var mixed $decoded */ $decoded = json_decode($json, true, 32, JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new BatchJobException('Response body is not valid JSON.', previous: $e); }
if (!is_array($decoded) || !isset($decoded['data']) || !is_array($decoded['data'])) { throw new BatchJobException('Response is missing the data envelope.'); }
/** @var array<string, mixed> $data */ $data = $decoded['data'];
return $data; }
/** * @param array<string, mixed> $body * * @throws BatchJobException */ private function encode(array $body): string { try { return json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); } catch (JsonException $e) { throw new BatchJobException('Render request body is not encodable.', previous: $e); } }
/** * @param non-empty-string $dir * * @throws BatchJobException */ private function assertWritableDir(string $dir): void { if (!is_dir($dir) || !is_writable($dir)) { throw new BatchJobException(sprintf('Output directory "%s" is not writable.', $dir)); } }}
// ---------------------------------------------------------------------------// Wiring. Provide your project's concrete PSR-18 client and PSR-17 factories.// ---------------------------------------------------------------------------
/** @var ClientInterface $httpClient *//** @var RequestFactoryInterface $requestFactory *//** @var StreamFactoryInterface $streamFactory */
$baseUrl = getenv('NEXTPDF_CONNECT_URL');$token = getenv('NEXTPDF_CONNECT_TOKEN');
if ($baseUrl === false || $baseUrl === '' || $token === false || $token === '') { fwrite(STDERR, "Set NEXTPDF_CONNECT_URL and NEXTPDF_CONNECT_TOKEN.\n"); exit(2);}
/** @var array<non-empty-string, array<string, mixed>> $documents */$documents = [ 'invoice-0001' => [ 'page_size' => 'A4', 'orientation' => 'portrait', 'operations' => [ ['type' => 'add_text', 'text' => 'Invoice 0001'], ], ], 'invoice-0002' => [ 'page_size' => 'A4', 'orientation' => 'portrait', 'operations' => [ ['type' => 'add_text', 'text' => 'Invoice 0002'], ], ],];
$runner = new ConnectBatchRunner( httpClient: $httpClient, requestFactory: $requestFactory, streamFactory: $streamFactory, baseUrl: rtrim($baseUrl, '/'), bearerToken: $token, maxInFlight: 8,);
try { $outcomes = $runner->run($documents, getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: sys_get_temp_dir());} catch (BatchJobException $e) { fwrite(STDERR, 'Batch stopped: ' . $e->getMessage() . "\n"); exit(1);} catch (ClientExceptionInterface $e) { fwrite(STDERR, 'Transport failure: ' . $e->getMessage() . "\n"); exit(1);}
foreach ($outcomes as $line) { echo $line, "\n";}STDOUT ที่คาดหวังจะมีหนึ่งบรรทัดต่อเอกสารหนึ่งฉบับ เส้นทางขึ้นอยู่กับไดเรกทอรีเอาต์พุตของคุณ:
invoice-0001 -> completed, written to /tmp/invoice-0001.pdfinvoice-0002 -> completed, written to /tmp/invoice-0002.pdfกรณีขอบและข้อควรระวัง
หัวข้อที่มีชื่อว่า “กรณีขอบและข้อควรระวัง”- อ่านฟิลด์ของงานภายใต้
dataไม่ใช่ที่ระดับบนสุด การตอบกลับที่สำเร็จทุกครั้งอยู่ใน envelope{ "data": ..., "meta": ... }data.statusและdata.progressคือฟิลด์ที่ใช้ตัดสินใจmetaมีrequest_idสำหรับเชื่อมโยงกับฝ่ายสนับสนุน progressอาจไม่มีอยู่ เซิร์ฟเวอร์จะรวมprogressก็ต่อเมื่อมีการติดตามค่านี้สำหรับงานนั้น ให้ถือว่าฟิลด์ที่ขาดหายไปเป็น “ไม่ทราบ” ไม่ใช่ศูนย์ และให้ขับเคลื่อนลูปจากstatusซึ่งมีอยู่เสมอ- การส่งอาจเข้าสู่สถานะปลายทางแล้ว ในรุ่นปัจจุบัน เซิร์ฟเวอร์เรนเดอร์แบบ inline ก่อนที่จะตอบ
POSTดังนั้นการตอบกลับการส่งจึงอาจมีstatus: completedและผลลัพธ์อาจพร้อมตั้งแต่การสำรวจครั้งแรก ลูปการสำรวจต้องยอมรับสถานะปลายทางตั้งแต่ความพยายามครั้งที่ศูนย์ แทนที่จะยืนกรานให้มีpendingก่อน - ปฏิบัติตาม
Retry-Afterการตอบกลับสถานะที่ยังไม่ถึงปลายทางจะตั้งค่าRetry-After(ช่วงเวลา 2 วินาที) การสำรวจเร็วกว่านั้นเป็นการสิ้นเปลืองคำขอและเสี่ยงต่อการเกิด429จำกัดค่านี้ให้อยู่ในช่วงที่สมเหตุสมผลแทนที่จะเชื่อถือค่าโดยไม่ตรวจสอบ - การเรียก
/resultก่อนเสร็จสมบูรณ์จะคืน409เรียกจุดปลายผลลัพธ์ก็ต่อเมื่อการสำรวจสถานะแสดงcompletedแล้วเท่านั้น409 Conflictหมายความว่างานยังไม่เสร็จสมบูรณ์ ไม่ใช่ข้อผิดพลาดในการขนส่ง - Idempotency-Key ป้องกันงานซ้ำซ้อน การส่งซ้ำด้วยคีย์เดียวกันจะคืนงานเดิม (
200แทน201) ใช้คีย์ต่อเอกสารที่คงที่เพื่อให้การลองใหม่เมื่อเครือข่ายขัดข้องไม่เริ่มการเรนเดอร์ครั้งที่สอง คีย์ที่นำกลับมาใช้ซ้ำกับเนื้อหาที่ แตกต่าง ถือเป็นความขัดแย้ง409เสมอ - งานมีขอบเขตเป็นของเจ้าของ งานที่ส่งภายใต้คีย์ API หนึ่งจะมองไม่เห็นโดยอีกคีย์หนึ่ง การเรียก
GETข้ามเจ้าของจะคืน404ไม่ใช่403สำรวจด้วยข้อมูลรับรองเดียวกันกับที่คุณใช้ส่ง - งานที่อยู่ในสถานะ
failedมีข้อความerrorอ่านdata.errorในสถานะปลายทางfailedและบันทึกไว้ อย่าลองใหม่โดยไม่ตรวจสอบ
ประสิทธิภาพ
หัวข้อที่มีชื่อว่า “ประสิทธิภาพ”ต้นทุนของการประมวลผลแบบกลุ่มคือผลรวมของการเรนเดอร์บวกกับโอเวอร์เฮดของการสำรวจ มีตัวควบคุมฝั่งไคลเอนต์สองอย่าง ประการแรก จำกัดการทำงานพร้อมกัน: ขีดจำกัด maxInFlight กำหนดจำนวนงานที่ติดตามพร้อมกัน ซึ่งทำให้จำนวนคำขอที่เปิดค้างอยู่และหน่วยความจำของไคลเอนต์คงที่โดยไม่ขึ้นกับขนาดของกลุ่ม ตั้งค่าให้ตรงกับจำนวน worker ของเซิร์ฟเวอร์ และอย่าตั้งให้สูงกว่านั้น งานที่กำลังดำเนินการมากกว่าจำนวน worker มีแต่จะยืดเวลารอในคิวของแต่ละงาน ประการที่สอง ปฏิบัติตามช่วงเวลาการสำรวจ: การสำรวจแต่ละครั้งเป็นการอ่านสถานะที่มีต้นทุนต่ำ แต่ลูปแบบกระชั้นจะเพิ่มปริมาณคำขอและทำให้ตัวจำกัดอัตราทำงาน Retry-After 2 วินาทีของเซิร์ฟเวอร์เป็นค่าเริ่มต้นที่เหมาะสม และตัวรันเนอร์จำกัดให้อยู่ในช่วง 1 ถึง 30 วินาที เพื่อไม่ให้งานช้าเพียงงานเดียวทำให้ลูปหมุนถี่เกินไปหรือทำให้หน้าต่างค้าง
สำหรับการประมวลผลแบบกลุ่มที่มีขนาดใหญ่มาก ให้ประมวลผลเป็นหน้าต่าง (ตัวรันเนอร์ใช้ array_chunk) แทนที่จะส่งทุกอย่างพร้อมกันตั้งแต่ต้น วิธีนี้จำกัดทั้งสถานะที่ไคลเอนต์ติดตามและความลึกของคิวเซิร์ฟเวอร์ ดังนั้นกลุ่มที่มีรูปแบบผิดหรือมีขนาดใหญ่เกินไปจะล้มเหลวภายในหนึ่งหน้าต่างแทนที่จะล้มหลังจากการส่งหลายพันครั้ง
หมายเหตุด้านความปลอดภัย
หัวข้อที่มีชื่อว่า “หมายเหตุด้านความปลอดภัย”- เก็บ bearer token ให้พ้นจากล็อกและ URL คีย์ API ส่งผ่านส่วนหัว
Authorizationเท่านั้น อย่าวางคีย์ไว้ในสตริงคิวรี บรรทัดล็อก หรืออาร์ติแฟกต์ที่เขียนออกมาเป็นอันขาด ตัวรันเนอร์บันทึกjob_idและstatusแต่ไม่เคยบันทึกข้อมูลรับรอง - สร้างเส้นทางเอาต์พุตจากคีย์ที่ฝั่งไคลเอนต์ควบคุม ตัวรันเนอร์สร้างเส้นทางเอาต์พุตแต่ละเส้นจากคีย์เอกสารที่โค้ดของคุณเลือก โดยต่อเข้ากับไดเรกทอรีเอาต์พุตที่ตายตัว และไม่เคยมาจากค่าในการตอบกลับของเซิร์ฟเวอร์ อย่าแทรกฟิลด์ของงานเข้าไปในเส้นทางระบบไฟล์ เพราะจะเปิดช่องให้เกิด path traversal
- ตรวจสอบไบต์ที่ดาวน์โหลดมา ตัวรันเนอร์ตรวจสอบ
200จาก/resultและตรวจหาส่วนหัว%PDFก่อนที่จะเขียนไฟล์ สถานะดาวน์โหลดที่สำเร็จเพียงอย่างเดียวไม่ได้พิสูจน์ว่าเนื้อหาเป็น PDF - ถือว่าผลลัพธ์ไม่น่าเชื่อถือจนกว่าจะตรวจสอบ งานที่เสร็จสมบูรณ์หมายความว่าเซิร์ฟเวอร์เรนเดอร์ไบต์ออกมา ไม่ได้หมายความว่าไบต์เหล่านั้นปลอดภัยที่จะส่งต่อ นำผลลัพธ์ผ่านขั้นตอนการตรวจสอบเชิงโครงสร้างก่อนส่งมอบให้ไคลเอนต์หรือระบบปลายทาง
- ใช้คีย์ที่มีสิทธิ์น้อยที่สุด พื้นผิวงานอะซิงโครนัสเป็นพื้นผิวสำหรับการเรนเดอร์ระดับ core ออกคีย์สำหรับกลุ่มโดยกำหนดขอบเขตให้ตรงกับการดำเนินการที่จำเป็นพอดี และหมุนเวียนคีย์ตามกำหนดเวลาที่นโยบายการจัดการความลับของคุณกำหนด
- จำกัดงบประมาณการสำรวจ
maxPollsป้องกันไม่ให้งานที่ค้างยึดไคลเอนต์ไว้ตลอดไป การประมวลผลแบบกลุ่มบันทึกการหมดเวลาเป็นผลลัพธ์แทนที่จะบล็อก ซึ่งทำให้งานที่เสียเพียงงานเดียวไม่ทำให้งานที่เหลือถูกปฏิเสธการให้บริการ
ความสอดคล้อง
หัวข้อที่มีชื่อว่า “ความสอดคล้อง”สูตรนี้ไม่อ้างมาตรฐานเชิงบรรทัดฐานใด สูตรนี้ใช้จุดปลาย REST แบบงานอะซิงโครนัสของ NextPDF Connect (POST /api/v1/jobs GET /api/v1/jobs/{id} GET /api/v1/jobs/{id}/result) และอ่านฟิลด์เรกคอร์ดงานที่เซิร์ฟเวอร์กำหนด (status progress error result_url poll_url) การตรวจสอบส่วนหัว %PDF ในผลลัพธ์ที่ดาวน์โหลดมายืนยันเพียงว่าการตอบกลับเริ่มต้นด้วยเครื่องหมาย PDF เท่านั้น ไม่ใช่การตัดสินความถูกต้องหรือความสอดคล้อง สำหรับการตรวจสอบมาตรฐานข้ามชุดเอกสาร ให้ใช้เครื่องมือตรวจสอบความสอดคล้องแบบกลุ่มระดับ Enterprise ดู การตรวจสอบมาตรฐานแบบกลุ่มผ่าน Connect ซึ่งเป็นพื้นผิวคนละส่วนกับงานเรนเดอร์ที่กล่าวถึงในหน้านี้
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- Hello world over Connect: การเรนเดอร์เดี่ยวขั้นต่ำก่อนเริ่มประมวลผลแบบกลุ่ม
- Connect recipe conventions: สัญญาด้านการขนส่ง การยืนยันตัวตน และความสอดคล้องที่สูตร Connect ทุกสูตรใช้ร่วมกัน
- Exception-aware error handling over Connect: เซิร์ฟเวอร์รายงานข้อผิดพลาดอย่างไรและไคลเอนต์ควรตอบสนองอย่างไร
- Batch standards check over Connect: พื้นผิวความสอดคล้องระดับ Enterprise ที่แยกจากงานเรนเดอร์เหล่านี้