Przejdź do głównej zawartości
getnextpdf.com

Enterprise edycja

Stream: przetwarzanie zadań dokumentowych

NextPDF\Enterprise\Stream\DocumentJobStreamProcessor zamienia strumień manifestów renderowania w trwałe, rozliczalne wyniki. Konsumuje iterable<RenderManifest> jako generator, renderuje ograniczone okna przez silnik renderujący Pro i finalizuje każde zadanie w kolejności źródłowej. Każde zadanie kończy się dokładnie jednym stanem końcowym: wynik zatwierdzony, rozpoznany jako już zatwierdzony lub skierowany do dead-letter. Postęp jest zapisywany w checkpointach, więc przerwany awaryjnie przebieg wznawia się bez ponownej publikacji czegokolwiek.

Historia Stream dzieli się na dwie edycje, a podział jest celowy. Pro dostarcza trwały, współbieżny silnik renderujący oraz lokalne, jednohostowe magazyny na systemie plików — połowę wewnątrzprocesową. Enterprise dostarcza ten procesor strumienia zadań dokumentowych plus elementy przekraczające granice hostów: committer magazynu obiektów (ObjectStorageCommitter) oraz trwały outbox zdarzeń końcowych (FilesystemOutboxEmitter). Strona Pro Stream wyraża tę samą granicę ze swojej strony.

Ta funkcja jest dostarczana w NextPDF Enterprise (nextpdf/enterprise) i aktywuje się z kopertą licencyjną poziomu Enterprise. Wdrożenie bez tego uprawnienia nie ładuje klas tej funkcji. Porównaj edycje i zdobądź licencję.

Okno terminala
composer require nextpdf/enterprise

Klasy na tej stronie znajdują się w NextPDF\Enterprise\Stream oraz NextPDF\Enterprise\Stream\Storage. Konsumują zamrożone kontrakty Pro w NextPDF\Pro\Stream — interfejsy silnika, committera, checkpointu, idempotencji, ponawiania i dead-letter.

Zadaniem procesora jest semantyka dostawy, nie renderowanie. Grupuje strumień manifestów w okna offsetów źródłowych nie większe niż rozmiar batcha silnika. Każde okno renderuje się przez RenderEngineInterface::renderBatch(), z ograniczonym, deterministycznym ponawianiem przekroczeń czasu poszczególnych elementów. Następnie każdy element jest finalizowany w kolejności offsetów źródłowych do stanu końcowego.

Granica dokładnie raz obowiązuje na element i jest zakotwiczona w committerze, nie w koordynacji. Trwały postęp to najwyższy znacznik offsetu liczonego od 1: każdy offset na poziomie checkpointu lub poniżej osiągnął stan końcowy. Kolejność bariery jest stała: zatwierdź bajty, przesuń znacznik, zapisz checkpoint, opróżnij zbuforowane znaczniki idempotencji, następnie emituj zdarzenia końcowe. Awaria między zatwierdzeniem a checkpointem powoduje idempotentne ponowne zatwierdzenie przy wznowieniu, ponieważ committer porównuje sygnatury. Awaria po checkpointcie przewija do przodu poza offset, więc nic nie publikuje się dwa razy.

ObjectStorageCommitter implementuje OutputCommitterInterface z Pro wobec magazynu obiektów przez minimalny ObjectStorageClientInterface. container celu to bucket, a jego key to klucz obiektu. Ponowne zatwierdzenie identycznych bajtów to operacja pusta z porównaniem sygnatury. Rozbieżne bajty bez overwrite podnoszą konflikt SPEC-COMMIT-409. Świeży obiekt jest tworzony wyłącznie atomowym zapisem warunkowym putIfAbsent(); przegranie tego wyścigu wyzwala ograniczoną pętlę ponownego odczytu i rozstrzygnięcia. Dokładnie raz między zapisującymi obowiązuje zatem dokładnie w takim stopniu, w jakim putIfAbsent() Twojego adaptera jest prawdziwym zapisem warunkowym — If-None-Match: * na S3, ifGenerationMatch: 0 na GCS. Ten cykl dostarcza interfejs plus wewnątrzpamięciowy NullObjectStorageClient; działający adapter S3/GCS jest dostarczany przez hosta.

Zdarzenia końcowe domykają pętlę dla systemów pobocznych. Po barierze checkpointu procesor próbuje wyemitować JobTerminalEvent dla każdego sfinalizowanego zadania — identyfikatory, status, potwierdzenie, szczegóły błędu, liczbę prób i nigdy żadnych bajtów PDF. Przy zwykłym emiterze zwrotnym emisja jest co najwyżej raz: zdarzenia po checkpointcie mogą zostać pominięte przy wznowieniu po awarii. FilesystemOutboxEmitter czyni każde zdarzenie trwałym z chwilą uruchomienia emit(): każde zdarzenie to jeden atomowy plik JSON nazwany hashem swojego deterministycznego eventId, więc ponowna emisja po wznowieniu jest idempotentna, przekaźnik dostarcza co najmniej raz, a konsumenci deduplikują po eventId. Jedna granica pozostaje w każdym przypadku: emisja następuje po barierze checkpointu, więc awaria między checkpoint.save() a emit() pomija zdarzenie końcowe tego elementu przy wznowieniu. Systemy poboczne wymagające kompletnego rejestru zdarzeń powinny uzgadniać wobec zatwierdzonych obiektów (magazyn jest źródłem prawdy), nie wobec samego outbox.

Licencjonowanie jest wpięte w ścieżkę wyjściową. Fabryka withBrandingFromLicense() rozwiązuje strategię brandingu ewaluacyjnego raz na przebieg z licencji. Płatna licencja rozwiązuje się do transformacji tożsamościowej. Ewaluacyjna lub brakująca licencja znakuje wodnym znakiem każdy zatwierdzony dokument, a dokument, którego nie da się zabrandować, trafia do dead-letter — procesor nigdy nie zatwierdza niezabrandowanych bajtów ewaluacyjnych.

Nośną decyzją jest to, że dokładnie raz opiera się na porównywanym sygnaturą, warunkowo tworzonym obiekcie committera — nie na rozproszonych blokadach czy konsensusie. Zapis warunkowy magazynu obiektów jest jedynym prymitywem atomowym, jakiego wymaga projekt, a wszystko inne może zawieść i się odtworzyć. Dlatego silnik renderujący musi pozostać wolny od efektów ubocznych, dlaczego kluczowany stan i wewnątrzprzebiegowe pamięci podręczne deduplikacji traktowane są jako przeliczalne przyspieszenia i dlaczego niejednoznaczne zatwierdzenie przerywa przebieg zamiast zgadywać: ścieżka wznowienia zbiega się przez to samo porównanie sygnatur. To także powód, dla którego pojedynczy zapisujący na runId jest deklarowanym wymaganiem, a nie egzekwowaną dzierżawą — magazyn checkpointu celowo pozostaje prosty, a warstwa zatwierdzania pozostaje siatką bezpieczeństwa.

Tło projektowe: Generowanie dokumentów o dużej objętości.

Hosty powinny konstruować przez fabrykę, aby kontrola licencja-branding nigdy nie pozostała niepodłączona:

public static function withBrandingFromLicense(
RenderEngineInterface $engine,
OutputCommitterInterface $committer,
IdempotencyStoreInterface $idempotency,
CheckpointStoreInterface $checkpoints,
KeyedStateStoreInterface $state,
DeadLetterStoreInterface $deadLetters,
RetryPolicy $retryPolicy,
ClockInterface $clock,
EntitlementEvaluator $entitlementEvaluator,
?LicenseKey $license,
?StreamProcessorProbe $probe = null,
?JobCompletionEmitterInterface $emitter = null,
?BrandingApplicator $brandingApplicator = null,
): self

$clock to Symfony\Component\Clock\ClockInterface (backoff ponawiania śpi przez niego). Licencja null rozwiązuje się fail-closed do brandingu ewaluacyjnego.

Jedyny punkt wejścia przetwarza jeden przebieg i zwraca jego liczniki:

public function process(iterable $manifests, StreamProcessorConfig $config): ProcessingSummary

Rzuca lub zawodzi z: NextPDF\Enterprise\Stream\Exception\StreamProcessorException, gdy nie są spełnione warunki wstępne bezpieczeństwa po awarii (przebieg crashSafe z nietrwałymi współpracownikami) lub gdy zatwierdzenie jest niejednoznaczne; InvalidArgumentException, gdy windowSize przekracza maxBatchSize() silnika.

public function __construct(
public string $runId,
int $windowSize = 32,
int $checkpointIntervalJobs = 100,
public bool $crashSafe = true,
public bool $emitSkippedCompletions = false,
)

Rzuca lub zawodzi z: InvalidArgumentException, gdy windowSize lub checkpointIntervalJobs jest poniżej 1. $runId to stabilny identyfikator przebiegu z pojedynczym zapisującym, który jest kluczem wznowienia z checkpointu.

