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:
parent
ca9129a924
commit
67bbf69a5c
5 changed files with 534 additions and 31 deletions
|
|
@ -12,6 +12,7 @@ export type BrainyErrorType =
|
|||
| 'VALIDATION'
|
||||
| 'FIELD_NOT_INDEXED'
|
||||
| 'GRAPH_INDEX_NOT_READY'
|
||||
| 'MIGRATION_IN_PROGRESS'
|
||||
|
||||
/**
|
||||
* Custom error class for Brainy operations
|
||||
|
|
@ -253,3 +254,46 @@ export class GraphIndexNotReadyError extends BrainyError {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a data-plane read or write is issued against a brain that is
|
||||
* running its one-time, automatic 7.x → 8.0 on-disk upgrade — the coordinated
|
||||
* migration LOCK. While a native provider rebuilds all derived indexes from the
|
||||
* canonical records, brainy blocks reads and writes so no operation touches a
|
||||
* half-built index; the caller waits for the correct answer rather than getting
|
||||
* a partial one. This error is raised ONLY when the wait exceeds the configured
|
||||
* window ({@link BrainyConfig.migrationWaitTimeoutMs}, default 30 s) — never
|
||||
* instead of a partial/incorrect result.
|
||||
*
|
||||
* It is `retryable`: the upgrade continues in the background. Retry shortly,
|
||||
* watch `brain.getIndexStatus().migration` for progress, or for a very large
|
||||
* brain run the offline migrator. Data is safe; nothing is lost. Consumers can
|
||||
* catch this (e.g. request middleware) and answer HTTP 503 + `Retry-After`.
|
||||
*
|
||||
* @example
|
||||
* try {
|
||||
* await brain.find({ query })
|
||||
* } catch (e) {
|
||||
* if (e instanceof MigrationInProgressError) {
|
||||
* res.set('Retry-After', '5').status(503).json({ upgrading: true, percent: e.percent })
|
||||
* return
|
||||
* }
|
||||
* throw e
|
||||
* }
|
||||
*/
|
||||
export class MigrationInProgressError extends BrainyError {
|
||||
/** Milliseconds the operation waited on the migration lock before timing out. */
|
||||
public readonly elapsedMs: number
|
||||
/** Latest observed migration progress (0–100), when the provider reports it. */
|
||||
public readonly percent?: number
|
||||
|
||||
constructor(message: string, elapsedMs: number, percent?: number, originalError?: Error) {
|
||||
super(message, 'MIGRATION_IN_PROGRESS', true, originalError)
|
||||
this.name = 'MigrationInProgressError'
|
||||
this.elapsedMs = elapsedMs
|
||||
this.percent = percent
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, MigrationInProgressError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue