Skip to content

Extraction runs: the Drivine store - #99

Merged
jimador merged 4 commits into
jimador/feat/extraction-run-store-contractfrom
jimador/feat/extraction-run-store
Sep 10, 2026
Merged

jimador merged 4 commits into
jimador/feat/extraction-run-store-contractfrom
jimador/feat/extraction-run-store

Conversation

@jimador

@jimador jimador commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

PR 8 of the extraction-integrity train (refs #67), stacked on #98. The Drivine/Neo4j run store. The terminal transition is one Cypher statement taking the node's exclusive lock before reading status, backstopped by a uniqueness-constrained terminal-write node, so two writers that both saw RUNNING cannot both commit; the constraint's sufficiency is proven by mutation (removing both guards yields six racing writers, all APPLIED, three contradictory endings for one run), the lock's by the documented isolation argument, and the docs say which is which. Fingerprint strings are stored verbatim, compared verbatim, never re-derived — the inherited contract case that kills re-derivation passes. Invocation records live as child rows on their own constrained key (contextId, runId, index, attempt) and survive header saves; every read is tenant-scoped ahead of its LIMIT and cross-tenant fail-closed is integration-proven; ContextId is capped at 1024 on writes, closing the run key's last unbounded component. The #98 contract suite runs unmodified against this store, plus Drivine-specific integration tests including the multi-process-shaped race.

Changed in this review round:

  • The failure vocabulary persists as typed columns. Failures store code, stage, providerStatus, measure, at and the invocation reference; no stored property is shaped to hold text, so the model-level privacy guarantee survives the round-trip.
  • Applied transitions are announced from Drivine. The store takes a constructor-injected DiceEventListener and emits ExtractionRunTransitioned exactly once per run that ended, matching the in-memory reference; the base suite's event cases run inherited against Neo4j.
  • Header and invocation rows digest through ExtractionRunFingerprint.ofFields under HEADER_VERSION and INVOCATION_VERSION, backing the versioned compare-and-set header writes from Extraction runs: lifecycle machine and store contract #98.

Breaking changes: none. New classes in dice-storage; hosts adopting the run store declare the schema items the CHANGELOG enumerates.

Opt-in and status: EXPERIMENTAL. DrivineExtractionRunStore activates only where a host wires it; nothing in DICE produces a run yet — the coordinator is the first real caller and is sequenced next, after #101's lineage write.

Next in stack: #101 — proposition lineage, and the canonical-persistence rule.

@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 3b88997 to 1eb1161 Compare September 1, 2026 04:20
@jimador
jimador marked this pull request as ready for review September 1, 2026 04:20
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 1eb1161 to 0f340d4 Compare September 1, 2026 14:07
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 0f340d4 to 365472e Compare September 2, 2026 10:51
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 365472e to 6ebed96 Compare September 2, 2026 14:11
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch 2 times, most recently from 5785b1a to dba14a1 Compare September 2, 2026 15:24
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from dba14a1 to 170f2b9 Compare September 2, 2026 20:35
@jimador
jimador requested a review from igordayen September 2, 2026 20:41
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 170f2b9 to 95a61be Compare September 2, 2026 20:50
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 95a61be to 9106d5e Compare September 7, 2026 04:14
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch 2 times, most recently from a36bb34 to 695947b Compare September 8, 2026 20:46
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 695947b to 3ec1027 Compare September 8, 2026 21:07
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch 2 times, most recently from 996790d to 222a184 Compare September 9, 2026 00:02
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 222a184 to f074078 Compare September 9, 2026 01:13

@igordayen igordayen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jimador - looks good, could you please review few inquiries i left, thank you

@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from f074078 to 0a0149b Compare September 9, 2026 21:57
@jimador
jimador requested a review from igordayen September 9, 2026 21:58

@igordayen igordayen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jimador - looks good, subject to rebasing, thanks

@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 0a0149b to abbf4eb Compare September 10, 2026 02:02
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from abbf4eb to 467a5b4 Compare September 10, 2026 02:10
The terminal transition is one statement that takes the node's exclusive
lock before reading status, backstopped by a uniqueness-constrained
terminal-write node so two writers that both saw RUNNING cannot both
commit — the constraint's sufficiency is measured by mutation, the lock's
argued, and the docs say which is which.

Header writes carry a version and take it under compare-and-set, and they
write header fields only. recordInvocation is the only door onto invocation
state, insert-or-compare on the invocation's own constrained key, so a
header save can never create, update or delete an invocation row. Records
are locked once terminal through that door — SUCCEEDED, FAILED and
CANCELLED alike; an identical resend replays, and one differing in a single
field, including a field cleared to absent, is rejected.

Fingerprints come off the canonical length-prefixed encoding with a version
tag per kind, so a serializer or bind-map ordering change leaves equivalent
state equivalent, and a header digest can never collide with an invocation
or terminal one. Validation precedes mutation: a rejected write leaves no
orphan node even when the ambient transaction that caught it commits, and
independent attempts written in one caller transaction each settle on their
own merits. A writer recovering from Neo4j's deadlock detector retries in a
transaction the failure never touched.

Every read scopes to the tenant ahead of its limit, chain walks are bounded
client-side with a seen set, and all runs of a root resolve in one seek off
the denormalized ref. The contract suite from the previous slice runs
unmodified against this store, fifty-nine for fifty-nine.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Failures store as flat typed fields with no text-shaped property, and
a raw-property canary pins the stored key set against an allowlist on
both write doors, so a free-text column cannot return unnoticed. Reads
tolerate an unrecognized property on a corrupted node and return the
recognized fields, stated in the mapper's KDoc. The store announces an
applied transition after the surrounding transaction commits: a
caller-owned rollback delivers nothing and leaves the run untouched,
pinned by test, and a replay or rejected write announces nothing. The
terminal fingerprint comparison carries the v2 identity-only material,
and the dead v1 count and failure encoders are gone. The inherited
contract suite runs unmodified against Neo4j.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Every org.neo4j.driver.exceptions.Neo4jException carries a code() straight
from the server, and a uniqueness constraint violation always carries
Neo.ClientError.Schema.ConstraintValidationFailed. That code holds across
driver versions the way an exception message never can, and Drivine does
not translate driver exceptions into Spring's DataAccessException
hierarchy, so there is no portable Spring type to catch here either.

Add Neo4jErrors, an internal object in dice-storage with one function,
isUniquenessViolation, that walks a cause chain (cycle-safe) and answers
true when any cause is a Neo4jException carrying that code. Neo4jErrorsTest
covers a ClientException carrying the code, a wrapping exception whose
cause carries it, a ClientException with a different code, a message that
merely says already exists, and a self-referential cause chain.

dice-storage now declares org.neo4j.driver:neo4j-java-driver directly so
the exception class is visible at compile time. Drivine already runs on
this driver at runtime, and embabel-agent-dependencies manages its version
at 6.1.0.

Signed-off-by: James Davis <jamesd1184@gmail.com>
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
DrivineExtractionRunStore.transition and
DrivinePropositionRepository.saveInOwnedTransaction each carried their own
private isUniquenessViolation, matching "ConstraintValidationFailed" or
"already exists" in an exception's message. Both now call
Neo4jErrors.isUniquenessViolation, dropping the private functions and the
KDoc that said the two checks were duplicated because neither module had a
home for a shared helper.

CHANGELOG: one bullet under the DrivineExtractionRunStore entry noting the
race detection keys on the driver's status code and the new pom
dependency. Additive, no public signature changes.

Signed-off-by: James Davis <jamesd1184@gmail.com>
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
@jimador
jimador force-pushed the jimador/feat/extraction-run-store branch from 467a5b4 to 417a5f5 Compare September 10, 2026 02:17
@jimador
jimador merged commit 0dc362d into main Sep 10, 2026
16 checks passed
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.

3 participants