Rendering sicuro di PDF in un worker a lunga esecuzione
In sintesi
Sezione intitolata “In sintesi”Un worker PHP a lunga esecuzione (RoadRunner, Swoole, Laravel Octane) mantiene il processo attivo per molte richieste. Eseguire ripetutamente il parsing degli stessi font e ridecodificare le stesse immagini a ogni richiesta spreca CPU e fa crescere la memoria residente. Per evitarlo, NextPDF separa due cicli di vita:
- Durata del processo, risorse condivise:
FontRegistryeImageRegistrycontengono le tabelle dei font analizzate e le cache delle immagini decodificate. Crearli una sola volta all’avvio del worker. - Durata della richiesta, risorsa usa e getta: il
Documentrestituito daDocumentFactory::create(). Costruirlo, scriverlo e lasciarlo uscire dall’ambito. Il garbage collector recupera quindi l’intero grafo degli oggetti.
Questa ricetta presenta la sequenza di avvio del worker, il corpo eseguito per ogni richiesta e il reset periodico che mantiene costante il picco di memoria.
Installazione
Sezione intitolata “Installazione”composer require nextpdf/core:^3Il pattern worker non richiede estensioni aggiuntive, e un runtime worker (RoadRunner / Swoole / Octane) è facoltativo. Lo stesso pattern factory funziona anche in un semplice ciclo CLI for, che è esattamente quello esercitato dall’harness.
Panoramica concettuale
Sezione intitolata “Panoramica concettuale”Il punto di ingresso consigliato per i worker è DocumentFactory. Costruirlo una sola volta con un FontRegistry e un ImageRegistry condivisi:
FontRegistry::warmup()analizza i file di font indicati e memorizza nella cache le tabelle analizzate.FontRegistry::lock()blocca quindi il registro, così che nessun codice eseguito durante una richiesta possa modificare il set di font condiviso.isLocked()indica lo stato corrente. Una volta bloccato, il registro può essere condiviso in sicurezza tra coroutine concorrenti.- Costruire
ImageRegistrycon un budgetmaxCacheBytes. Quando il budget viene superato, il registro sfratta le voci utilizzate meno di recente. Una singola immagine più grande del budget aggira completamente la cache invece di sovraccaricarla. ImageRegistry::reset()sfratta tutte le immagini in cache mantenendo il registro pienamente funzionante. La richiesta successiva lo ripopola su richiesta. Richiamarlo a cadenza regolare (ogni N richieste oppure quandomemoryUsage()supera una soglia) per riportare il picco alla linea di base.
Ogni documento creato dalla factory è un PDF indipendente. La norma ISO 32000-2 §7.5.5 definisce il trailer di un file mai aggiornato come privo di voce Prev, e ogni richiesta del worker emette esattamente un file di prima generazione di questo tipo. Pertanto le richieste non condividono lo stato del documento, anche se condividono le cache dei font e delle immagini. Il tag BaseFont del font sottoinsieme (ISO 32000-2 §9.6.4) resta stabile tra le richieste perché il font analizzato risiede nel registro condiviso.
Superficie API
Sezione intitolata “Superficie API”La superficie dell’API di questa ricetta è generata dal PHPDoc su NextPDF\Core\DocumentFactory, NextPDF\Typography\FontRegistry, NextPDF\Graphics\ImageRegistry e NextPDF\Support\MemoryReport. I membri principali usati di seguito sono DocumentFactory::create(), FontRegistry::warmup() / lock() / isLocked() / memoryUsage(), ImageRegistry::reset() / memoryUsage() e MemoryReport::$currentBytes / $peakBytes / $entryCount / utilizationPercent().
Esempio di codice — Avvio rapido
Sezione intitolata “Esempio di codice — Avvio rapido”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// --- Worker boot (run ONCE, before the request loop) ---------------------$fonts = new FontRegistry();$fonts->lock(); // freeze the shared font set$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$factory = new DocumentFactory($fonts, $images);
// --- Per request ---------------------------------------------------------$doc = $factory->create();$doc->setTitle('Worker output');$doc->addPage();$doc->setFont('helvetica', 'B', 16);$doc->cell(0, 12, 'Generated in a shared-registry worker', newLine: true);$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');// $doc leaves scope here → GC reclaims the whole document tree.Esempio di codice — Produzione
Sezione intitolata “Esempio di codice — Produzione”L’esempio completo rispetta il canale di output dell’harness. Mostra la sequenza di avvio, un ciclo limitato di richieste, il reset() periodico e un’asserzione sul picco di memoria. È lo script che l’harness di riproducibilità esegue due volte.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// --- Worker boot: shared, process-lifetime registries --------------------$fonts = new FontRegistry();$fonts->lock(); // share-safe once locked$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$factory = new DocumentFactory($fonts, $images);
$resetEvery = 4; // reset cadence in requests$peakAfterReset = 0;
// --- Simulated request loop ---------------------------------------------for ($request = 1; $request <= 12; $request++) { $doc = $factory->create(); $doc->setTitle("Worker Request #{$request}"); $doc->addPage(); $doc->setFont('helvetica', 'B', 16); $doc->cell(0, 12, "Worker Request #{$request}", newLine: true); $doc->setFont('helvetica', '', 11); $doc->cell(0, 8, 'Shared FontRegistry / ImageRegistry across requests.', newLine: true);
// The harness captures the LAST request's PDF via the side channel. if ($request === 12) { $doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf'); } else { $doc->getPdfData(); // force render, then drop }
unset($doc); // explicit end-of-request
// Bound the cache high-water mark on a fixed cadence. if ($request % $resetEvery === 0) { $images->reset(); \gc_collect_cycles(); $report = $images->memoryUsage(); $peakAfterReset = \max($peakAfterReset, $report->currentBytes); }}
$final = $images->memoryUsage();
fwrite(STDERR, \sprintf( "fonts.locked=%s images.entries=%d images.current=%dB peak_after_reset=%dB\n", $fonts->isLocked() ? 'yes' : 'no', $final->entryCount, $final->currentBytes, $peakAfterReset,));STDOUT resta libero per l’harness; i messaggi di avanzamento vanno su STDERR. Il PDF viene scritto solo su NEXTPDF_COOKBOOK_OUTPUT, mai inviato allo standard output.
Casi limite e insidie
Sezione intitolata “Casi limite e insidie”- Bloccare prima di condividere. Richiamare
FontRegistry::lock()all’avvio. Se due coroutine accedono a un registro ancora modificabile, si crea una race condition. UsareisLocked()come asserzione in un health check. reset()non èunset().ImageRegistry::reset()sfratta i dati binari in cache ma mantiene il registro utilizzabile, quindi è la chiamata periodica corretta. Distruggere e ricostruire il registro a ogni richiesta vanifica l’intero scopo della cache condivisa.- Bypass delle immagini sovradimensionate. Un’immagine più grande di
maxCacheBytesviene decodificata a ogni utilizzo e mai memorizzata nella cache, quindi non può causare lo sfratto del working set. Questo è intenzionale. Dimensionare il budget per le immagini comuni, non per quella rara e molto grande. - Il documento deve uscire dall’ambito. Lasciare il
Documentin una variabile statica, in un binding di container a lunga durata o in una closure catturata dal worker mantiene in vita l’intero grafo degli oggetti e vanifica la raccolta per richiesta. Una chiamata aunset()o un’uscita dall’ambito è obbligatoria. - Posizionamento di
gc_collect_cycles(). Il garbage collector ciclico di PHP non conosce i confini delle richieste. Richiamarlo dopo la cadenza di reset, non a ogni richiesta. In questo modo il valore massimo rimane limitato senza pagare il costo della raccolta sul percorso critico. - Avvertenza sul determinismo. I timestamp del documento e il
/IDdel trailer vengono rigenerati a ogni salvataggio (ISO 32000-2 §14.3). Il PDF prodotto viene pertanto confrontato con il profilo semantico (AST strutturale più metadati, mai byte volatili). Vedere «Conformità».
Prestazioni
Sezione intitolata “Prestazioni”- Il registro condiviso trasforma il parsing ripetuto dei font e la decodifica ripetuta delle immagini in un costo di avvio una tantum. Il lavoro per richiesta si riduce quindi all’impaginazione e alla serializzazione.
- Il picco di memoria residente è limitato da
maxCacheBytespiù il working set di un documento in elaborazione. Ilreset()periodico riporta la cache alla linea di base, quindi un worker a lunga durata non mostra un andamento a dente di sega crescente. - Il front-matter
performance_budget(wall_ms: 4000,peak_mb: 192) impone un limite all’esecuzione dell’harness del ciclo di 12 richieste. L’harness lo applica; non è una garanzia per documenti arbitrari. - Questa ricetta copre la voce «memory/GC» dell’elenco delle lacune §4.3 per #31. L’
examples/14-worker-factory.phpdi supporto esiste, e il nuovotests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.phpaggiunge l’asserzione memory/GC mancante (il picco non cresce tra i cicli dopo il reset).
Note sulla sicurezza
Sezione intitolata “Note sulla sicurezza”- Il pattern worker elabora un documento per richiesta e condivide solo le cache dei font analizzati e delle immagini decodificate. Nessun contenuto del documento supera il confine della richiesta. Una richiesta non può leggere i dati del documento di un’altra richiesta tramite i registri condivisi.
- Gli input non attendibili continuano a passare attraverso i normali confini di input di NextPDF, e il pattern worker non allenta alcuna convalida. Trattare l’input HTML/asset di ogni richiesta come non attendibile, esattamente come si farebbe in un processo per richiesta.
Conformità
Sezione intitolata “Conformità”| Dichiarazione | Specifica | Clausola | reference_id |
|---|---|---|---|
| La data di modifica del documento viene rigenerata a ogni salvataggio, quindi l’output per richiesta non è stabile a livello di byte. | ISO 32000-2 | §14.3 | |
Ogni documento del worker è un file mai aggiornato (nessun Prev nel trailer); le richieste non condividono lo stato del documento. | ISO 32000-2 | §7.5.5 | |
| Il prefisso del tag del font sottoinsieme è stabile tra le richieste perché il font analizzato risiede nel registro condiviso. | ISO 32000-2 | §9.6.4 |
Poiché il /ID del trailer e la data di modifica vengono rigenerati a ogni salvataggio, questa ricetta è verificata con il profilo di riproducibilità semantico (uguaglianza dell’AST strutturale più un confronto basato solo sui metadati). Un’affermazione bit per bit o strutturale sarebbe scorretta per l’output del worker.