feat(8.0): #18 coordinated migration LOCK — block-and-queue the 7.x→8.0 auto-upgrade

Replaces rc.8's no-freeze deference with an automatic, observable, coordinated
migration LOCK (David's reversal: "unknown/halfway states are more dangerous
than blocking"). While a native provider runs its one-time 7.x→8.0
rebuild-from-canonical (`isMigrating()===true`), brainy blocks/queues data-plane
reads AND writes so no operation ever touches a half-built index — closing the
three seam-map gaps (lost mid-rebuild writes, degraded reads, read-time rebuild).

Mechanism (one choke point):
- awaitMigrationLock() at ensureInitialized() covers all ~40 data-plane methods;
  a non-migrating brain pays one boolean check. Polls isMigrating() at 250ms;
  after migrationWaitTimeoutMs (default 30s) throws a retryable, exported
  MigrationInProgressError. The timeout bounds the CALLER'S WAIT, not the
  migration — the rebuild is unbounded and never interrupted.
- init() awaits the lock before VFS bootstrap + before serving ("not ready until
  upgraded"); a rebuild past the budget surfaces MigrationInProgressError at init
  (raise the budget, or run the offline migrator).

Observability (readiness-probe correct — never gated):
- getIndexStatus() gains `migrating` + `migration` (MigrationProgress); a probe
  maps migrating→HTTP 503+Retry-After, not 500. health()/checkHealth() lock-exempt
  (health() reports a `warn` migration check). stampBrainFormat()/close() are
  ungated so cor can clear the lock — no deadlock.
- Optional provider migrationStatus() is relayed verbatim for a live %.

verifyGraphAdjacencyLive() honors the lock (no self-rebuild mid-migration); the
stamp is still withheld while any provider migrates (cor stamps after verify).

Adversarially verified: fixed a critical init/VFS-bootstrap deadlock, a mixed-
provider busy-spin (dropped the event-driven signal → pure poll), a not-yet-
initialized getIndexStatus crash, and once-per-window log/clock resets. 10 lock
tests (incl. the init-during-migration regression). Gates: typecheck 0, build 0,
test:unit 1753/1753.
This commit is contained in:
David Snelling 2026-07-01 10:26:27 -07:00
parent ca9129a924
commit 67bbf69a5c
5 changed files with 534 additions and 31 deletions

View file

@ -137,6 +137,11 @@ export { RevisionConflictError } from './transaction/RevisionConflictError.js'
// transact/with() when a referenced entity or relation does not exist.
export { EntityNotFoundError, RelationNotFoundError } from './errors/notFound.js'
// Base error + typed migration-lock error — thrown by any data-plane call while a
// brain runs its one-time 7.x→8.0 upgrade; catch to answer HTTP 503 + Retry-After.
export { BrainyError, MigrationInProgressError, GraphIndexNotReadyError } from './errors/brainyError.js'
export type { BrainyErrorType } from './errors/brainyError.js'
// ============= 8.0 Db API — generational MVCC =============
// Immutable database values: brain.now() / brain.transact() / brain.asOf() /
// db.with() / db.persist() / Brainy.load(). See src/db/ for the record layer.