fix: recover VFS content blobs stranded by a 7→8 upgrade, in place on open
The 7.x branch system stored Virtual Filesystem content as blobs in its
copy-on-write area (`_cow/`). 8.0 removed that system and stores content blobs
in the content-addressed store (`_cas/`), but the one-time layout migration only
moves entities — it never adopted the `_cow/` content blobs. A store that used
the VFS was left with every VFS-backed read throwing "Blob metadata not found"
and its pages 500ing, with no first-party recovery and the pre-upgrade backup
already removed on entity-migration "success".
Add an on-open recovery pass (`autoAdoptLegacyVfsBlobsIfNeeded`) that runs right
after the layout migration and adopts every orphaned `_cow/` blob into `_cas/` —
copying both the bytes and the metadata via the raw object primitives,
idempotently and non-destructively (the `_cow/` originals are never deleted). It
is a separate phase, not folded into the migration, so it also heals a store
already upgraded by an earlier 8.0.x that stranded the blobs (whose layout-
migration marker is stamped): it is gated on the presence of `_cow/` and its own
`_system/vfs-blob-adoption.json` marker. Filesystem-only; native-8.0 and non-VFS
stores no-op on a cheap existence check.
- `BaseStorage.adoptLegacyCowBlobs()` — the scan/copy primitive, returning
`{ cowBlobs, adopted, alreadyPresent, incomplete }`; skips (does not half-adopt)
a blob missing its bytes or metadata.
- `brain.vfs.adoptOrphanedBlobs()` — the explicit force path; self-initializes.
- Backup retention: the automatic pre-upgrade backup is now kept if any blob
can't be fully adopted (`incomplete > 0`), instead of being removed on
entity-migration success alone — a blob-parity gate the migration signal
cannot provide.
Adds an end-to-end integration test (storage-level adopt with idempotency and
incomplete handling; self-heal on reopen; the explicit API) and an upgrade guide.
This commit is contained in:
parent
6821e1980b
commit
c0f6ccd958
5 changed files with 584 additions and 1 deletions
|
|
@ -352,6 +352,52 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Recover VFS content blobs orphaned by a 7→8 upgrade. 7.x stored
|
||||
* VFS blobs under the copy-on-write area (`_cow/`) of the branch/versioning
|
||||
* system that 8.0 removed; a brain upgraded before the migration adopted them
|
||||
* reads a stranded blob and throws "Blob metadata not found" (every page
|
||||
* backed by it 500s). This adopts every orphaned `_cow/` blob into the 8.0
|
||||
* content-addressed store (`_cas/`) IN PLACE — no snapshot restore — then
|
||||
* clears the VFS caches so the recovered content reads live immediately.
|
||||
*
|
||||
* Idempotent and non-destructive: blobs already adopted are skipped and the
|
||||
* `_cow/` originals are never deleted (a re-run or rollback stays possible).
|
||||
* Safe to run on a healthy or native-8.0 brain (returns zeros). Delegates to
|
||||
* {@link BaseStorage.adoptLegacyCowBlobs}.
|
||||
*
|
||||
* @returns `{ cowBlobs, adopted, alreadyPresent, incomplete }` counts.
|
||||
* @example
|
||||
* const brain = new Brainy({ storage: { type: 'filesystem', path: '/data/brain' } })
|
||||
* await brain.init()
|
||||
* const r = await brain.vfs.adoptOrphanedBlobs()
|
||||
* console.log(`Adopted ${r.adopted} stranded VFS blobs; ${r.alreadyPresent} already present.`)
|
||||
*/
|
||||
async adoptOrphanedBlobs(): Promise<{
|
||||
cowBlobs: number
|
||||
adopted: number
|
||||
alreadyPresent: number
|
||||
incomplete: number
|
||||
}> {
|
||||
// The recovery scan works on raw storage objects, so it only needs the
|
||||
// brain's storage wired up — not the VFS's own init. brain.init() is
|
||||
// idempotent (a no-op on an already-open brain) and, on a cold brain,
|
||||
// sets up storage AND runs the on-open auto-adoption itself; the explicit
|
||||
// scan below is then the idempotent "force re-scan" equivalent.
|
||||
await this.brain.init()
|
||||
const storage = this.brain['storage'] as unknown as
|
||||
| { adoptLegacyCowBlobs?: () => Promise<{ cowBlobs: number; adopted: number; alreadyPresent: number; incomplete: number }> }
|
||||
| undefined
|
||||
if (!storage || typeof storage.adoptLegacyCowBlobs !== 'function') {
|
||||
return { cowBlobs: 0, adopted: 0, alreadyPresent: 0, incomplete: 0 }
|
||||
}
|
||||
const result = await storage.adoptLegacyCowBlobs()
|
||||
// Drop any cached content/stat so a re-read hits the freshly adopted blobs.
|
||||
this.contentCache.clear()
|
||||
this.statCache.clear()
|
||||
return result
|
||||
}
|
||||
|
||||
// ============= File Operations =============
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue