Skip to content

[YTDB-382] Transactional schema operations and per-class schema records - #1150

Merged
Andrii Lomakin (andrii0lomakin) merged 341 commits into
developfrom
transactional-schema
Jul 28, 2026
Merged

[YTDB-382] Transactional schema operations and per-class schema records#1150
Andrii Lomakin (andrii0lomakin) merged 341 commits into
developfrom
transactional-schema

Conversation

@andrii0lomakin

@andrii0lomakin Andrii Lomakin (andrii0lomakin) commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Before this change, storage led a schema change: creating or dropping a class or an index mutated storage structure first (the collections and the index engines), then reflected the result into a metadata record. Each such operation self-committed in its own micro-transaction outside the user's transaction, the whole schema lived in one record rewritten on any class change, and a schema change could not be rolled back with the transaction that made it. YTDB-382 targets exactly this: the write amplification of the monolithic schema record and the non-transactionality of schema operations.

Planned changes

Current state (before this branch). Schema and index operations self-committed outside the user transaction; the entire schema was one record; rollback of a schema change was impossible; a class rename rewrote storage files through a rename path the WAL did not journal; entity validation silently skipped constraints created in the same transaction.

What changes. Schema and index DDL is fully transactional: a change mutates only metadata records during the transaction, stays invisible to other sessions until commit, and rolls back for free with the user transaction — a rolled-back or crashed-before-commit transaction leaves storage files byte-for-byte unchanged, and a committed structural change replays from the WAL. Per-class schema records replace the monolithic record, so a one-class change writes one record. A class rename touches zero storage files and keeps every index accelerating under the new name; an index rename is metadata-only. The immutable schema snapshot is transaction-aware, so entity validation and serialization enforce same-transaction classes, property types, and constraint rules instead of silently skipping them. Exactly one schema-changing transaction runs at a time, serialized by blocking (never aborted on contention). Three on-disk formats move: the schema record format (4 to 6, strict equality gate), the storage-configuration format (23 to 24, rejected in both directions), and the export dump format (14 to 15); opening an old-format database is rejected with a redirect to the operator export/import migration, and a genesis completion marker makes a crashed half-created database refuse to open loudly.

How. During a transaction, schema writes route through SchemaProxy to a per-session copy-on-first-write SchemaShared, seeded by a read-only re-parse of the committed records; SchemaProxedResource is the write choke point, and TxSchemaState carries the copy, the changed-class set, and the IndexOverlay (index definitions overlaid: committed + tx-created − tx-dropped). Collections created in the transaction carry provisional negative ids resolved to real ids at commit before any record serializes. At commit, AbstractStorage diffs committed against tx-local collection ids (a rename is structurally inert), drops and creates collections and engines inside the commit's own atomic operation, builds a tx-created index eagerly only when its source collection is empty (a populated source is rejected loudly), promotes the tx-local schema into the existing shared instances, and publishes the overlay as replacement objects — all under a fixed four-lock order headed by the new MetadataWriteMutex, with the storage write lock held from commit entry (pure-data commits keep the read-lock fast path). OperationsFreezer gains a freeze-kind taxonomy so a schema commit throws loudly with zero locks held against an operator freeze while parking normally for transient quiesces. A late, CI-caught ABBA inversion between that commit sequence and the index manager's exclusive regions — which could reach a fresh schema read lock deep inside record deserialization — was fixed by funnelling every non-commit exclusive region of IndexManagerEmbedded through one ordered acquire (committed schema read lock first, index-manager write lock second, held across the region), and the lock order is now enforced by an always-on runtime guard in SchemaShared (overhead within noise, deterministic regression tests). Index-engine files are keyed by a persisted per-engine file-base id and collection names by a bare counter — that is what makes renames metadata-only. Genesis is a two-phase bootstrap (one schema transaction, then one data transaction) with the blob collections created by storage itself. Migration ships as hardened DatabaseExport and DatabaseImport (fail-closed, version-keyed import strictness) plus the operator runbook operator-migration-procedure.md in the product docs. The permanent design record — design-final.md and adr.md, the latter restating every decision record in full — lives in docs-internal/adr/transactional-schema.

Key decisions. The schema gets a full tx-local copy rather than a merge-on-read overlay (the copy reuses the existing derived-state recomputation; an overlay would re-implement it inside the read path), while indexes get the opposite call — a definition overlay, never a content copy, because an index is a thin handle over a storage-backed engine. Single schema writer by blocking; contention-abort was rejected. The commit-time delta reads the transaction's own change tracking (no separate intent list), with drops detected as a set difference over collection ids. Structural revertibility rides the existing atomic-operation WAL with no deletion pool, resting on a replay fix that materializes a missing file from the create record inside its own atomic unit. Engine files are keyed by a persisted file-base id after the planned registry-slot keying was falsified (slot ids are reused by design after failed-commit cleanup). The tx-aware snapshot replaced both per-field proxy resolution (too slow for the validation hot path) and the committed-only snapshot (silently skipped same-transaction constraints). Format migration is operator-driven export/import; an in-place on-open migrator was rejected so new code never parses the old format and no partial-migration state exists. Lock-order conformance is enforced by an always-on runtime guard rather than review discipline alone — assert-only was rejected because production runs with assertions disabled, and the silent alternative is a non-interruptible parked deadlock.

Out of scope. The populated-source (off-lock, streamed) index build and incremental index-manager link-set serialization (YTDB-1064); the inert index-name rename and an explicit rename statement (YTDB-1066); the residual index-visibility window for concurrent pure-data commits (YTDB-1101); cross-thread reaping of a stranded schema transaction (YTDB-1114). A rid-mapping-aware database comparator was considered and not commissioned — the import renumbers collections, so id-keyed comparison is structurally unsatisfiable and migration verification is logical equivalence; a cross-section point-in-time export pin remains a possible future design. The per-index lock inside IndexAbstract remains outside the documented lock order (the index rebuild, index fill, and standalone index delete paths take it above the ordered tiers) — a pre-existing latent inversion family the runtime guard does not cover, tracked separately in the issue tracker.

Risks & accepted trade-offs

Design review: user-approved — 2026-06-16 (carried over from legacy workflow gates)
Adversarial review: passed, 5 accepted risks — 2026-06-15 (carried over from legacy workflow gates)

No branch artifact enumerates a labeled five-item list — that verdict line was synthesized at a mid-branch workflow migration. The accepted residuals on record at design close were the four issue-backed deferrals listed under Out of scope (YTDB-1064, YTDB-1066, YTDB-1101, YTDB-1114) plus the one genuine trade-off: a schema-carrying commit holds the storage write lock for its whole duration and excludes concurrent data commits, accepted on the premise that the schema-change rate is low — the same premise that bounds the stall envelope together with the empty-source-only in-commit index build.

Two mid-execution adversarial design gates also ran and passed: the engine-file identity decision was revised (two blockers resolved) and re-approved before implementation, and the concurrency-control design (the metadata-write-mutex lifecycle and the freezer gate) was re-reviewed over three rounds on 2026-07-21, all blockers resolved by amendment and the amended design re-approved. The full per-decision accepted-risk register (engine-file stems, mutex lifecycle, freezer gate, including the pre-existing identity-less freeze-counter residuals) is restated in adr.md under docs-internal/adr/transactional-schema.

Standing note for reviewers: the earlier flake-family classification of SchemaCommitReconciliationTest disk-profile failures is withdrawn — the intermittent CI kills included a real ABBA lock-order deadlock, fixed on this branch with a runtime guard and deterministic regression tests. Treat any failure there as a genuine signal first.

Verification approach

Current state, after the post-flip lock-order fix: Spotless clean; full core and tests unit suites green with zero failures; integration profile green (0 failures, ~3h17m); coverage of changed lines 88.2% line / 79.9% branch versus develop; CI green on all platform legs.

Andrii Lomakin (andrii0lomakin) added a commit that referenced this pull request Jun 16, 2026
…pened

The handoff bridged the paused readability-pass session. Pass-2 (Mutation 4)
is committed and the draft PR is open, so the handoff's purpose is fulfilled.
Removing it lets the next /create-plan session auto-resume Step 4b (plan
derivation) cleanly: design.md is committed and clean with no
implementation-plan.md, which is exactly the Step-1c auto-resume condition.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds a comprehensive set of workflow documentation, adversarial analysis logs, design mutations, and handoff planning files for the transactional schema operations feature. The reviewer's feedback highlights several formatting and documentation standard issues, including the need to explicitly define the target audience and prerequisite knowledge in the first paragraph of the design overview, and to consistently enclose property names, tool names, file names, and parameter references in backticks across the Markdown files.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

I am having trouble creating individual review comments. Click here to see my feedback.

docs/adr/transactional-schema/_workflow/design.md (6-12)

medium

According to the general rules, design documents must establish the intended audience and prerequisite knowledge through prose framing in the Overview's first paragraph. Standalone metadata blocks are forbidden. Please update the first paragraph to explicitly frame the target audience (e.g., YouTrackDB core developers and maintainers) and the required prerequisite knowledge (e.g., familiarity with YouTrackDB's storage, transaction, and schema architecture).

References
  1. Design documents must establish the intended audience and prerequisite knowledge through prose framing in the Overview's first paragraph; standalone metadata blocks are forbidden.

docs/adr/transactional-schema/_workflow/handoff-planning.md (31)

medium

According to the general rules, property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks for consistency. Please enclose mcp-steroid, CLAUDE.md, steroid_list_projects, transactional-schema, PSI, grep, and Read in backticks.

References
  1. Property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks for consistency.

docs/adr/transactional-schema/_workflow/handoff-planning.md (35)

medium

Please enclose the tool names mcp-steroid and PSI in backticks to maintain consistency with the repository rules.

References
  1. Property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks for consistency.

docs/adr/transactional-schema/_workflow/handoff-planning.md (37)

medium

Please enclose the file names design-mutations.md and design.md in backticks for consistency.

References
  1. Property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks for consistency.

docs/adr/transactional-schema/_workflow/handoff-planning.md (42)

medium

For consistency and adherence to the general rules, file names like house-style.md and design.md should be enclosed in backticks.

References
  1. Property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks for consistency.

docs/adr/transactional-schema/_workflow/reviews/readability-feedback-design-pass2.md (14)

medium

According to the general rules, property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks. Please enclose ScalableRWLock, OperationsFreezer, cutWaitingList, unpark, freezeRequests, and EXPORTER_VERSION in backticks.

References
  1. Property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks for consistency.

docs/adr/transactional-schema/_workflow/reviews/readability-feedback-design-pass2-round2.md (14)

medium

According to the general rules, property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks. Please enclose ScalableRWLock, OperationsFreezer, cutWaitingList, unpark, freezeRequests, and EXPORTER_VERSION in backticks.

References
  1. Property names, tool names, and parameter references in Markdown documentation should be enclosed in backticks for consistency.

Andrii Lomakin (andrii0lomakin) added a commit that referenced this pull request Jul 14, 2026
…pened

The handoff bridged the paused readability-pass session. Pass-2 (Mutation 4)
is committed and the draft PR is open, so the handoff's purpose is fulfilled.
Removing it lets the next /create-plan session auto-resume Step 4b (plan
derivation) cleanly: design.md is committed and clean with no
implementation-plan.md, which is exactly the Step-1c auto-resume condition.
Andrii Lomakin (andrii0lomakin) added a commit that referenced this pull request Jul 20, 2026
…pened

The handoff bridged the paused readability-pass session. Pass-2 (Mutation 4)
is committed and the draft PR is open, so the handoff's purpose is fulfilled.
Removing it lets the next /create-plan session auto-resume Step 4b (plan
derivation) cleanly: design.md is committed and clean with no
implementation-plan.md, which is exactly the Step-1c auto-resume condition.
Andrii Lomakin (andrii0lomakin) added a commit that referenced this pull request Jul 27, 2026
…pened

The handoff bridged the paused readability-pass session. Pass-2 (Mutation 4)
is committed and the draft PR is open, so the handoff's purpose is fulfilled.
Removing it lets the next /create-plan session auto-resume Step 4b (plan
derivation) cleanly: design.md is committed and clean with no
implementation-plan.md, which is exactly the Step-1c auto-resume condition.
@andrii0lomakin Andrii Lomakin (andrii0lomakin) changed the title [YTDB-382] Transactional schema operations [YTDB-382] Transactional schema operations and per-class schema records Jul 27, 2026
@andrii0lomakin
Andrii Lomakin (andrii0lomakin) marked this pull request as ready for review July 27, 2026 14:14
The YTDB-382 Phase-0 research lived in a bespoke decision-log layout that predates the current research-log convention. Reshape it into the canonical _workflow/research-log.md form that /create-plan Phase 1 reads, so the branch can enter planning without a format gap.

Rename the file and map the four working sections to canonical headings (Initial request, Surprises & Discoveries, Decision Log, Open Questions), demote the adversarial-findings block to a subsection, and append an Adversarial gate record with one dated verdict heading per hardening pass (1-12). Findings, decision records, and open questions are preserved verbatim with their cross-references; D-records keep their prose form rather than the Why/Alternatives template. Baseline and re-validation is omitted (not a workflow-modifying branch); F107's stale section references are repointed to the renamed subsection.
F108 claimed per-unpark re-evaluation covered the layered freeze case, but
the freezer only unparks the waiting list when freezeRequests hits zero
(OperationsFreezer:97), and freezeOperations never unparks. A schema commit
parked behind a transient quiesce (DiskStorage:356) while an operator freeze
layers on (1->2) and the transient then releases (2->1) is therefore never
woken for the operator freeze's whole duration, holding the four-lock window
and reopening the F86 DDL outage the gate exists to prevent.

Accept shape (a): the operator-kind arm of freezeOperations cuts and unparks
the waiting list after its increment, so the woken schema-commit entrant
re-evaluates the kind-aware gate and throws while data entrants re-park. The
freezer's existing Dekker discipline bounds the race and cutWaitingList stays
cut-safe under the now-two unpark sites. Shape (b) (timed park) was rejected:
its parkNanos backstop leaves a bounded residual outage and demotes the
in-window gate to polling.

Folds the layered-case wake pin and a fourth acceptance line into D7's freezer
bullet, plus correction/extension notes on F108 and F86. PSI-confirmed: sole
callers releaseOperations<-AOM:252, cutWaitingList<-OF:105, addThread<-OF:44.
The F104 handshake pinned the holder write order and the engagement record's
survival across clear()/close(), but not the session-side record's write
position. The foreign teardown's release pass read that record for the ordinal
it presents to the session-keyed CAS; a record written after the mark re-check
as a plain write has no happens-before edge to the foreign read, so the
teardown sees no ordinal, the CAS mismatches and warn-noops, and the permit
wedges -- the exact failure F104 set out to kill.

Accept shape (b): route the release ordinal by path. The normal release runs
on the acquiring thread, which both wrote and reads the session-side record --
same-thread program order, no publication question, captured ordinal for
anti-stale. The teardown is the only foreign-thread releaser, and it already
loads the volatile holder to identify the session; the holder carries the
ordinal (F105 triple), is cross-thread published by the engage's holder write,
and survives session wipes on the mutex. The teardown therefore CAS-clears off
the holder it reads and never consults the record, so the record's write
position no longer gates the foreign heal. Shape (a) (write record before
holder) was rejected as the heavier argument: it keeps the foreign path on the
record and needs two ordering pins. Zero new fences either way.

Folds the path split into D7's abnormal-termination bullet, plus amendment and
extension notes on F104 and F96.
F106 named the checkOpenness gate as a second load-bearing exclusion property
against a still-running commit-phase zombie, but it reads the plain status
field (DatabaseSessionEmbedded:223) with no JMM edge from the foreign
teardown's CLOSED write (internalClose:2234), so "never a fresh one" is
best-effort, not structural.

State it as a best-effort early cap and name F52's whole-commit SchemaShared
lock scope as the structural exclusion authority. A late-visible status admits
at most one more zombie commit, harmless because that straggler serializes
behind F52's lock on a cleared tx (F85). Rejected the volatile-status
alternative: D7 already settled plain tx status as a shipped memory mode (the
F83-F85 settlement), so making it volatile would reverse a standing decision
and tax every checkOpenness call for a property F52's lock already delivers.

Folds the demotion into D7's teardown bullet plus an amendment note on F106.
The re-keyed Guard (F38) bullet and the teardown bullet's pre-existing
sentence both said a different-session release is "rejected loudly", splitting
the mismatch outcome two ways and contradicting the abnormal-termination
bullet, which specifies one outcome for all three mismatch arms. A loud throw
is wrong at the release site: it runs in the teardown finally, where a throw
masks the owner's real exception (the F97 shape).

Align both sentences to the mechanism: every release-site mismatch (different
session, stale ordinal, holder already null) loses the CAS and warn-noops,
permit untouched. The only loud rejection is the engage-side predicate (F105):
holder.thread == currentThread && holder.session != engagingSession.

Folds both D7 sentences plus a closure note on F107 (the anchor it re-keyed).
The F110 relabel of the legacy exporter's promote class was still not
context-exact. Three gaps: a fault between writeFieldName and its value leaves
a pending name, so close() emits "name":} -- structurally closed but malformed
and parse-rejected at import, not well-formed; the F109 swallow arm was keyed
on "record object open" rather than the innermost stranded context, so a
swallow mid-embedded-array (which strands at array context and must NOT
promote) was miscounted; and the between-sections / array listings read as
definitions rather than illustrations.

Make "leaves the generator at object context" the sole classifier, demote the
listings to illustrations, qualify object-context promotion as structurally
closed but possibly malformed (fail-closed at import), and condition the
swallow arm on the stranded context. Jackson 2.21.4 confirmed: writeEndObject
checks only inObject() with no dangling-name guard.

Folds the corrected statement into D20's bounding parenthetical plus a
refinement note on F110; F90/F100 inherit via their relabel pointers.
F109's "the primary-exception pin trivializes" and D20's "two mechanism pins
deliver those outcomes" overclaimed. The promote-only-on-success and
per-record-isolation pins deliver the no-file outcome, but the discard path
still performs cleanup I/O (generator and gzip/file close, .tmp delete), so a
finally-resident cleanup throw can still replace the in-flight scan failure
(correlated disk-full is the live case). The pins narrow the secondary class
to cleanup-I/O failures; they do not eliminate it.

Reword "trivializes" to "narrows" in the F109 resolved block and D20's outcome
clause, and state that the primary-exception outcome stays delivered by the
F94 (b) addSuppressed / log-then-rethrow discipline, which stays load-bearing.
F109's per-record write isolation buffered each record whole, but "record
sized" is unbounded (embedded recursion, nested collections, base64 4:3), so
the buffer was O(rendered record). A too-large record fails its render:
best-effort sheds a healthy record as broken, fail-fast aborts -- the next
migration cannot export a database the storage handles fine. And OutOfMemoryError
is an Error the per-record catch(Exception) at DatabaseExport:221 does not
catch, so "discarded whole" is undeliverable for the OOME class.

Bound the buffer with a spill-to-temp path: render in memory up to a threshold,
spill to a transient temp file beyond it (streamed in on success, discarded on
render failure), preserving whole-or-nothing isolation at O(threshold) memory
for any record size on this offline tool. Bounding also removes the per-record
OOME (a large record spills rather than exhausting the heap); a general OOME
stays fail-closed via promote-only-on-success. Rejected the report-oversized /
loud-abort alternative: it leaves a valid database un-migratable.

Folds the bound into F109's resolved block, D20's isolation pin, and F94's
isolation note.
The F113 error-capture liveness control provoked a sentinel "through the
logger", but SLF4JLogManager resolves and caches a logger per requester-class
name (:48-64), and every DatabaseExport error line travels the
...db.tool.DatabaseExport category (requester this at
152/213/225/281/293/606), which JUL/logback/log4j2 filter and route
independently. A sentinel provoked through any other category lands in the
capture while the real export error lines route elsewhere, so the control
false-passes and an empty capture reads as clean.

Pin the sentinel at error level through the export tool's own category
(LogManager.error(DatabaseExport.class, ...)), and check that category's
effective destination, not the root's. Level direction was already safe.

Folds the category pin into D20's review pin and F113's resolved block; F102's
two-capture structure is unchanged.
All eight pass-12 findings (F114-F121) are settled, so the gate-state note and
the pass-12 entry now record that. The gate stays open pending the
pass-13-vs-dry re-attack, after which the formal Phase 0->1 gate runs.
Track 8 Step 6 - the ruled Q-M2/SR2 info-validation matrix at the
pre-flight seam plus the WI3 operator migration-procedure page.

Before this change the importer read only the exporter version and the
best-effort marker from the dump's info section: a dump produced by
NEWER binaries (exporter version >= 16) sailed through the v15
strictness arms and imported silently, an absent, malformed, or
out-of-range schema-version was skipped unread, a known info field
carrying the wrong type was ignored, an unparseable exporter version
surfaced as a bare NumberFormatException naming nothing, and a dangling
info field name (the mid-write crash shape) desynced the reader past
the deferred-preamble boundary before erroring.

The matrix (design M2.b-5, ruling R4 + Q-M2, gate WI12a/b), judged in
runPreFlightChecks so every rejection is genuinely pre-mutation:

- exporter-version dispatch: >= 16 rejects with a redirect naming both
  versions AHEAD of every v15 arm (making the >= 15-keyed arms
  effectively == 15 - the Step 5 as-built end-state; also resolves
  CQ24's hardcoded-v15 wording by ordering); an unparseable version
  rejects fail-closed naming the raw value (WI12a, same outcome as an
  undeclared one); declared <= 14 rides the lenient path unchanged.
- schema-version: mandatory for v15, must sit in
  MIN_IMPORTABLE_SCHEMA_VERSION..SchemaShared.CURRENT_VERSION_NUMBER
  (6..6 today, a one-constant bump next time); missing, malformed, and
  out-of-range each reject naming declared vs supported.
- known optional info fields are type-checked if present (WI12b;
  violations collected at parse, judged v15-only for R1); unknown extra
  fields are tolerated and logged (the exporter version is the
  compatibility contract, not field enumeration).
- a dangling/malformed info field name is rejected at parse (FM-M10),
  pre-mutation, instead of desyncing the reader - no honest dump of any
  version produces one, so acceptance is unchanged.
- the best-effort ack gate stays MARKER-KEYED per the Step 6 ruling
  (SR3, resolving BG23/CQ26/CS66): no honest legacy exporter writes the
  marker, and rejecting a hand-edited dump absent an explicit
  acknowledgment is intended fail-closed behavior.

The new docs/operator-migration-procedure.md (WI3, folding CS44 and the
SR1 condemn-target doctrine, CN59 genesis-incomplete guidance incl. the
OSystem case, CS59/FM-M18 crash residue, and the Step-5-gate exit-0
phrasing: exit 0 = every dump entry was consumed and verified against
the manifest) is indexed in docs/README.md and pinned by a content
test.

Pin M.5 #12's literal DatabaseCompare comparison is id-keyed and
structurally unsatisfiable against the renumbering import (the
historical DbImportExportTest using it is @disabled for the same
reason); the rehearsal pins logical equivalence instead - schema,
per-class counts, record contents, link topology, index, and blob
bytes - recorded as a blocked-letter deviation for the orchestrator.
Checkbox + commit slot, progress-log line, the Episodes entry (ten
red-first signatures, the SR3 marker-keyed ack-gate ruling recorded in
the design-drafts rulings section, the WI3 page content record, pin
coverage incl. the pre-existing #10 host test), and the Surprises entry
for pin M.5 #12's blocked DatabaseCompare letter (id-keyed comparison
vs the renumbering import; logical-equivalence rehearsal shipped,
letter left to an orchestrator ruling).
…, scalar values

Review-fix iteration 1 for Track 8 Step 6 (commit 612340c), merged
should-fix set from baseline/crash-safety/docs step-6 reviews:

- WI60 (BLOCKER): the runbook documented EXPORT DATABASE / IMPORT
  DATABASE commands that do not exist anywhere in the product (no
  grammar token, no console command, no server op, no CLI). The page
  now documents the REAL surface - the programmatic DatabaseExport /
  DatabaseImport tools driven from a wrapper JVM via
  YourTracks.instance(...) -> YouTrackDBImpl.open(...) - with
  copy-runnable snippets, an honest note on their internal-package
  status, and the failure contract (the tools signal every failure by
  throwing).
- F1 (BG29=CS76): the best-effort MARKER now parses from the
  quote-stripped token (parent readBoolean parity) - a hand-edited
  quoted "true" on a declared-legacy dump arms the SR3 marker-keyed
  ack gate again (fail-closed) instead of silently disarming it; the
  v15 WI12b type violation for the quoted form is kept as-is.
- F2 (BG30=CS78): the importInfo and importManifest field loops are
  EOF-bounded (hasNext()) with loud post-loop truncation rejections -
  a dump truncated mid-info previously spun forever on stale reader
  state while growing the unknown-field list toward OOM (demonstrated:
  OutOfMemoryError in 34s), and a dump truncated mid-manifest hung
  outright. Ungated by version: such dumps could never import (they
  hung), so acceptance is unchanged - the hang becomes a rejection.
- F3 (CS75): info-field VALUES are now rejected at parse when
  structured ('{'/'['-led) - the reader's until-the-separator scan
  cannot distinguish a nested closing brace from the info object's own
  close, so an object-valued unknown field desynced the parse, passed
  pre-flight on a truncated capture, MUTATED the target, and rejected
  post-mutation (an SR1-boundary violation); the trailing placement
  even imported silently. Chosen remedy: the recorded SCALAR-ONLY rule
  (pre-mutation, loud, naming the field) over a structure-aware skip -
  no dump shape any exporter has ever written carries structured info
  values, field-NAME tolerance is untouched, and the version number
  remains the compatibility contract.
- F4 (TQ29): ordering pin - a v16 dump missing its schema-version must
  get the redirect message, not the schema-version complaint
  (discrimination proven by temporarily reordering the arms).
- F5 (WI61): 'exit status 0' is bound to the real observable (wrapper
  process; tool returned without throwing).
- F6 (WC61): the exit-0 meaning is scoped - legacy (<= 14) dumps carry
  no manifest and receive no structural verification; stated as an
  explicit migration caveat.
- F7 (WC62): the accept/reject table's schema-version row is scoped to
  v15 dumps (legacy alien schema-versions stay lenient, as pinned).
- F8 (WC63): step 2 no longer claims interrupted exports delete their
  temp file - only in-process failures clean up; kill/crash leaves
  residue (cross-referenced to the residue section, FM-M18).
- F9 (WI62): 'discard' is bound to the drop surface
  (manager.drop(...), CN54-exempted for genesis corpses; the OSystem
  case keeps directory deletion - it has no drop surface).

The doc pin test's mandated-content list grew to cover the new
obligations (real surface named, without-throwing contract, drop
binding, legacy no-structural-verification caveat, v15-scoped row,
killed-or-crashed wording) while keeping all previous pins verbatim.
Log the applied findings (WI60 blocker runbook rewrite to the real
programmatic surface; F1 quoted-marker gate parity; F2 EOF-bounded
info/manifest loops with the live OutOfMemoryError and timeout red
signatures; F3 recorded scalar-only info-value rule with both desync
shapes; F4 redirect-ordering pin with its discrimination proof; F5-F9
doc corrections with the content-pin kept in sync), the chosen-remedy
records, verification numbers, and the deferred suggestion list.
Log the Step 6 review-fix iteration 1 gate verdict (integrity check
PASS - all F1-F3 production hunks present despite the mid-work git
checkout mishap; WI60 + F1-F9 + pin-test coherence all VERIFIED; 0 RG
findings, ledger stands at RG4), fix the gate's cosmetic nit (the
runbook snippets' adminPassword placeholder is now declared so both
main() examples compile verbatim; content-pin test re-run green), and
add the three Step 6 review reports (baseline, crash-safety, docs).
Deferred suggestions (CQ29-32, TQ30-32, CS77, WI63/64, WS60/61) and
Step-5 residuals unchanged.
…, message order

Track 8 track-level (cumulative) review-fix iteration, merged set from
the baseline/crash/concurrency cumulative reports:

- CN60 (should-fix): AbstractStorage.getCollectionNames and
  SchemaShared.getBlobCollections now COPY under their guarding locks
  instead of returning live backing views that escaped the lock. The
  exporter iterated those views with no lock held while concurrent DDL
  commits mutate the backing HashMap/IntSet under the write lock -
  JMM-undefined: a CME at best, silently skipped entries at worst,
  under-reading maxCollectionId and truncating an exit-0 export whose
  self-consistent manifest still verifies. Deterministic pins assert
  the snapshot property that makes the race impossible (the returned
  set must not reflect DDL performed after the call - a genuine
  cross-thread interleaving needs a HashMap resize racing an unlocked
  iterator and is not deterministically buildable). Side benefit:
  ImmutableSchema snapshots no longer silently track live blob-set
  changes.
- CS80: the hasNext() EOF-bound + loud post-loop truncation rejection
  (the 46b0446 pattern) is extended from importInfo/importManifest
  to every remaining reader loop - collections, records, indexes
  (outer and inner), collectionsToIndex, brokenRids, schema classes,
  class fields, globalProperties, super-classes, properties,
  customFields - via a shared truncatedDump helper. Red-first: a dump
  truncated inside an indexes entry spun forever on stale reader state
  (silent CPU hang, TestTimedOutException at HEAD); the records family
  proved protected-by-accident at HEAD for every constructible cut
  (stale replays die on the rid-map unique key or stale-token parse
  errors) - its test pins the now-by-construction loudness with a
  timeout guard. All post-loop checks key on the missing terminator,
  never on hasNext(), so an honestly-closed structure cannot
  false-trip (R1-safe; truncated dumps of any version previously hung
  or desynced, never imported).
- CN62: the import constructor captures the physical size from the
  OPENED descriptor (FileChannel.size, fstat-after-open) instead of a
  path stat before open - a concurrent re-export promoting between
  stat and open made the step-(3) size arithmetic falsely condemn a
  healthy import. Not deterministically testable without filesystem
  interposition (the property is structural: size and bytes now refer
  to one inode); the round-trip suites pin the arithmetic. Incidental:
  a missing dump file now surfaces as FileNotFoundException again
  (BG21's exception-type drift reverted).
- CS79: the exporter's 'Database export completed' message moves after
  the trailer flush, promote, and completion flag - a transcript can
  no longer claim completion ahead of the durability point.
- CN61 (doc): the runbook's step 1 and the design drafts now record
  the live-export consistency envelope - records are snapshotted at tx
  begin but schema/collections/blob-set/index sections are later live
  reads; an export under concurrent DDL is manifest-verifiable but not
  cross-section point-in-time consistent; quiesce DDL during migration
  exports. Pinned by new mandated-content fragments.
- CS80 (doc): the runbook's truncation row is scoped honestly (v15
  guarantee; lenient path carries no verification guarantee).
- CQ33 (record): the version-ungated CS63 latch widening (declared-
  legacy differing re-declaration now rejected vs silent last-wins) is
  recorded in the design-drafts rulings as an accepted fail-closed
  widening, per the SR3 precedent.

Deferred per the work order: BG31, CQ34, TQ33.
Log the applied cumulative findings (CN60 snapshot-copy accessors with
both red-first signatures; CS80 EOF bounds on all remaining reader
loops with the indexes-spin timeout red and the records-family
protected-by-accident justification; CN62 fstat-after-open; CS79
message ordering; CN61 consistency-envelope records; CQ33 latch-
widening ruling record), correct the Step-5 Surprises bullet's
internal-collection-exclusion inaccuracy flagged by both cumulative
baseline passes, and record the verification numbers incl. the full IT
re-run (first attempt was a surefire fork-start infra flake) and the
deferred set (BG31, CQ34, TQ33).
Gate findings from the cumulative-iteration verification:

- RG7: the CS80 truncation rejections thrown by the schema-family EOF
  bounds landed inside importSchema's pre-existing legacy-tolerance
  catch (Exception) swallow - and the legacy (< 15) path has no
  post-loop structural check - so a legacy dump truncated inside its
  schema section completed with exit 0 and only an ERROR log line,
  contradicting the track's fail-loudly promise. The truncation
  rejection is now a dedicated type (TruncatedDumpImportException,
  thrown by every bounded loop via the truncatedDump helper) and
  importSchema's catch rethrows it, making truncation loud on every
  path and version. R1 justification, recorded: no honest dump of any
  version is truncated (the dangling-name-guard precedent), so the
  rethrow costs no honest acceptance. Swallow audit: importSchema's
  catch was the only eater - importRecord's Throwable-catch rethrows
  the type (it only swallows DatabaseException, which the new type is
  not), importRecords and importDatabase wrap loudly, and the
  remaining catches (framing detection, close, type checks, reflection)
  enclose no bounded loop.
- RG6: SharedContext's genesis blob-registration comment still claimed
  getCollectionNames() returns a live view - false since the CN60
  copy-under-lock fix; synced (the defensive List.copyOf stays as
  belt-and-suspenders).
- Doc follow-through: with truncation loud everywhere, the runbook's
  truncation row drops last iteration's v15-only scoping and reads
  unconditionally: rejected loudly, on every path and for every
  declared version.
Log the gate verdict (all 8 findings VERIFIED, full-suite side effect
corroborating no honest-dump false-trips) and the RG6/RG7 fix addendum
with the red-first signature, the swallow audit, the R1 justification,
the unconditional runbook truncation row, and verification numbers.
…review

Log the RG6/RG7 micro-gate verdict (both VERIFIED, exhaustive
catch-site audit clean, no RG8), tick the step-implementation and
track-level-code-review progress boxes (track completion pends user
review), record the canonical deferred-items ledger in Outcomes &
Retrospective (cumulative BG31/CQ34/TQ33; Step-6 suggestion set;
Step-5 residuals incl. the justified-deferred CS64; the CN61
future-design note; the out-of-scope follow-up candidates), and add
the three track-level cumulative review reports (baseline, crash,
concurrency).
User-approved 2026-07-25. Ticks track completion and records the
closing status alongside ruling SR4: pin M.5 #12's rehearsal letter is
amended from DatabaseCompare-level to LOGICAL equivalence, because the
comparator is rid/collection-id-keyed while the import renumbers
collections and randomizes blob placement, making the original letter
structurally unsatisfiable (the historical DbImportExportTest using
that pattern is @disabled for the same reason). The pin is therefore
discharged as-built by the logical-equivalence rehearsal and no longer
reads as blocked; a rid-mapping-aware DatabaseCompare mode was not
commissioned and stays an optional out-of-scope follow-up in the
deferred-items ledger.
Post-implementation artifact: design-final.md, the as-built design that
supersedes the frozen planning-time design. It reconciles the original
document with what execution actually built: the tx-aware schema
snapshot, the genesis completion marker, storage-embedded blob
collections, engine files keyed by a persisted file-base id behind the
storage-format v24 gate, and the version-keyed strict import matrix.
Decision codes appear only in section footers and will be restated in
full by the companion adr.md; invariants are carried as prose.
Companion to design-final.md: restates decision records D1-D23 in full
(including the corrected as-built engine-file identity record and the
genesis-marker and blob-collection records added during execution), so
the design's section footers have a durable resolution target. Carries
the invariants as prose contracts, the accepted risks folded into their
owning decision records, the surviving limitations, and the adversarial
gate verdict trail that otherwise dies with the workflow scaffolding
and the PR-body notes at the ready-for-review flip.
Corrections from the post-commit fidelity reviews of design-final.md
and adr.md:

- Snapshot-tier consumer figure refreshed from the stale mid-execution
  174 to ~190 (re-counted at HEAD: 98 production + 91 test call sites),
  consistently across both artifacts.
- The changed-class marking described as its actual three channels
  (write choke point, explicit create/drop/rename marking, root-payload
  diff) instead of a single-site simplification.
- Serializer synchronization disambiguated to the schema write lock in
  both artifacts.
- adr.md gate-verdict trail gains the mid-execution adversarial
  re-review of the concurrency-control design (three rounds,
  2026-07-21, blockers resolved, re-approved) — recorded before the
  workflow archive deletion destroys the source.
- Operator migration runbook cited by bare filename in adr.md.
- design-final.md polish: genesis lazy-creator edge case no longer
  reads as contradicting its TL;DR, migration rehearsal cited by test
  class name, preposition pileup fixed, schema-carry shorthand
  introduced, all TL;DR blocks trimmed to the 5-line template cap.
The previous fix pass corrected design-final.md but propagated only
part of each correction to adr.md, leaving cross-artifact
contradictions in permanent records:

- D6 and the Component Map now state the changed-class marking as its
  three actual channels (write choke point on routed writes, explicit
  marking by whole-schema create/drop/rename, root-payload diff) —
  matching design-final.md and the code.
- D18 no longer claims the functions/sequences libraries stay on the
  legacy creation path: genesis creates their classes in its schema
  transaction; only the lazy create-if-absent seam stays legacy.
- The Constraints serializer bullet names the schema write lock, like
  every other serializer-contract site.
- The migration-rehearsal citation unified to the test class name in
  both records, matching the artifacts' own convention.
- The unintroduced schema-carry shorthand removed from adr.md prose.

Symmetric sweep also caught the reverse gap: design-final.md's
migration section now cites the operator runbook by bare filename,
as adr.md already did; plus one over-long prose line re-wrapped.
The reconciliation section's footer still glossed D6 with the
superseded single-channel phrasing; the body and the ADR record both
describe the changed-class signal as three marking channels.
Move design-final.md and adr.md from the working plan directory to
docs-internal/adr/transactional-schema/, matching the archive's
per-effort-subdirectory convention (adr.md + design-final.md per
entry, as in the sibling entries). The two artifacts reference each
other by bare filename, so no cross-reference changes are needed; the
operator migration runbook stays under docs/ as a user-facing product
document.
Delete the docs/adr/transactional-schema/_workflow/ archive (plan,
frozen design, track files, research log, adversarial passes, review
scaffolding, mutation log — 204 files). Its decision and mechanism
content was distilled into the two permanent artifacts now living at
docs-internal/adr/transactional-schema/ (design-final.md, adr.md),
including the adversarial gate-verdict trail folded into adr.md before
this deletion; the artifact-review substance was extracted into the
issue-draft archive outside the repo. The now-empty
docs/adr/ directory is removed with it.
CI intermittently killed the forked test JVM through the deadlock
watchdog: a schema-carrying commit holds the schema write lock and
blocks on the index-manager write lock (commitSchemaCarry's documented
four-lock order), while an index-manager exclusive region holds the
index-manager write lock and blocks on a fresh schema READ lock frames
down inside record deserialization - the immutable-snapshot rebuild
that runs whenever the shared snapshot cache was concurrently
invalidated. ReentrantReadWriteLock.lock() is non-interruptible, so
the cycle survived test timeouts and surfaced only as fork death
(~2.5% per run locally). The inversion family covers every non-commit
exclusive region of IndexManagerEmbedded (load, reload, the legacy
eager createIndex/dropIndex, membership add/remove, addIndexInternal),
all of which do entity work under the lock; the latent defect predates
this branch on develop, but the branch made the schema-write->index-
write side common to every schema-carrying commit.

Fix: IndexManagerEmbedded.acquireExclusiveLock now takes the committed
schema READ lock before the index-manager write lock and holds it for
the whole exclusive region (released in reverse order by
releaseExclusiveLock). Every call site routes through this single
pair, so the whole family conforms to the documented order at once.
Holding the read lock (rather than pinning a snapshot) is deliberate:
the legacy drop of a non-empty index batch-deletes entries on a COPIED
session (IndexAbstract.clearAllEntries), whose separate thread-local
snapshot state a pin on the calling session would not cover, while a
held read lock makes every nested schema-lock acquisition on the
thread - any depth, any session - reentrant and non-blocking, and
freezes the committed schema for the region so no staleness window
exists. The commit path's acquireExclusiveLockForCommit stays exempt:
it already holds the schema write lock, conforming to the same order.

Enforcement: SchemaShared now carries an always-on runtime lock-order
guard (wired to the index manager by SharedContext.init) that throws
IllegalStateException on a fresh schema-lock acquisition while the
thread holds the index-manager lock, and on a read-to-write upgrade
of the schema lock (which self-deadlocks on RRWL). Always-on rather
than assert-only for the same reason as the metadata-write mutex
engage-order guard: production JVMs run with -da, and the silent
alternative is a non-interruptible parked deadlock. The guard uses the
locks' own per-thread accounting (no extra bookkeeping, no allocation)
and measures within noise of a bare read-lock acquisition (<0.1 ns on
a ~5.3 ns operation). Deterministic regression tests exercise the
previously-inverted paths with the snapshot cache force-invalidated,
so a regression now fails loudly on first execution instead of hanging
one CI run in forty; the existing concurrent race test stays as the
stress complement. Post-fix stress: 40/40 clean runs.
@github-actions

Copy link
Copy Markdown

Test Count Gate Results

Tolerance: 5% drop allowed per module

Overall: ✅ 30200 tests (baseline: 29805, +395)

Module Baseline Current Change Status
core 19237 19632 +395
docker-tests 1891 1891 +0
embedded 1931 1931 +0
examples 6 6 +0
gremlin-annotations 30 30 +0
jmh-ldbc 39 39 +0
server 5524 5524 +0
tests 1147 1147 +0

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

Coverage Gate Results

Thresholds: 85% line, 70% branch

Line Coverage: ✅ 88.7% (2764/3116 lines)

File Coverage Uncovered Lines
core/src/main/java/com/jetbrains/youtrackdb/api/config/GlobalConfiguration.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/common/concur/lock/ScalableRWLock.java ✅ 100.0% (35/35) -
core/src/main/java/com/jetbrains/youtrackdb/internal/common/io/FileUtils.java ❌ 80.0% (12/15) 358, 361-362
core/src/main/java/com/jetbrains/youtrackdb/internal/core/config/IndexEngineData.java ✅ 100.0% (3/3) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/DatabasePoolImpl.java ✅ 100.0% (6/6) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/DatabaseSessionEmbedded.java ✅ 86.7% (104/120) 2263-2264, 2364, 3733, 3783, 3789-3790, 3799-3800, 3803-3805, 3918-3919, 3922-3923
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/DatabaseSessionEmbeddedPooled.java ✅ 100.0% (10/10) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/MetadataWriteMutex.java ✅ 85.7% (48/56) 170-172, 174-175, 215-216, 220
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/SharedContext.java ✅ 100.0% (24/24) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/YouTrackDBInternalEmbedded.java ❌ 62.5% (55/88) 479-480, 485-486, 540, 542-543, 561, 563-564, 829, 833-834, 888-889, 899-901, 904, 971, 1180, 1185-1186, 1188-1191, 1193, 1195-1199
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/record/EntityLinkSetImpl.java ✅ 92.9% (13/14) 345
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/DatabaseExport.java ❌ 83.9% (115/137) 154, 159-162, 164-168, 284-285, 292-293, 432-435, 444, 744, 791, 841
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/DatabaseImport.java ✅ 90.9% (210/231) 365, 535, 571, 699, 751, 859, 952-953, 1062-1064, 1088, 1092, 1095, 1213, 1234, 1247, 1418, 1512, 1977, 2066
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/SpillableRecordBuffer.java ✅ 85.7% (42/49) 108-109, 116-118, 120, 126
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/TruncatedDumpImportException.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/ValidatedGZIPInputStream.java ❌ 77.5% (79/102) 85, 127, 158, 166, 183, 194-197, 199, 202, 205, 210-214, 223, 237, 240-243
core/src/main/java/com/jetbrains/youtrackdb/internal/core/exception/GenesisIncompleteException.java ❌ 50.0% (2/4) 30-31
core/src/main/java/com/jetbrains/youtrackdb/internal/core/exception/StorageComponentException.java ✅ 100.0% (1/1) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/gql/executor/GqlExecutionPlanCache.java ✅ 100.0% (4/4) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/gremlin/YTDBGraphImplAbstract.java ❌ 75.0% (9/12) 144-146
core/src/main/java/com/jetbrains/youtrackdb/internal/core/id/RecordIdInternal.java ✅ 100.0% (1/1) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/CompositeIndexDefinition.java ✅ 100.0% (8/8) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/DefaultIndexFactory.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexAbstract.java ✅ 85.8% (109/127) 285-286, 288, 381, 387-388, 408, 412, 415-416, 484, 486, 488, 490, 497, 499, 501, 503
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexManagerAbstract.java ❌ 83.3% (5/6) 198
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexManagerEmbedded.java ✅ 94.8% (380/401) 332, 339, 367, 487, 507, 538, 577, 706, 725, 728-729, 1111, 1116, 1394, 1517, 1521-1522, 1524, 1573, 1675, 1824
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexMultiValues.java ✅ 100.0% (7/7) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexOneValue.java ✅ 85.7% (6/7) 384
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexOverlay.java ✅ 98.8% (84/85) 211
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/PropertyIndexDefinition.java ✅ 100.0% (3/3) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/SimpleKeyIndexDefinition.java ❌ 66.7% (2/3) 95
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/engine/v1/BTreeMultiValueIndexEngine.java ✅ 100.0% (9/9) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/engine/v1/BTreeSingleValueIndexEngine.java ✅ 100.0% (6/6) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/iterator/RecordIteratorCollection.java ✅ 100.0% (8/8) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/iterator/RecordIteratorUtil.java ❌ 75.0% (3/4) 37
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/MetadataDefault.java ✅ 100.0% (6/6) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaClassEmbedded.java ✅ 91.3% (21/23) 337, 611
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaClassImpl.java ✅ 91.2% (31/34) 647, 651, 656
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaClassProxy.java ✅ 90.1% (100/111) 77, 84, 118, 173, 179, 185, 192, 200, 213, 225, 368
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaEmbedded.java ✅ 91.3% (42/46) 221, 369, 514, 633
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaPropertyProxy.java ✅ 87.0% (60/69) 41-42, 47-48, 53-54, 84, 185, 263
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaProxedResource.java ✅ 86.4% (38/44) 218-219, 224-225, 230-231
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaProxy.java ✅ 93.0% (66/71) 111, 158, 161-162, 165
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaShared.java ✅ 94.5% (206/218) 705, 709, 714, 962, 1034-1035, 1043, 1045-1046, 1050, 1380, 1421
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/TxSchemaState.java ✅ 100.0% (39/39) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/security/SecurityShared.java ❌ 66.7% (12/18) 609, 611, 655, 657-659
core/src/main/java/com/jetbrains/youtrackdb/internal/core/record/impl/EntityImpl.java ✅ 100.0% (1/1) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/security/symmetrickey/SymmetricKeySecurity.java ❌ 0.0% (0/3) 144-145, 149
core/src/main/java/com/jetbrains/youtrackdb/internal/core/serialization/serializer/JSONReader.java ✅ 92.9% (13/14) 100
core/src/main/java/com/jetbrains/youtrackdb/internal/core/sql/executor/FetchFromClassExecutionStep.java ✅ 100.0% (7/7) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/sql/executor/SelectExecutionPlanner.java ✅ 100.0% (3/3) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/config/CollectionBasedStorageConfiguration.java ✅ 93.3% (42/45) 1377, 1379, 1453
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/disk/DiskStorage.java ✅ 100.0% (4/4) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/impl/local/AbstractStorage.java ✅ 85.0% (579/681) 1091, 1192, 2048, 2076, 2619-2620, 2623-2625, 2634-2637, 2703-2704, 2725, 2728-2731, 2794, 3022, 3047-3050, 3054-3056, 3067, 3108, 3192, 3211, 3218-3219, 3266-3268, 3274-3275, 3278, 3442, 3446, 3560, 3584, 3605-3607, 3611, 3624-3625, 3629, 3686, 3704-3709, 3714-3715, 3768, 3772, 3775-3776, 3782, 3899, 3947-3948, 3951-3952, 3958, 3970, 3981-3982, 3984, 3987-3989, 4065-4068, 4099, 4451-4452, 4619, 5034-5035, 5170, 5766, 6625, 6634, 6645, 7070, 7106, 8413-8418
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/impl/local/paginated/atomicoperations/AtomicOperationsManager.java ✅ 100.0% (6/6) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/impl/local/paginated/atomicoperations/operationsfreezer/FreezeKind.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/impl/local/paginated/atomicoperations/operationsfreezer/OperationsFreezer.java ✅ 97.6% (41/42) 151
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/impl/local/paginated/base/StorageComponent.java ✅ 100.0% (4/4) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/index/sbtree/singlevalue/CellBTreeSingleValue.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/index/sbtree/singlevalue/v3/BTree.java ✅ 100.0% (5/5) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/ridbag/LinkCollectionsBTreeManagerShared.java ✅ 85.7% (6/7) 130
core/src/main/java/com/jetbrains/youtrackdb/internal/core/tx/FrontendTransactionImpl.java ✅ 100.0% (19/19) -

Branch Coverage: ✅ 81.2% (1300/1601 branches)

File Coverage Lines with Uncovered Branches
core/src/main/java/com/jetbrains/youtrackdb/internal/common/concur/lock/ScalableRWLock.java ✅ 90.0% (18/20) 712, 741
core/src/main/java/com/jetbrains/youtrackdb/internal/common/io/FileUtils.java ❌ 50.0% (2/4) 354, 367
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/DatabaseSessionEmbedded.java ✅ 85.4% (41/48) 2256, 3729, 3782, 3798, 3837, 3915, 3968
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/DatabaseSessionEmbeddedPooled.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/MetadataWriteMutex.java ✅ 82.1% (23/28) 152, 158, 198-199, 211
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/SharedContext.java ✅ 100.0% (6/6) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/YouTrackDBInternalEmbedded.java ✅ 81.8% (18/22) 852, 970, 1186
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/record/EntityLinkSetImpl.java ❌ 62.5% (15/24) 89, 98, 342, 344-345
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/DatabaseExport.java ❌ 68.4% (26/38) 130, 133, 281, 289, 379, 439, 743, 790, 856-857
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/DatabaseImport.java ✅ 79.3% (172/217) 362, 534, 543, 563, 643, 679, 683-684, 688, 697-698, 826, 852, 858, 925, 936, 943, 959, 1062-1063, 1085, 1094, 1170, 1197, 1212, 1226, 1233, 1246, 1411, 1417, 1438, 1511, 1892, 1915, 1976, 2061, 2065
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/SpillableRecordBuffer.java ✅ 85.0% (17/20) 117, 125
core/src/main/java/com/jetbrains/youtrackdb/internal/core/db/tool/ValidatedGZIPInputStream.java ❌ 66.7% (32/48) 84, 124, 157, 182, 193, 196, 201, 204, 209, 213, 222, 225, 242
core/src/main/java/com/jetbrains/youtrackdb/internal/core/gql/executor/GqlExecutionPlanCache.java ✅ 100.0% (4/4) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/gremlin/YTDBGraphImplAbstract.java ❌ 61.1% (11/18) 139, 145-146
core/src/main/java/com/jetbrains/youtrackdb/internal/core/id/RecordIdInternal.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/CompositeIndexDefinition.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexAbstract.java ❌ 63.9% (23/36) 241, 280, 353-354, 407, 479, 518, 652, 658, 909, 914, 1134
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexManagerEmbedded.java ✅ 85.0% (221/260) 123, 322, 326, 329, 336, 354, 360, 452, 484, 486, 490, 506, 535, 564, 576, 580, 701, 910, 915, 1110, 1115, 1148, 1370, 1391, 1461, 1501, 1568, 1572, 1603, 1647, 1650, 1672, 1823
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexMultiValues.java ✅ 100.0% (4/4) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexOneValue.java ✅ 75.0% (3/4) 382
core/src/main/java/com/jetbrains/youtrackdb/internal/core/index/IndexOverlay.java ✅ 94.4% (51/54) 210, 286-287
core/src/main/java/com/jetbrains/youtrackdb/internal/core/iterator/RecordIteratorCollection.java ✅ 100.0% (6/6) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/iterator/RecordIteratorUtil.java ✅ 75.0% (3/4) 33
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/MetadataDefault.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaClassEmbedded.java ✅ 75.0% (15/20) 53, 334, 336, 606, 610
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaClassImpl.java ✅ 78.6% (22/28) 646, 650, 655, 1685, 1690, 1699
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaClassProxy.java ✅ 100.0% (12/12) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaEmbedded.java ✅ 80.0% (40/50) 211, 220, 364, 368, 426, 474, 513, 633, 659
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaPropertyProxy.java ✅ 71.4% (10/14) 36, 40, 46, 52
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaProxedResource.java ✅ 84.6% (22/26) 214, 217, 223, 229
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaProxy.java ✅ 80.0% (8/10) 201, 237
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/SchemaShared.java ✅ 90.5% (134/148) 704, 708, 713, 948, 961, 965, 1033, 1377, 1379, 1418, 1432, 1673
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/schema/TxSchemaState.java ✅ 100.0% (6/6) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/metadata/security/SecurityShared.java ✅ 75.0% (3/4) 654
core/src/main/java/com/jetbrains/youtrackdb/internal/core/serialization/serializer/JSONReader.java ✅ 75.0% (3/4) 99
core/src/main/java/com/jetbrains/youtrackdb/internal/core/sql/executor/FetchFromClassExecutionStep.java ✅ 80.0% (8/10) 118, 219
core/src/main/java/com/jetbrains/youtrackdb/internal/core/sql/executor/SelectExecutionPlanner.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/config/CollectionBasedStorageConfiguration.java ✅ 75.0% (9/12) 327, 329, 1374
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/impl/local/AbstractStorage.java ✅ 77.3% (255/330) 1090, 1096, 1152, 1163, 1191, 1626, 1632, 2045-2047, 2073-2075, 2350, 2365, 2618, 2702, 2716, 2723, 2729, 2790, 3017, 3026, 3041, 3047, 3054, 3062, 3133, 3187, 3191, 3390-3391, 3559, 3562-3563, 3583, 3586, 3685, 3697, 3767, 3771, 3774, 3781, 3895, 3898, 3941, 3956-3957, 3965, 3987, 4059, 4098, 4582, 4618, 5033, 5138, 5169, 5761, 5765, 5936, 7069, 7105, 7946
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/impl/local/paginated/atomicoperations/operationsfreezer/OperationsFreezer.java ✅ 96.9% (31/32) 150
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/impl/local/paginated/base/StorageComponent.java ✅ 100.0% (2/2) -
core/src/main/java/com/jetbrains/youtrackdb/internal/core/storage/ridbag/LinkCollectionsBTreeManagerShared.java ❌ 50.0% (1/2) 129
core/src/main/java/com/jetbrains/youtrackdb/internal/core/tx/FrontendTransactionImpl.java ✅ 81.2% (13/16) 191, 193, 1046

Two findings from the baseline review of the ABBA lock-inversion fix
(deadlock-fix-baseline-iter1: TQ34-1 should-fix, CQ35-2 suggestion).

TQ34-1: the guard tests had no @test(timeout). If the read-to-write
upgrade guard ever regresses, schemaReadToWriteUpgradeThrowsInsteadOf-
SelfDeadlocking parks forever on the non-interruptible RRWL write
acquisition - the test would BECOME the silent forked-JVM death this
fix exists to eliminate, leaving no surefire failure record and making
the regression invisible. Both guard tests now carry timeout=60_000
(matching the sibling stress test, ~80x observed runtime, so it cannot
flake under CI load) plus the watchdog-thread session re-activation a
timed body requires. Validated by neutering the upgrade guard: the
test now reports a named TestTimedOutException at 60s instead of fork
death. The other three deterministic tests deliberately stay without
timeouts: they run single-threaded with no contending thread, so no
lock acquisition in them can block - their regressions surface as the
guard's IllegalStateException, never as a park.

CQ35-2: releaseExclusiveLock re-resolved the schema instance through
session.getSharedContext() instead of reusing the instance the acquire
read-locked. Unreachable today (SharedContext.reInit has no callers),
but it was the one structural fragility with a real leak shape: a
re-init between acquire and release would unlock a DIFFERENT
SchemaShared and leak the original read hold, parking every later
schema write forever. The outermost acquireExclusiveLock now captures
the locked instance in a field guarded by the index-manager write
lock; nested acquires and every release reuse the capture, so the
region locks and unlocks one and the same instance end to end. A
non-commit entry under the commit-path acquisition (which captures no
schema) now fails with a targeted IllegalStateException instead of the
generic guard message. Lock acquisition order, release order, and the
exception-path release balance are unchanged on every reachable path.
The post-flip ABBA deadlock fix changed the concurrency contract in
ways the permanent record must carry:

- New decision record D24: the always-on runtime lock-order guard on
  the schema lock (probe wired at shared-context init), the ordered
  index-manager exclusive acquire (schema read lock first, held across
  the region), the exempt commit-path acquire, and why always-on beat
  assert-only. Cited from design-final.md's lock-order section.
- The documented four-lock order is stated honestly as incomplete:
  the per-index lock inside IndexAbstract sits outside it (rebuild,
  fill, and standalone delete take it above the ordered tiers without
  the index-manager lock held) — a pre-existing latent inversion
  family the guard does not see, tracked in the issue tracker. The
  storage-state tier remains runtime-unassertable.
- The stall-envelope characterization gains the genuinely new edge:
  index-manager exclusive regions now hold the schema read lock across
  potentially unbounded region I/O, stalling non-commit schema writers
  even without an overlapping commit.

Both artifacts amended in the same pass and cross-grepped so neither
carries a claim the sibling contradicts.
Merged via the queue into develop with commit d02fa77 Jul 28, 2026
26 checks passed
@andrii0lomakin
Andrii Lomakin (andrii0lomakin) deleted the transactional-schema branch July 28, 2026 17:23
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