Skip to content

Preserve 5.2 rollback compatibility for first-time table creates - #3118

Merged
kriszyp merged 5 commits into
mainfrom
fix/first-create-downgrade-compatibility
Oct 8, 2026
Merged

kriszyp merged 5 commits into
mainfrom
fix/first-create-downgrade-compatibility

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

A confirmed 5.3 → 5.2 rollback opens tables first created on 5.3 as empty because 5.2.15 does not read generation-suffixed store names. Writes during rollback then disappear on re-upgrade. Closes #3102.

❓ Your call: Implement option 2 as requested: restore compatibility for future first-time creates. Existing stamped tables and recreates remain unreadable by 5.2.15; patching the old reader or migrating those stores is separate work.

💡 Solution

First-time RocksDB creates use legacy primary/index names. A create with local name history uses a fresh generation. Crash recovery keeps a separate journal identity, and local-only drops leave untimed name history without advertising a replicated drop.

⚖️ Alternatives

Planning review: Framing-Verdict: chosen-approach-sound. Adopted its compatibility and lifecycle amendments: bare creating names live in creatingStores, which shipped 5.3 recovery ignores; untimed local-drop markers preserve naming history; retirement checks its tombstone before touching stores.

Patching 5.2 requires users to install a newer rollback target. Synchronous commit draining reverses #2604's nonblocking drop behavior. Conditional naming without create journaling leaves crash orphans unreclaimable. The existing journal supplies recovery without a second mechanism.

❓ Your call: Review depth is four full passes, the test-only delta and the final cleanup repair delta. The full passes cost roughly 25–30 minutes each; the diff grew from about 342 to 518 lines. Accepted changes protect legacy ownership, retire stale stores and coordinate overlapping journals. The closing pass corrected the causal interpretation of the baseline fixture, so the newly reachable cleanup defect is fixed here. The repair review confirms both journal orderings, and its conditional concurrent-create concern is refuted by the shared native catalog lock. Preserve the stated crash boundary and existing safeguards.

❓ Your call: Retain the late tombstone re-read and the field-compatibility comments: the former is required by the existing reentrant drop regression, and the latter explains shipped readers' destructive interpretation. A hypothetical live owner of the stale dropper's primary cannot arise under the preserved name-history rule. CI version provisioning stays in #3104, whose actual diff updates both the environment list and the older consumer.

🔧 Changes

Product and architecture tour

Store names and compatibility

Table state Physical names after this change Readable by 5.2.15
First local create, no name history T/, T/str Yes
Recreate, including after local-only cleanup T/@uuid, T/str@uuid Unsupported
Existing stamped 5.3.0/5.3.1 table Its existing names No
Existing legacy table Its existing names Yes

The catalog lock protects the name-history decision and independent create journal. The native database lock spans the under-lock existing-primary recheck, store selection, publication and failed-create cleanup; another worker must reload the published table or wait for cleanup. Incoming primary-attribute generations are replaced with the local choice. Peer drop history can require stamping on a node that never held the table. Look hardest at publication ordering and the creatingStores field: shipped 5.3 recovery interprets the old stores field destructively even when a bare primary was published.

The publication flag follows the primary write immediately, so failure to remove the journal cannot roll back a live table. Failure cleanup drops the recorded physical names and retains the journal when a physical drop fails. Recovery matches a published create by its primary name, including a dropping primary whose retirement belongs to its dropper. Unpublished creates are reclaimed by recorded names; old suffix-only journals remain supported.

Drops and durable name history

An untimed /dropped/T marker records local name history without advertising a replicated drop; timed drop readers continue skipping it. The tombstone identity guard precedes store capture and retirement, with the existing post-drop guard retained for reentrant callbacks. A stale drop retires only its own primary/index handles whose names the live catalog does not own, waits for admitted writes and sweeps its incarnation's blobs. It cannot journal or drop replacement-only indexes.

The existing lifecycle note records the naming, journal and rollback constraints; the design index points to that amended note.

Preserve asynchronous interrupted-drop recovery: rolling back while a bare predecessor still awaits reclamation can expose its rows on 5.2. Closing that crash window would require synchronous recovery before readiness and reverse #2604's lifecycle. Ordinary completed drops retire their own stores before success.

Recovery ownership and proof

The live catalog wins when a 5.2 writer reuses a bare name while ignoring an old journal. The active dropper retains its primary handle for the final blob sweep, even when another handle already dropped the family. Reclamation still removes unowned predecessor indexes and rechecks ownership before a deferred blob sweep starts and when it finishes under the catalog lock. The legacy-writer blob regression proves the live blob survives and an unowned index is removed. The additional current-version restart proves rollback-created rows survive reopening after reclamation; the same test checks rows and secondary indexes through a real 5.2.15 round trip. The two journal-order crash cases require blob unlinking before retirement completes.

A creating journal leaves a primary named by a pending retired journal to that journal's blob sweep. The retired journal clears after blob unlinking; the regression covers both row orderings.

Creation and publication order

databases.ts:4388

exclusiveLock()
journalId = randomUUID()
generation = hasPriorStores ? journalId : undefined
createJournal = {
  table: tableName,
  generation: journalId,
  phase: 'creating',
  primaryStore: storeNameFor(dbiName, generation),
  creatingStores: [storeNameFor(dbiName, generation), …]
}
attributesDbi.putSync(generationRowKey(journalId), createJournal)
… // open primary/index stores and write secondary catalog rows
attributesDbi.put(tableName + '/', deferredPrimaryRow)
published = true
attributesDbi.remove(generationRowKey(journalId))

Reclamation handoff

databases.ts:5672

currentRows = … // refresh journals under the catalog lock
retiredPrimaries = new Set(…)
for ({ key, value } of currentRows) {
  …
  owned = liveStoreNamesFor(attributesDbi, value.table)
  if (value.phase === 'retired' && columns.includes(value.primaryStore) && !owned.has(value.primaryStore)) {
    scheduleGenerationBlobSweep(rootStore, attributesDbi, databaseName, key, value)
    continue
  }
  for (columnName of columns) {
    if (value.phase === 'creating' && retiredPrimaries.has(columnName)) continue
    if (!owned.has(columnName) && (retired.has(columnName) || columnName.endsWith(suffix)))
      dropColumnFamily(rootStore, columnName)
  }
  … // clear this row after drops settle; retired rows own deferred primaries
}

✅ Verification

End-to-end route: integrationTests/upgrade/first-create-downgrade.test.ts uses a real 5.2.15 install and five boots (5.2 → current → confirmed 5.2 rollback → current → current restart). It checks primary rows, ordinary secondary-index searches and rollback writes in both an existing database and a newly created database. A stamped sibling recreate with a nonempty predecessor verifies the stated compatibility boundary. A table dropped immediately before rollback is recreated on 5.2, and its replacement survives re-upgrade despite the old retirement journal. It uses HARPER_PREVIOUS_MINOR_PATH, including #3104's list form (that PR also updates the existing minor-upgrade suite to parse the list); no configured 5.2 install means a clean skip. CI provisioning of that install is supplied by #3104, not duplicated here.

  • Fresh build and focused create/drop/catalog/lifecycle tests: RocksDB 59 passed, 3 skips; LMDB 40 passed, 20 skips. The two overlapping-journal cases fail before the repair and pass afterwards. Full resource suites on preceding HEAD 3567715: RocksDB 4,069 passed, 54 skips; LMDB 3,074 passed, 697 skips. The final RocksDB resource rerun passes 4,071 tests with 54 skips on a4bbe27; this repair is confined to RocksDB reclamation.
  • New integration test: 1 passed, 0 failures, 0 TODOs against 5.2.15. The original first-create probe fails on isolated base f038807 with actual [] versus rows a,b. The live-retirement blob probe also fails before its fix because a live blob is unlinked.
  • The schema-listener retirement test also fails on the base when the old drop journals replacement-only attributes.
  • Core unit suite: 6,786 passed, 207 pending, one failure in gitCredentials.test.js:395. The same install-spawn assertion fails on built clean base f038807 because Dispatch supplies inherited GIT_CONFIG_* keys. The credential environment was preserved. Full integration on preceding HEAD 3567715: 2,334 passed, zero assertion failures, 18 skipped, one TODO; exit 1 because the Ollama before-hook cancels six tests with ERR_IMPORT_ATTRIBUTE_MISSING on systemSchema.json. The same JSON-import error reproduces with untouched base source on Node 26.2.0; the Ollama backend, test and JSON import sites are unchanged. The five-boot rollback regression and existing minor-upgrade/restart suites pass inside that full run; the five-boot regression also passes again on the repair head.
  • Repository formatting, lint and design-note checks pass (scratch artifacts excluded).
  • Optional integration typecheck fails with the same 242 diagnostic lines on a freshly built, clean f038807 base using identical dependencies; this file adds none.

Release qualification: main is mergeable, but the automatic v5.3 cherry-pick reports conflicts on its generated branch, so release-line integration tests have not started. The executed local checks above cover this main-targeted PR; the backport still needs conflict resolution.

No companion documentation PR: this restores the existing rollback behavior for ordinary first creates and adds no public API/configuration. The existing stamped-table and recreate limits are explicit above.

🤖 Generated by OpenAI Codex; posted via @kriszyp.

Related PRs: #2155 independent (expiration sweeps), #2955 independent (cross-worker drop enforcement), #3084 independent (replication authorization), #3104 independent (CI version fixtures), #3114 independent (v5.3 backport of those fixtures), #3100 independent (subscription resume), #2604 overlaps (narrows create naming while retaining deferred reclamation), #2431 independent (TLS/deploy anchors), #1916 independent (eviction/removal anchors)
Complexity: complicated

Review-Coverage: authored=codex; ran=cursor-composer,claude,gemini; adjudicated=domain; declined=cursor-grok,cursor-kimi,cursor-muse; rounds=6; full=4 @ a4bbe27

Review-Attention: deep ~30m (critical: Table.ts, databases.ts; decisions: rollback-boundary, physical-naming, do-less-alternative) @ a4bbe27

kriszyp and others added 5 commits October 8, 2026 07:36
Keep legacy names until a table name has local history, journal unstamped creates independently, and preserve drop ownership during recovery.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>
Retirement journals can outlive a rollback that reuses bare store names. Preserve stores owned by the live catalog and their blobs, and verify the 5.2 recreate/re-upgrade path.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>
Guard retirement of the dropper’s own physical handles against live catalog ownership, then preserve write settlement and blob cleanup. Document untimed name history and older-reader crash recovery limits.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>
Reopen the storage engine before the final rollback-recreate and index assertions, so retained handles cannot mask an erroneously reclaimed family.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>
Snapshot journal phases under the catalog lock and defer creating-row primary retirement to any pending retired journal. Verify both journal orderings unlink blobs before retirement completes.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>
@kriszyp kriszyp added this to the v5.3 milestone Oct 8, 2026
@kriszyp kriszyp self-assigned this Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Release cherry-pick v5.3: conflict

Cherry-pick onto v5.3 produced conflicts on commit(s): 4db030f2a0b8f4d064dfdf60446b4181ec597a40 b8fca4ec8a97f5549f05a2d0a4596bb775fb1f49 5f8f49e86fe741acfda7ae6ae4112b7f34b67c8e

The conflict markers are committed on branch cherry-pick/v5.3/pr-3118.
A pull request has been opened to land this patch: #3124

@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 updates the table creation and drop lifecycle to support rollbacks to version 5.2, ensuring first-time creates on RocksDB use bare names when no prior drop history exists. It also refactors generation reclamation, adds a downgrade integration test, and updates unit tests. The review feedback suggests avoiding RocksDB-specific cleanup on LMDB in Table.ts while optimizing column array access, adding the missing table property to the GenerationRow type definition, and wrapping the test path resolution in a try/catch block to prevent potential test suite crashes.

Comment thread resources/Table.ts
Comment on lines +3264 to +3273
if (!removed) {
withUpdateAttributesLock(rootStore, `retire stale stores of '${databaseName}.${tableName}'`, () => {
const owned = liveStoreNamesFor(dbisDb, tableName);
const columns = new Set<string>((rootStore as any).columns);
for (const store of [primaryStore, ...Object.values(indices)]) {
if (store.name && columns.has(store.name) && !owned.has(store.name))
dropColumnFamily(rootStore, store.name);
}
});
} else await dbisDb.committed;

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.

high

On LMDB storage engines, rootStore does not have a columns property, which means (rootStore as any).columns will be undefined. Passing undefined to new Set() is technically safe, but attempting to run this RocksDB-specific cleanup block on LMDB is unnecessary and could lead to unexpected behavior. Additionally, using optional chaining store?.name is safer in case any index store is not fully initialized or is nullish. Also, avoid copying the columns array (such as by creating a Set) because the columns getter in rocksdb-js already materializes a fresh array on each access; we can use .includes() directly to avoid redundant allocations.

					if (!removed) {
						withUpdateAttributesLock(rootStore, `retire stale stores of '${databaseName}.${tableName}'`, () => {
							const columnsArray = (rootStore as any).columns;
							if (!columnsArray) return;
							const owned = liveStoreNamesFor(dbisDb, tableName);
							for (const store of [primaryStore, ...Object.values(indices)]) {
								if (store?.name && columnsArray.includes(store.name) && !owned.has(store.name))
									dropColumnFamily(rootStore, store.name);
							}
						});
					} else await dbisDb.committed;
References
  1. Do not use Array.from() or copy arrays returned by getters that already materialize a fresh array on each access (such as columns in rocksdb-js), as the loop already iterates over a snapshot and copying it would be redundant.

Comment thread resources/databases.ts
generation: string;
phase: 'creating' | 'retired';
stores?: string[];
creatingStores?: string[];

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.

medium

The table property is used extensively on GenerationRow objects (e.g., value.table in reclaimGenerations and row.table in scheduleGenerationBlobSweep), but it is missing from the GenerationRow type definition. Adding it as an optional property ensures TypeScript type safety and completeness.

Suggested change
creatingStores?: string[];
creatingStores?: string[];
table?: string;

Comment on lines +17 to +20
const previousPath = process.env.HARPER_PREVIOUS_MINOR_PATH?.split(delimiter).find((path) => {
const { version } = JSON.parse(readFileSync(join(path, 'package.json'), 'utf8'));
return version.startsWith('5.2.');
});

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.

medium

If HARPER_PREVIOUS_MINOR_PATH contains paths that do not have a package.json file or contain invalid JSON, readFileSync or JSON.parse will throw an error, causing the entire test suite to crash during module loading. Wrapping this in a try/catch block ensures that any such errors are caught and the path is safely skipped.

const previousPath = process.env.HARPER_PREVIOUS_MINOR_PATH?.split(delimiter).find((path) => {
	try {
		const { version } = JSON.parse(readFileSync(join(path, 'package.json'), 'utf8'));
		return version.startsWith('5.2.');
	} catch {
		return false;
	}
});

@kriszyp
kriszyp marked this pull request as ready for review October 8, 2026 19:05
@kriszyp
kriszyp merged commit 4dba9f5 into main Oct 8, 2026
62 of 63 checks passed
@kriszyp
kriszyp deleted the fix/first-create-downgrade-compatibility branch October 8, 2026 19:05
kriszyp added a commit that referenced this pull request Oct 8, 2026
…n how they compose

After rebasing over #3118: a node-local drop's untimed name history is read as no drop by every drop-time reader, a refused peer create leaves no create journal or store, and a kept peer drop journals nothing for reclamation. retireRocksStores now also resolves false when another thread completed the tombstone; this drop still retired its generation, so dropTable resolves true either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMpTV1zehf49BFieVUy7d1
Dispatch-Task: harper-3119-rebase-main-3118
kriszyp added a commit that referenced this pull request Oct 8, 2026
The conflict hunks pulled in main's #2962 lifecycle machinery (timed drop
markers, createdTime stamps, replication helpers), which is milestoned
v5.4 and absent here; it is dropped. #3118 keys "this name has history"
off the /dropped/<table> row that #2962 writes at every drop completion,
so v5.3 gets only the untimed form #3118 itself introduced: both RocksDB
completion paths (retireRocksStores, completeInterruptedDrop) record it
before removing the tombstone, and the load parser skips /dropped/ rows.
5.4 reads the same row as its untimed marker. The design notes keep
v5.3's section heading and drop the lifecycle-stamp paragraph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYAaU3uVxkbjHe19UNWRfK
Dispatch-Task: cherry-resolve-kriszyp_harper_3118-6e6e74cf
kriszyp added a commit that referenced this pull request Oct 8, 2026
… creates (conflicts → v5.3) (#3124)

* Keep first-time table stores readable after a minor rollback

Keep legacy names until a table name has local history, journal unstamped creates independently, and preserve drop ownership during recovery.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>

* Protect legacy replacement stores during generation reclamation

Retirement journals can outlive a rollback that reuses bare store names. Preserve stores owned by the live catalog and their blobs, and verify the 5.2 recreate/re-upgrade path.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>

* Retire stale drop stores before completing a rollback-visible drop

Guard retirement of the dropper’s own physical handles against live catalog ownership, then preserve write settlement and blob cleanup. Document untimed name history and older-reader crash recovery limits.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>

* Verify rollback data survives a further current-version restart

Reopen the storage engine before the final rollback-recreate and index assertions, so retained handles cannot mask an erroneously reclaimed family.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>

* Keep overlapping create recovery behind retired-primary blob cleanup

Snapshot journal phases under the catalog lock and defer creating-row primary retirement to any pending retired journal. Verify both journal orderings unlink blobs before retirement completes.

Co-Authored-By: OpenAI Codex GPT-6.1 <noreply@openai.com>

* Resolve the v5.3 cherry-pick of #3118 without #2962's drop markers

The conflict hunks pulled in main's #2962 lifecycle machinery (timed drop
markers, createdTime stamps, replication helpers), which is milestoned
v5.4 and absent here; it is dropped. #3118 keys "this name has history"
off the /dropped/<table> row that #2962 writes at every drop completion,
so v5.3 gets only the untimed form #3118 itself introduced: both RocksDB
completion paths (retireRocksStores, completeInterruptedDrop) record it
before removing the tombstone, and the load parser skips /dropped/ rows.
5.4 reads the same row as its untimed marker. The design notes keep
v5.3's section heading and drop the lifecycle-stamp paragraph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYAaU3uVxkbjHe19UNWRfK
Dispatch-Task: cherry-resolve-kriszyp_harper_3118-6e6e74cf

* Drop v5.3-ignored localOnly drop arguments; state the name-history row contract

v5.3's dropTable() takes no options, so the picked tests' localOnly
argument only repeated the ordinary drop; the test now names what it
checks. The recordTableNameHistory comment states the row-shape
constraint instead of issue history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYAaU3uVxkbjHe19UNWRfK
Dispatch-Task: cherry-resolve-kriszyp_harper_3118-6e6e74cf

---------

Co-authored-by: Kris Zyp <kriszyp@gmail.com>
Co-authored-by: OpenAI Codex GPT-6.1 <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
kriszyp added a commit that referenced this pull request Oct 9, 2026
…l tables when a peer drops one (#3119)

* Enforce the table-generation rule in the catalog-locked create and drop

A create that carries a peer's generation is refused under the catalog lock when a
drop marker here retires it, and an unstamped one is kept at 0 instead of "now". A
peer's drop never retires a replicate:false table, and with a time only a
generation created before it, re-checked against the locked catalog row; the
marker is recorded either way. Peer provenance comes from the operation context,
never the JSON body. A node-local table's drop leaves no marker and is not
forwarded, and dropTableMeta keeps a live or still-dropping primary row.

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Refuse a stale stamped peer definition of a live table, and read replicate from the catalog first

A stamped peer generation a drop retired could still merge its attributes into
the live table through the existing-table branch; it is refused there too. A
persisted replicate flag on the catalog row wins over this thread's class, which
alone carries only runtime exclusions. Adds a test-only switch for a node that
simulates a build before the stamps.

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Cover a forwarded drop's provenance through server.operation, and drop a narrating comment

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Bound when an unstamped table was created, at the first load on a build that stamps

A table with no createdTime was created by a build that stored none, so before
the first load on a build that stamps; that load writes the bound as
createdBefore on its primary row. A drop recorded after the bound retires the
table outright, so only an older drop leaves the replication layer's upgrade
heuristic anything to decide. A table created during a rollback to such a build
gets its own bound at the next load.

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Judge a stamped peer definition of a live table under the catalog lock

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Announce a recorded drop marker on every thread, with a per-thread drop epoch

Each worker owns its own replication connections, so a marker learned on one
must reach the others: to re-announce it to their peers, and to rejudge a peer
generation their connections accepted before the marker existed.

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Never fail a database load over the createdBefore bound, and keep it on the loaded row

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Contain a replayed drop announcement's listener errors, and keep a concurrent bound and system-table exclusions

A drop announcement queued before its handler registered is replayed outside
manageThreads' catch, so the handler contains listener errors itself. A load that
finds another thread already bounded an unstamped table keeps that bound on its
snapshot, which its tableId repair may write back. A runtime-excluded system table
stays node-local whatever its catalog row says.

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Advance the recording thread's drop epoch at commit, before the broadcast reaches the others

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Judge a peer's generation against a drop still completing, not only a recorded marker

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Re-dispatch a peer drop that reached a stale class, and never fail a completed drop over its metadata cleanup

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Announce a dropped table even when its metadata cleanup times out; comment fixes

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Mark an LMDB drop in progress, as the RocksDB drop already is

A schema reload on the dropping thread (one the drop's own schema signal can
trigger) found the LMDB drop's tombstone and completed it mid-drop, so the drop
then threw "a replacement table became current". The reload already skips a
drop generation marked in progress; only the RocksDB path marked it.

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Mark an LMDB drop in progress as soon as its tombstone commits, and announce a drop whose cleanup cannot open its root

Refs #1212

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT
Dispatch-Task: hp-956-core-enforced-lifecycle

* Release an LMDB drop's in-progress mark when table cleanup throws

A cleanup failure after the tombstone left the mark held for the worker's
life, so every later load on that thread skipped the tombstone instead of
completing the drop.

Dispatch-Task: hp-956-core-enforced-lifecycle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCffnMMpweL8Q9EceYqBbT

* Describe the table lifecycle and 5.2 store naming as one rule, and pin how they compose

After rebasing over #3118: a node-local drop's untimed name history is read as no drop by every drop-time reader, a refused peer create leaves no create journal or store, and a kept peer drop journals nothing for reclamation. retireRocksStores now also resolves false when another thread completed the tombstone; this drop still retired its generation, so dropTable resolves true either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMpTV1zehf49BFieVUy7d1
Dispatch-Task: harper-3119-rebase-main-3118

* Assert the announced marker carries the peer's exact drop time, and finish three comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMpTV1zehf49BFieVUy7d1
Dispatch-Task: harper-3119-rebase-main-3118

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

Rolling back from 5.3 to 5.2 hides every table created on 5.3, and rows written during the rollback vanish on re-upgrade

1 participant