Skip to content

feat(pipeline)!: transaction-aware Writer and the Executor→Reader rename#559

Open
ddeboer wants to merge 3 commits into
mainfrom
feat/transactional-writer
Open

feat(pipeline)!: transaction-aware Writer and the Executor→Reader rename#559
ddeboer wants to merge 3 commits into
mainfrom
feat/transactional-writer

Conversation

@ddeboer

@ddeboer ddeboer commented Jul 6, 2026

Copy link
Copy Markdown
Member

First slice of #534 (search as a Configurable Pipeline instance): the @lde/pipeline core changes everything else builds on. Design captured in ADR 6.

Rename ExecutorReader (commit 1)

The input unit is now named for the read side of the familiar Reader/Writer I/O pair (the Stack’s source/sink): ExecutorReader, execute()read(), SparqlConstructExecutorSparqlConstructReader, ExecutorContextReaderContext, AttachedExecutorAttachedReader, StageOptions.executorsreaders. Mechanical; @lde/pipeline-void re-exports the renamed types, @lde/pipeline-shacl-sampler adapts internally.

Transaction-aware Writer (commit 2)

Writer becomes a factory of per-run transactions, generic over its payload (Writer<Item = Quad>):

interface Writer<Item = Quad> {
  openRun(context: RunContext): Promise<RunWriter<Item>>;
}
interface RunWriter<Item = Quad> extends DatasetWriter<Item> {
  flush?(dataset: Dataset): Promise<void>;
  reset?(dataset: Dataset): Promise<void>;
  commit(): Promise<void>;
  abort(error: unknown): Promise<void>;
}
  • Pipeline.run drives openRun → write* → commit/abort uniformly and never branches on the writer’s update mode: an alias swap is a private step of a Blue/green writer’s commit(), a deletion sweep of an In-place writer’s.
  • RunContext carries runId, startedAt, selectedSources() (complete by commit time, including datasets skipped as unchanged – the input to a registry-membership sweep) and the provenance store.
  • Stages write through the narrower DatasetWriter, so a stage can never commit or abort the run.
  • Lifecycle-free destinations wrap a per-dataset write with perDatasetWriter() – deliberately no second TransactionalWriter interface.
  • A stage failure stays a per-dataset outcome (run still commits); a run-level failure (e.g. selection dying mid-iteration) aborts every fanned-out branch, even when one abort fails.

Ridden-along fixes

  • Per-run state (open files, cleared graphs) now lives in the run, so re-running a pipeline on the same writer instance replaces output instead of appending – previously a latent bug.
  • Chained (sub-stage) scratch FileWriters are now flushed before their output path is resolved – previously the chain resolved a final path that only materialized on a flush nobody issued.
  • FileWriter.commit finalizes files still open; abort discards temp output, never leaving a truncated final file.

Breaking changes

Both commits are marked !. Custom writers implement openRun (or wrap with perDatasetWriter); Executor call sites rename. The Dataset Knowledge Graph pipeline adapts on its next @lde/pipeline upgrade.

Part of #534 (slice 1 of 4; next: BlueGreenRebuild/InPlaceRebuild writers in @lde/search-typesense).

ddeboer added 2 commits July 6, 2026 17:47
The pipeline input unit is now named for the read side of the familiar
Reader/Writer I/O pair: a Reader produces quads for a dataset from
outside the pipeline, whether that is a SPARQL endpoint, a dump, or a
non-RDF facade. Only the input renames; Writer keeps its name.

BREAKING CHANGE: ‘Executor’ is now ‘Reader’ and its ‘execute()’ method
is ‘read()’. Renamed accordingly: SparqlConstructExecutor →
SparqlConstructReader, SparqlConstructExecutorOptions →
SparqlConstructReaderOptions, ExecuteOptions → ReadOptions,
ExecutorContext → ReaderContext, AttachedExecutor → AttachedReader (its
‘executor’ property → ‘reader’), StageExecutors → StageReaders, and
StageOptions.executors → StageOptions.readers. @lde/pipeline-void
re-exports the renamed types.
A Writer is now a factory of per-run transactions: Pipeline.run opens
one run (openRun(context) → RunWriter), writes every dataset through it,
and ends it with exactly one commit() or abort(error) – the home of
run-level lifecycle such as alias swaps, deletion sweeps and cross-pod
locks. The pipeline never branches on the writer’s update mode. Stages
write through the narrower DatasetWriter, so a stage can never commit or
abort the run. RunContext carries runId, startedAt, selectedSources()
(complete by commit time, including datasets skipped as unchanged) and
the provenance store. Lifecycle-free destinations wrap a per-dataset
write with the new perDatasetWriter() helper.

FileWriter and SparqlUpdateWriter keep per-run state (open files,
cleared graphs) in the run, so re-running a pipeline on the same writer
instance replaces output instead of appending – previously a latent bug.
FileWriter.commit finalizes files still open; abort discards temp
output. Chained (sub-stage) scratch FileWriters are now flushed before
their output is resolved. ShaclValidator opens one long-lived run per
report writer. See docs/decisions/0006.

BREAKING CHANGE: Writer’s per-dataset write/flush/reset moved to the
RunWriter returned by the new openRun(context); custom writers implement
openRun or wrap their per-dataset write with perDatasetWriter(). Generic
payload: Writer<Item = Quad>. Stage.run now takes a DatasetWriter.
Review cleanups on the transactional Writer:

- Fan-out commits run concurrently (mirroring abort): destinations are
  independent, so their alias swaps and sweeps – the slowest part of a
  run – no longer queue behind each other. Both share one
  settleBranches helper that rethrows the first failure after every
  branch had its chance.
- A chained stage’s scratch run is now bracketed like any other run:
  committed on success, aborted on failure – so a failing chained stage
  no longer leaves a stale temp file behind.
- RunWriter.commit documents the driver contract: every written dataset
  is flushed before commit, so commit-time unflushed writes only occur
  under direct use and should be finalized non-destructively.
- perDatasetWriter forwards flush/reset with bind instead of manual
  wrapper closures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant