feat(8.0): auto pre-upgrade backup — hard-link snapshot before the 7.x→8.0 migration
David's ask: back up the brain before the one-time upgrade, drop it after. On open, when the on-disk format is stale (a 7.x→8.0 rebuild will run) and the store holds data, brainy hard-link-snapshots the brain dir to a sibling `<brainDir>.migration-backup` BEFORE any provider rebuilds — near-zero cost/space (shared inodes; the store is immutable-by-rename), instant even at scale. Removed automatically once the upgrade verifies + stamps the marker; retained on failure for rollback. Default-on; opt out with `migrationBackup: false`. - Reuses the existing snapshotToDirectory() hard-link farm via new optional adapter methods createMigrationBackup()/removeMigrationBackup() (filesystem only — feature-detected; memory + non-fs are a graceful no-op). - Taken before the native provider inits, so it captures true pre-migration state; a prior failed upgrade's backup is reused, not overwritten. - Best-effort: a backup failure never blocks the upgrade (the migration is itself safe — reads canonical, reconstructable, self-heals). Catastrophe-insurance against a migration bug, not a data-loss guard or an off-device backup. 4 lifecycle tests (adapter hard-link / reuse / remove-without-touching-live / empty→null; stale-epoch reopen create+remove; opt-out; memory no-op). Gates: typecheck 0, build 0, test:unit 1753/1753, layout-migration suite 5/5.
This commit is contained in:
parent
ed178e2ce9
commit
1aad1f67de
5 changed files with 319 additions and 0 deletions
|
|
@ -1106,6 +1106,56 @@ export class FileSystemStorage extends BaseStorage {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Pre-upgrade backup: a hard-link snapshot of the whole store into
|
||||
* a SIBLING directory (`<rootDir>.migration-backup`, outside `rootDir` so the
|
||||
* snapshot never recurses into itself), taken before a 7.x → 8.0 upgrade
|
||||
* rebuilds the derived indexes. Zero-copy (shared inodes; the store is
|
||||
* immutable-by-rename) and instant even at scale. Returns the backup path, or
|
||||
* `null` when the store holds no canonical nouns (nothing to protect). If a
|
||||
* backup already exists from a prior FAILED upgrade, it is REUSED as-is (that
|
||||
* is the true pre-upgrade state; a retry must not overwrite it).
|
||||
*/
|
||||
public async createMigrationBackup(): Promise<string | null> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Nothing to protect if the store has no canonical nouns (e.g. a brand-new
|
||||
// brain whose marker is simply absent — `epochStale` is true but there is no
|
||||
// 7.x data to migrate).
|
||||
const probe = await this.getNouns({ pagination: { limit: 1 } })
|
||||
if ((probe.totalCount || 0) === 0 && probe.items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const backupPath = `${this.rootDir}.migration-backup`
|
||||
|
||||
// Reuse an existing backup (a prior failed upgrade's pre-state) rather than
|
||||
// overwrite it — snapshotToDirectory also refuses a non-empty target.
|
||||
try {
|
||||
const existing = await fs.promises.readdir(backupPath)
|
||||
if (existing.length > 0) return backupPath
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') throw error
|
||||
}
|
||||
|
||||
await this.snapshotToDirectory(backupPath)
|
||||
return backupPath
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Remove a {@link createMigrationBackup} snapshot. Best-effort: a
|
||||
* missing path is a no-op, and any error is swallowed (a leftover backup dir is
|
||||
* harmless — the operator can delete it). Removing the hard-links never touches
|
||||
* the live store's bytes (shared inodes; only the extra links go away).
|
||||
*/
|
||||
public async removeMigrationBackup(location: string): Promise<void> {
|
||||
try {
|
||||
await fs.promises.rm(location, { recursive: true, force: true })
|
||||
} catch {
|
||||
// best-effort — leaving the backup behind is safe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect snapshot-eligible files under `dirAbs`, excluding the
|
||||
* lock directory and in-flight `*.tmp.*` write files.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue