| … | … | @@ -43,9 +43,68 @@ export class OverlayRepoStore { |
| 43 | 43 | |
| 44 | 44 | async ready () { |
| 45 | 45 | fs.mkdirSync(this._recordsDir, { recursive: true }) |
| 46 | // Restore servability across restart. The overlay's path maps are in-memory and only registerFile |
| 47 | // fills them, so without this a restarted node serves ONLY records written since boot — every file |
| 48 | // still on disk, yet peers get "no connected peer served this oid" (75+ hours of it on wj62), which |
| 49 | // blocks signing. Seeding from the PERSISTED index costs one scan with no hashing and no file |
| 50 | // reads, and it avoids the protocol's tier-3 full-index re-scan on every content request. |
| 51 | // Our overlay paths are disk paths relative to root, so the resolver is a plain join. |
| 52 | if (this._overlay && typeof this._overlay.seedFilePathsFromIndex === 'function') { |
| 53 | try { |
| 54 | const n = await this._overlay.seedFilePathsFromIndex((overlayPath) => join(this._root, overlayPath)) |
| 55 | if (n) console.log(JSON.stringify({ msg: 'overlay: seeded record paths from the persisted index (servable-by-oid after restart)', did: this._did, seeded: n })) |
| 56 | } catch (err) { console.error(JSON.stringify({ msg: 'overlay path seed FAILED — this node may not serve its records by oid until re-registered', did: this._did, error: err.message })) } |
| 57 | } |
| 46 | 58 | return this |
| 47 | 59 | } |
| 48 | 60 | |
| 61 | // EXPLICIT REPAIR — not a boot step. ready() seeds the path maps from the persisted index, which is |
| 62 | // the cheap normal path. This is the heavier fallback for a store whose INDEX itself is missing or |
| 63 | // stale (records written before overlay indexing existed, or an index rebuilt/lost): it re-hashes |
| 64 | // every record file and re-registers it. Needed once on nodes whose indexes predate the seed fix; |
| 65 | // deliberately NOT wired into ready(), because re-hashing every file at every boot is the cold-start |
| 66 | // cost that made pgh-sidecar take ~190s to bind its socket. |
| 67 | // |
| 68 | // Re-register every record file with the overlay so peers can fetch it BY OID. |
| 69 | // |
| 70 | // hyper-overlay's serve path resolves contentHash -> disk path through an IN-MEMORY map |
| 71 | // (overlay.js `_contentHashPaths`, populated only by registerFile and handed to protocol.js by |
| 72 | // reference). Nothing rebuilds it at boot, and the persisted FileIndex stores { contentHash, size, |
| 73 | // mtime } WITHOUT the disk path — so the overlay cannot repair itself; only the consumer knows the |
| 74 | // layout. Net effect before this: after a restart a node could serve ONLY the records it had |
| 75 | // written since boot ("registering a file makes it servable", overlay.js:15), even though every |
| 76 | // file was still on disk. |
| 77 | // |
| 78 | // That silently breaks cross-node byte resolution, which GATES SIGNING: the forward-only sign gate |
| 79 | // skips while a converged pointer's bytes are unresolved. Measured on the live fleet — wj62's |
| 80 | // app.bsky.feed.post/3mr6v6r2ofs22 sat unresolvable for 75+ hours while the peer holding it had the |
| 81 | // exact bytes on disk (hash verified equal to the pointer's oid), reporting only "no connected peer |
| 82 | // served this oid"; both co-owners held different incomplete subsets for the same reason. |
| 83 | // |
| 84 | // Idempotent and safe to run every boot: registerFile re-asserts an identical entry. Deliberately |
| 85 | // passes NO `rev` (a fresh TID would make a re-registration look like a newer write to rev-wins |
| 86 | // conflict resolution) and `prepare: false` (the serve path chunks lazily on first request, so we |
| 87 | // avoid a second full read of every file at boot). |
| 88 | async reindexOverlay () { |
| 89 | if (!this._overlay) return { skipped: 'no-overlay' } |
| 90 | let registered = 0 |
| 91 | let failed = 0 |
| 92 | const files = await this.records() |
| 93 | for (const { key, bytes } of files) { |
| 94 | const i = key.indexOf('/') |
| 95 | const collection = key.slice(0, i) |
| 96 | const rkey = key.slice(i + 1) |
| 97 | try { |
| 98 | const r = await this._overlay.registerFile(this._overlayPath(collection, rkey), this._pathFor(collection, rkey), { size: bytes.length, prepare: false }) |
| 99 | if (r) registered++; else failed++ |
| 100 | } catch { failed++ } |
| 101 | } |
| 102 | if (files.length) { |
| 103 | try { console.log(JSON.stringify({ msg: 'overlay reindex: record files re-registered as servable-by-oid', did: this._did, registered, failed, total: files.length })) } catch {} |
| 104 | } |
| 105 | return { registered, failed, total: files.length } |
| 106 | } |
| 107 | |
| 49 | 108 | _pathFor (collection, rkey) { |
| 50 | 109 | if (!isSafeSeg(collection) || !isSafeSeg(rkey)) throw new Error(`OverlayRepoStore: unsafe record key "${collection}/${rkey}"`) |
| 51 | 110 | return join(this._recordsDir, collection, rkey + EXT) |