OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 commits / 22869a5

0.1.237: locations.json travels with the release, so a mirror can seed without a second copy
A vdrive stores `index -> contentHash`. Serving needs the other half, `contentHash -> (file, offset, length)`, and rebuilding it by re-chunking every published file is O(all bytes) on every boot. So it is written once at publish time as locations.json. It lived only at `<releaseDir>/<repo>/.vdrive/locations.json` — outside the version dir, so it was not part of what replicates with a release. Measured on the fleet: stortron holds all five ifsrenderer-linux versions and the key (which already rides in manifest.json), yet cannot resolve a single block. Seeding a drive it could serve for ~281KB would instead cost a second full copy of every byte — 141MB per release, which is the copy hyper-vdrive exists to avoid. Now copied into `<releaseDir>/<repo>/versions/<version>/` alongside manifest.json. A COPY, not a move, though org #146 says "move". The index is per-drive state and the origin needs it in place: publishTree reads it back to seed dedup on the next republish, and openDrive reads it to serve. Moving it would break both unless publish learned to locate the previous version's copy — and the index is semantically per-repo while a manifest is per-version, which #146 itself flags as unsettled. The version-dir copy is a replication vehicle, not the working copy. That also settles #146's snapshot-vs-pointer question, empirically rather than by assertion. The index is cumulative — publishTree loads the prior one, adds this version's chunks, saves the union — so each copy is a snapshot of the whole drive at that point and the NEWEST version's copy is sufficient alone. The third test publishes two versions and asserts every hash from the first snapshot survives into the second; if this ever became a per-version delta, a mirror would silently fail to resolve blocks from older releases. Paths are already relative to `<releaseDir>/<repo>`, which the version dir sits under, so the bytes are portable exactly as written. A test asserts no entry is absolute or escapes the root, since either would bake this machine's layout into an artifact peers can read. Non-fatal, like annotateManifest: a release must not fail because its index could not be copied. Reported as `locationsCopied`. Nothing reads the travelling copy yet — a mirror still cannot seed cheaply until something opens a drive against a rebuilt store using it. That work is now unblocked rather than blocked. Verified: test written first and confirmed red for the right reason (store copy present, version copy ENOENT). Now 3/3 tests, 13/13 asserts. Neighbours clean — vdrive-ci 23/23, vdrive-walk-order 11/11, vdrive-seeder 32/32. Full suite skipped deliberately: cutover-prod-copy exceeds its own timeout and strands ~21GB in /tmp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
22869a504ba14614c466331c0f98b8c7724a6a73
by plankatron <fleeky@gmail.com> · 2026-08-26T00:42:48.000Z

Changed files (3) +169 -2

lib/vdrive-publish.js +37 -1
……@@ -55,6 +55,41 @@ async function annotateManifest (releaseDir, repo, version, vdrive) {
5555 }
5656}
5757
58/**
59 * Copy the drive's locations index into the version dir, so it replicates.
60 *
61 * A vdrive stores `index -> contentHash`; serving needs `contentHash -> (file,
62 * offset, length)`. That index lives beside the drive, at
63 * `<releaseDir>/<repo>/.vdrive/locations.json` — outside the version dir, so it
64 * is not part of what replicates with a release. The consequence measured on the
65 * fleet: stortron holds every `ifsrenderer-linux` version and the key (which
66 * rides in manifest.json), yet cannot resolve a single block, so seeding a drive
67 * it could serve for ~281KB would cost a second full copy of every byte.
68 *
69 * A COPY, not a move. The index is per-drive state and the origin needs it in
70 * place: `publishTree` reads it back to seed dedup on the next republish, and
71 * `openDrive` reads it to serve. The version-dir copy is a replication vehicle,
72 * not the working copy.
73 *
74 * The index is cumulative — publishTree loads the prior one, adds this version's
75 * chunks, and saves the union — so each copy is a snapshot of the whole drive at
76 * that point, and the NEWEST version's copy is sufficient on its own. Paths are
77 * already relative to `<releaseDir>/<repo>`, which the version dir sits under, so
78 * the bytes are portable exactly as written.
79 *
80 * Non-fatal: a release must not fail because its index could not be copied.
81 */
82async function copyLocations (releaseDir, repo, version) {
83 const from = path.join(vdriveStoreDir(releaseDir, repo), 'locations.json')
84 const to = path.join(releaseDir, repo, 'versions', version, 'locations.json')
85 try {
86 await fsp.copyFile(from, to)
87 return true
88 } catch {
89 return false
90 }
91}
92
5893async function dirSize (dir) {
5994 let total = 0
6095 for (const e of await fsp.readdir(dir, { withFileTypes: true })) {
……@@ -164,6 +199,7 @@ export async function publishVersionToDrive ({
164199 })
165200
166201 const annotated = await annotateManifest(releaseDir, repo, version, result)
202 const locationsCopied = await copyLocations(releaseDir, repo, version)
167203
168204 if (onLog) {
169205 // Report the pointer/literal split and the store size. Without them a large
……@@ -184,5 +220,5 @@ export async function publishVersionToDrive ({
184220 )
185221 }
186222
187 return { ok: true, ...result, annotated }
223 return { ok: true, ...result, annotated, locationsCopied }
188224}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pear-git-v3",
3 "version": "0.1.236",
3 "version": "0.1.237",
44 "description": "P2P git forge — multi-writer issue/org/release/peer-mirror daemon. Built bottom-up after the failed v2.",
55 "type": "module",
66 "main": "index.js",
test/vdrive-locations-travel.test.js +131 -0
……@@ -0,0 +1,131 @@
1/**
2 * locations.json must travel with the release it describes (org #146).
3 *
4 * A vdrive stores `index -> contentHash`. Serving needs the other half,
5 * `contentHash -> (file, offset, length)`, and rebuilding that by re-chunking
6 * every published file is O(all bytes) on every boot. So it is written once at
7 * publish time as `locations.json`.
8 *
9 * It lived only at `<releaseDir>/<repo>/.vdrive/locations.json` — OUTSIDE the
10 * version dir, so it is not part of what replicates with a release. A mirror
11 * therefore holds the artifacts and the key (which rides in manifest.json) but
12 * cannot resolve a single block, and has to pay for a second full copy of every
13 * byte to seed a drive it could otherwise serve for ~281KB.
14 *
15 * The index is CUMULATIVE: publishTree loads the prior index, adds the new
16 * version's chunks, and saves the union. So the copy written into a version dir
17 * is a snapshot of the whole drive at that point, not a per-version delta —
18 * which is what makes the NEWEST version's copy sufficient on its own.
19 */
20import test from 'brittle'
21import fsp from '#node:fs/promises'
22import path from '#node:path'
23import os from '#node:os'
24
25import { publishVersionToDrive, vdriveStoreDir } from '../lib/vdrive-publish.js'
26
27function tmpdir (name) {
28 return fsp.mkdtemp(path.join(os.tmpdir(), 'pgv3-vdloc-' + name + '-'))
29}
30
31async function publish (releaseDir, repo, version, files) {
32 const versionDir = path.join(releaseDir, repo, 'versions', version)
33 await fsp.mkdir(versionDir, { recursive: true })
34 for (const [name, data] of Object.entries(files)) {
35 await fsp.writeFile(path.join(versionDir, name), data)
36 }
37 const { chunk } = await import('hyper-overlay/chunker')
38 const res = await publishVersionToDrive({
39 releaseDir,
40 repo,
41 version,
42 channel: 'production',
43 vdrive: { enabled: true, channels: null },
44 chunk
45 })
46 return { res, versionDir }
47}
48
49test('locations.json is written into the version dir so it replicates', async (t) => {
50 const releaseDir = await tmpdir('travel')
51 const repo = 'demo'
52
53 const { res, versionDir } = await publish(releaseDir, repo, '1.0.0', {
54 'app.bin': Buffer.alloc(512 * 1024, 3)
55 })
56 t.is(res.ok, true, 'published')
57
58 const inVersion = path.join(versionDir, 'locations.json')
59 const inStore = path.join(vdriveStoreDir(releaseDir, repo), 'locations.json')
60
61 t.ok(await exists(inStore), 'the drive keeps its working copy')
62 t.ok(await exists(inVersion), 'and a copy travels with the version')
63
64 const a = JSON.parse(await fsp.readFile(inStore, 'utf8'))
65 const b = JSON.parse(await fsp.readFile(inVersion, 'utf8'))
66 t.alike(b, a, 'the travelling copy is the same index')
67 t.ok(b.entries.length > 0, 'and it is not empty')
68
69 await fsp.rm(releaseDir, { recursive: true, force: true })
70})
71
72test('every recorded path is relative and inside the root', async (t) => {
73 // An absolute path would not resolve on another machine and would bake this
74 // machine's layout into an artifact peers can read.
75 const releaseDir = await tmpdir('rel')
76 const repo = 'demo'
77 const { versionDir } = await publish(releaseDir, repo, '1.0.0', {
78 'app.bin': Buffer.alloc(256 * 1024, 9),
79 'notes.txt': 'hello'
80 })
81
82 const idx = JSON.parse(await fsp.readFile(path.join(versionDir, 'locations.json'), 'utf8'))
83 for (const [, rel] of idx.entries) {
84 t.absent(path.isAbsolute(rel), 'relative: ' + rel)
85 t.absent(rel.startsWith('..'), 'inside the root: ' + rel)
86 }
87
88 await fsp.rm(releaseDir, { recursive: true, force: true })
89})
90
91test("the newest version's copy covers earlier versions too", async (t) => {
92 // The index accumulates, so a mirror needs only the latest version's copy to
93 // serve the whole drive. If this ever became a per-version delta, a mirror
94 // would silently fail to resolve blocks belonging to older releases.
95 const releaseDir = await tmpdir('accum')
96 const repo = 'demo'
97
98 const first = await publish(releaseDir, repo, '1.0.0', {
99 'one.bin': Buffer.alloc(256 * 1024, 1)
100 })
101 t.is(first.res.ok, true, 'first published')
102
103 const second = await publish(releaseDir, repo, '2.0.0', {
104 'two.bin': Buffer.alloc(256 * 1024, 2)
105 })
106 t.is(second.res.ok, true, 'second published')
107
108 const older = JSON.parse(
109 await fsp.readFile(path.join(first.versionDir, 'locations.json'), 'utf8')
110 )
111 const newest = JSON.parse(
112 await fsp.readFile(path.join(second.versionDir, 'locations.json'), 'utf8')
113 )
114
115 const newestHashes = new Set(newest.entries.map((e) => e[0]))
116 for (const [hash] of older.entries) {
117 t.ok(newestHashes.has(hash), 'newest snapshot still carries ' + hash.slice(0, 12))
118 }
119 t.ok(newest.entries.length >= older.entries.length, 'and it only grew')
120
121 await fsp.rm(releaseDir, { recursive: true, force: true })
122})
123
124async function exists (p) {
125 try {
126 await fsp.stat(p)
127 return true
128 } catch {
129 return false
130 }
131}