public function __construct(
private ObjectStorageClientInterface $client,
private string $scheme,
private ClockInterface $clock,
) {}

$scheme nazywa schemat celu, który obsługuje ten committer (na przykład s3 lub gcs); $clock tutaj to Psr\Clock\ClockInterface.

public function commit(
string $jobId,
OutputObjectKey $target,
string $bytes,
string $sha256,
bool $overwrite = false,
): CommitReceipt

Rzuca lub zawodzi z: UnsupportedTargetException przy niezgodności schematu; RenderManifestException, gdy klucz celu nie jest bezpieczny względem kontenera; CommitIntegrityException, gdy zadeklarowany sha-256 nie pasuje do bajtów; OutputCommitConflictException (SPEC-COMMIT-409) przy rozbieżnych bajtach bez overwrite; RuntimeException, gdy wyścig tworzenia nie może się zbiec po 5 próbach przy współbieżnej mutacji.

Minimalna powierzchnia adaptera, którą implementuje działająca integracja S3/GCS:

public function shaOf(string $bucket, string $key): ?string;
public function put(string $bucket, string $key, string $bytes, string $sha256): void;
public function putIfAbsent(string $bucket, string $key, string $bytes, string $sha256): bool;

putIfAbsent() musi być prawdziwym atomowym tworzeniem warunkowym (If-None-Match: * na S3, ifGenerationMatch: 0 na GCS) i zwraca true tylko wtedy, gdy to wywołanie zapisało obiekt. put() to bezwarunkowe nadpisanie używane wyłącznie, gdy manifest zażądał overwrite.

JobCompletionEmitterInterface i FilesystemOutboxEmitter

Dział zatytułowany „JobCompletionEmitterInterface i FilesystemOutboxEmitter”
public function emit(JobTerminalEvent $event): void;

Emiter jest opcjonalny w procesorze. Zdarzenia wystrzeliwują dopiero po trwałym sfinalizowaniu elementu. FilesystemOutboxEmitter to dostarczana trwała implementacja:

public function __construct(string $directory, ?AtomicFileWriter $writer = null)

Rzuca lub zawodzi z: InvalidArgumentException, gdy katalog nie istnieje; emit() rzuca RuntimeException, jeśli zdarzenia nie da się zakodować do JSON. hasEvent(string $eventId): bool sprawdza outbox; count(): int raportuje niedostarczone zdarzenia.

public function __construct(
public string $eventId,
public string $runId,
public int $sourceOffset,
public string $jobId,
public string $idempotencyKeyValue,
public JobTerminalStatus $status,
public ?CommitReceipt $receipt,
public ?string $errorCode,
public ?string $errorMessage,
public int $attempts,
public DateTimeImmutable $occurredAt,
) {}

eventId jest deterministyczny — runId:sourceOffset:idempotencyKey:status — co właśnie umożliwia deduplikację w outbox. toArray() serializuje zdarzenie do transportu; nie niesie żadnych bajtów PDF. JobTerminalStatus to enum łańcuchowy: Committed (committed), DeadLettered (dead_lettered), Skipped (skipped).

Niezmienne liczniki zwracane przez process(): runId, sourceRead, fastForwardedByCheckpoint, skippedByIdempotency, windows, renderBatchCalls, renderRetries, commitReceipts, deadLettered, checkpointSaves oraz finalCommittedOffset (końcowy najwyższy znacznik stanu końcowego).

Zatwierdzanie obiektu do magazynu dokładnie raz w izolacji. Wewnątrzpamięciowy NullObjectStorageClient zastępuje Twój adapter S3/GCS; semantyka, którą obserwujesz, jest tą, którą działający adapter musi zachować.

stream-object-commit-quickstart.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Stream\Storage\NullObjectStorageClient;
use NextPDF\Enterprise\Stream\Storage\ObjectStorageCommitter;
use NextPDF\Manifest\OutputObjectKey;
use NextPDF\Pro\Stream\Exception\OutputCommitConflictException;
use Symfony\Component\Clock\NativeClock;
$committer = new ObjectStorageCommitter(
client: new NullObjectStorageClient(), // swap in your S3/GCS adapter
scheme: 's3',
clock: new NativeClock(),
);
$target = new OutputObjectKey(scheme: 's3', container: 'invoices', key: '2026/07/inv-1001.pdf');
$bytes = '%PDF-1.7 example-rendered-bytes';
$sha = hash('sha256', $bytes);
$first = $committer->commit('inv-1001', $target, $bytes, $sha);
$replay = $committer->commit('inv-1001', $target, $bytes, $sha); // crash-resume replay
printf("first : reuse=%s, %d bytes\n", var_export($first->idempotentReuse, true), $first->bytesWritten);
printf("replay: reuse=%s\n", var_export($replay->idempotentReuse, true));
try {
$divergent = '%PDF-1.7 different-bytes';
$committer->commit('inv-1001', $target, $divergent, hash('sha256', $divergent));
} catch (OutputCommitConflictException $conflict) {
echo 'conflict: ' . $conflict->specCode() . "\n"; // no silent clobber
}

Oczekiwane wyjście:

first : reuse=false, 31 bytes
replay: reuse=true
conflict: SPEC-COMMIT-409

Pełny przebieg bezpieczny po awarii: trwałe magazyny Pro, committer magazynu obiektów, trwały outbox oraz branding rozwiązany z licencji. Ponowne uruchomienie tego samego runId po awarii przewija do przodu i zbiega się.

stream-run-production.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Licensing\EntitlementEvaluator;
use NextPDF\Enterprise\Stream\DocumentJobStreamProcessor;
use NextPDF\Enterprise\Stream\Exception\StreamProcessorException;
use NextPDF\Enterprise\Stream\FilesystemOutboxEmitter;
use NextPDF\Enterprise\Stream\Storage\ObjectStorageCommitter;
use NextPDF\Enterprise\Stream\StreamProcessorConfig;
use NextPDF\Manifest\Render\SingleDocumentRenderer;
use NextPDF\Manifest\RenderManifest;
use NextPDF\Pro\Stream\Checkpoint\FilesystemCheckpointStore;
use NextPDF\Pro\Stream\Dedup\FilesystemIdempotencyStore;
use NextPDF\Pro\Stream\Engine\InProcessRenderEngine;
use NextPDF\Pro\Stream\Retry\FilesystemDeadLetterStore;
use NextPDF\Pro\Stream\Retry\RetryPolicy;
use NextPDF\Pro\Stream\State\InMemoryKeyedStateStore;
use Symfony\Component\Clock\NativeClock;
// Production requires a host-supplied adapter whose putIfAbsent() is a TRUE
// atomic conditional create (S3 If-None-Match: *, GCS ifGenerationMatch: 0)
// and whose shaOf() reads durable object state. NullObjectStorageClient is
// for the quick start only - it keeps nothing across processes.
$s3Client = new \Aws\S3\S3Client(['region' => 'eu-central-1', 'version' => 'latest']);
$objectClient = new \Acme\Storage\S3ObjectStorageClient($s3Client); // implements ObjectStorageClientInterface
$stateDir = '/var/lib/nextpdf/stream';
foreach (['checkpoints', 'idempotency', 'dead-letters', 'outbox'] as $sub) {
if (!is_dir($stateDir . '/' . $sub)) {
mkdir($stateDir . '/' . $sub, 0770, true);
}
}
// One manifest per JSONL line; the generator never materialises the batch.
$manifests = (static function (string $path): Generator {
$handle = fopen($path, 'rb');
if ($handle === false) {
throw new RuntimeException('Cannot open job stream: ' . $path);
}
try {
while (($line = fgets($handle)) !== false) {
if (trim($line) !== '') {
yield RenderManifest::fromJson(trim($line));
}
}
} finally {
fclose($handle);
}
})('/var/spool/nextpdf/jobs.jsonl');
$license = null; // your licensing bootstrap yields a LicenseKey; null = evaluation branding
$processor = DocumentJobStreamProcessor::withBrandingFromLicense(
engine: new InProcessRenderEngine(SingleDocumentRenderer::standalone()),
// For a live bucket, implement ObjectStorageClientInterface over your S3/GCS SDK.
committer: new ObjectStorageCommitter($objectClient, 's3', new NativeClock()),
idempotency: new FilesystemIdempotencyStore($stateDir . '/idempotency'),
checkpoints: new FilesystemCheckpointStore($stateDir . '/checkpoints'),
state: new InMemoryKeyedStateStore(), // recomputable; durability not required here
deadLetters: new FilesystemDeadLetterStore($stateDir . '/dead-letters'),
retryPolicy: new RetryPolicy(maxAttempts: 3, baseDelayMs: 200, maxDelayMs: 5_000),
clock: new NativeClock(),
entitlementEvaluator: new EntitlementEvaluator(),
license: $license,
emitter: new FilesystemOutboxEmitter($stateDir . '/outbox'),
);
$config = new StreamProcessorConfig(
runId: 'nightly-invoices-2026-07-03',
windowSize: 32,
checkpointIntervalJobs: 100,
crashSafe: true,
);
try {
$summary = $processor->process($manifests, $config);
} catch (StreamProcessorException $e) {
// Ambiguous commit or a non-durable collaborator: the finalized prefix is
// checkpointed. Re-run the SAME runId; the committer converges by digest.
fwrite(STDERR, 'Run aborted for safe resume: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
printf(
"run %s: read=%d committed=%d dedup-skipped=%d dead-lettered=%d checkpoints=%d final-offset=%d\n",
$summary->runId,
$summary->sourceRead,
$summary->commitReceipts,
$summary->skippedByIdempotency,
$summary->deadLettered,
$summary->checkpointSaves,
$summary->finalCommittedOffset,
);

Przykładowe wyjście (liczniki zależą od Twojego strumienia zadań):

run nightly-invoices-2026-07-03: read=1200 committed=1187 dedup-skipped=13 dead-lettered=0 checkpoints=12 final-offset=1200
  • Pojedynczy zapisujący na runId to Twoja odpowiedzialność. Magazyn checkpointu nie ma dzierżawy ani compare-and-swap. Dwaj współbieżni zapisujący na jednym runId są poza kontraktem; wymuszaj wyłączność w swoim harmonogramie.
  • crashSafe: true zawodzi szybko przy nietrwałych współpracownikach. Committer oraz magazyny checkpointu, idempotencji i dead-letter muszą wszystkie implementować znacznik DurableCapability, w przeciwnym razie process() rzuca StreamProcessorException wskazując winowajców. Kluczowany magazyn stanu jest celowo wyłączony: utracony kluczowany stan jest przeliczany od checkpointu w przód.
  • windowSize musi zmieścić się w silniku. Okno większe niż maxBatchSize() rzuca InvalidArgumentException, zanim rozpocznie się jakakolwiek praca.
  • Niejednoznaczne zatwierdzenie przerywa; konflikt nie. SPEC-COMMIT-409 to deterministyczny końcowy konflikt: element trafia do dead-letter, a przebieg trwa dalej. Każda inna porażka zatwierdzenia jest niejednoznaczna: sfinalizowany prefiks jest zapisany w checkpointcie, a przebieg rzuca dla bezpiecznego wznowienia.
  • Porażki renderowania nigdy nie przerywają przebiegu. Wynik Failed poszczególnego elementu, wyczerpany budżet ponawiania lub niezabrandowalne bajty ewaluacyjne — wszystkie kierują ten element do dead-letter i kontynuują.
  • Zduplikowane wartości jobId są bezpieczne; zduplikowana praca jest kluczowana po idempotencyKey. Wyniki korelują z elementami po unikatowym offsecie źródłowym, nigdy po jobId. Zduplikowany klucz idempotencji jest rozpoznawany nawet w tym samym interwale bariery, przed ponownym renderowaniem.
  • Trwałość emitera decyduje o semantyce zdarzeń. Zwykły emiter zwrotny jest tylko obserwatorem i co najwyżej raz przy awarii. FilesystemOutboxEmitter czyni outbox trwałym i deduplikowanym po kluczu; dostawa przekaźnika jest wtedy co najmniej raz, a dokładnie raz po stronie pobocznej wymaga deduplikacji konsumenta po eventId. Jego katalog (jak każdego magazynu na systemie plików) musi istnieć wcześniej, w przeciwnym razie konstruktor rzuca InvalidArgumentException.
  • Zdarzenia pominięte są domyślnie wyłączone. Ustaw emitSkippedCompletions: true, aby emitować także końcowe zdarzenie Skipped dla elementów odciętych przez deduplikację.
  • Klucze wyjściowe zawodzą bezpiecznie. commit() ponownie potwierdza, że klucz celu jest bezpieczny względem kontenera: brak przejścia .., brak absolutnej ucieczki, brak bajtu null, brak osadzonego schematu stream-wrapper i brak dwukropka (który zamyka wektor alternatywnego strumienia danych NTFS). Niebezpieczne klucze rzucają przed jakimkolwiek wywołaniem magazynu.
  • Integralność jest ponownie weryfikowana na granicy. Committer przelicza sha-256 na rzeczywistych bajtach i odrzuca niezgodność z CommitIntegrityException, więc uszkodzone przekazanie nie może wylądować po cichu.
  • Zdarzenia nie niosą treści dokumentu. JobTerminalEvent i wiersze outbox zawierają tylko identyfikatory, sygnatury, znaczniki czasu i łańcuchy błędów. Komunikaty błędów mogą powtarzać diagnostykę silnika; oczyść je oraz każdy schemat jobId identyfikujący najemcę, zanim wyślesz pliki outbox do zewnętrznych ujść.
  • Wyjście ewaluacyjne nigdy nie jest publikowane bez brandingu. Gdy branding jest wymagany i nie da się go zastosować, element trafia do dead-letter zamiast zostać zatwierdzony.
  • Dokładnie raz między zapisującymi jest tak silne, jak Twój adapter. Jeśli putIfAbsent() nie jest prawdziwym atomowym zapisem warunkowym, gwarancja degraduje się do semantyki pojedynczego zapisującego. Poświadczenia magazynu obiektów i polityka bucketu to sprawy hosta; moduł nigdy nimi nie zarządza.

Żaden opublikowany standard nie definiuje zachowania tego modułu. Gwarancje dokładnie raz, checkpointu i outbox na tej stronie są inżynierskimi kontraktami API NextPDF Enterprise, podanymi tu jako zewnętrznie obserwowalne zachowanie — nie są zgodnością z żadnym standardem ani certyfikacją wobec niego. Wewnętrzne użycie SHA-256 jako sygnatury integralności jest podobnie instalacją, nie deklaracją zgodności. Jak wszędzie w NextPDF: wsparcie to nie zgodność, a zgodność to nie certyfikacja. NextPDF nie posiada certyfikacji i żadnej nie udziela; to, czy wdrożenie zbudowane na tym module spełnia Twoje obowiązki regulacyjne lub kontraktowe, jest rozstrzygnięciem Twoich asesorów.

Zdarzenia końcowe nie są częścią transakcji checkpointu: emisja odbywa się po checkpoint.save(), więc outbox trzyma trwale każde wyemitowane zdarzenie, ale nie jest kompletnym rejestrem przez awarie. Zatwierdzone obiekty pozostają źródłem prawdy.

  • Każdy offset źródłowy na poziomie finalCommittedOffset lub poniżej osiągnął dokładnie jeden stan końcowy: Committed, Skipped lub DeadLettered.
  • Elementy są finalizowane w kolejności offsetów źródłowych; kolejność bariery to zatwierdzenie, zapis checkpointu, opróżnienie znaczników idempotencji, następnie emisja zdarzeń.
  • Ponowne uruchomienie przebiegu z tym samym runId nigdy nie publikuje podwójnie: offsety z checkpointu przewijają do przodu, a bajtowo identyczne ponowne zatwierdzenia to operacje puste z porównaniem sygnatury z idempotentReuse: true.
  • Świeży obiekt jest tworzony wyłącznie atomowym tworzeniem warunkowym; rozbieżne bajty pod zajętym kluczem bez overwrite to deterministyczny dead-letter SPEC-COMMIT-409, nigdy nadpisanie.
  • Niejednoznaczne zatwierdzenie zapisuje w checkpointcie sfinalizowany prefiks i przerywa z StreamProcessorException; nieudany offset nie jest przesuwany.
  • Przebieg crashSafe odmawia nietrwałych współpracowników committera, checkpointu, idempotencji lub dead-letter przed odczytaniem jakiegokolwiek wejścia.
  • Identyfikatory zdarzeń są czystą funkcją przebiegu, offsetu, klucza idempotencji i statusu, więc trwały outbox trzyma co najwyżej jeden wiersz na zdarzenie.

NextPDF Core renderuje jeden dokument naraz przez writer i kontrakt render-manifest — zobacz Writer. Sam Core nie ma trwałych strumieni zadań, wznowienia z checkpointu, deduplikacji idempotencji, zatwierdzania do magazynu obiektów ani outbox zdarzeń końcowych. NextPDF Pro dodaje trwały, współbieżny silnik renderujący oraz jednohostowe magazyny na systemie plików (Stream w Pro). Połowa międzyhostowa — ten procesor, committer magazynu obiektów i trwały outbox — wymaga NextPDF Enterprise.

Ta strona dokumentuje wyłącznie zewnętrznie obserwowalne zachowanie i wspieraną publiczną powierzchnię API. Wewnętrzne ścieżki przestrzeni nazw, klasy pomocnicze, tabele mechanizmów, nazwy plików runbooków i prefiksy zgłoszeń są poza zakresem.