OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-store commits / 4b4dc1a

0.0.14: byte-resolution misses must name WHICH fault they are
`overlay.fetchFile` returns null for two ROOT-DISTINCT reasons — overlay.js:307-315 bails when no peer ever attaches, otherwise the attached peers simply don't hold the content — and the log line asserted only one of them: "no connected peer served this oid" That single string sent this investigation at the holder's index for two days while the real question was whether the overlay channel attaches at all. A message that names a cause it cannot distinguish is worse than one that says nothing. Now reports `overlayPeers` and classifies: 0 -> "NO overlay peers attached (transport/wiring fault, not content)" >0 -> "peers attached but none served this oid (distribution fault)" Also: dedup was keyed per-oid-per-PROCESS, so the line was emitted once at boot and never again. A stale line was then indistinguishable from a live failure and could not be used to judge whether a repair had worked — I misread exactly that today and reported a fixed condition as still-failing. Dedup is now keyed on oid + peerCount: quiet on repeats, re-emits the moment connectivity changes, which is precisely when the answer changes. 3 tests / 13 asserts capture the console output and pin both the classification and the re-emit-on-change behaviour. Suite 49/49, 130/130. This is the diagnostic surface for a path that GATES SIGNING — hyper-social #13 lesson 3: "a swallowed error on a path that gates something must log once." Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4b4dc1aed1b3608fbf030358e9ad5934b93e3345
by plankatron <fleeky@gmail.com> · 2026-07-26T17:46:54.000Z

Changed files (3) +104 -6

lib/overlay-repo-store.js +20 -5
……@@ -136,7 +136,7 @@ export class OverlayRepoStore {
136136 try {
137137 fs.mkdirSync(join(this._recordsDir, collection), { recursive: true })
138138 const r = await this._overlay.fetchFile(oid, { destPath: disk })
139 if (!r) { this._logFetchMiss(collection, rkey, oid, 'fetchFile returned null — no connected peer served this oid'); return null }
139 if (!r) { this._logFetchMiss(collection, rkey, oid, 'fetchFile returned null'); return null }
140140 } catch (err) { this._logFetchMiss(collection, rkey, oid, (err && err.message) || String(err)); return null }
141141 return this.get(collection, rkey)
142142 }
……@@ -147,15 +147,30 @@ export class OverlayRepoStore {
147147 // unresolved, so a permanent miss silently stops a co-owner signing at all, with no diagnosable
148148 // cause. Observed on wj62 — one record unresolved for 75+ hours with nothing logged anywhere.
149149 // Deduped per oid, because the resolve loop calls this once per record on every list().
150 // `fetchFile` returns null for two ROOT-DISTINCT reasons and the old message asserted only one of
151 // them ("no connected peer served this oid"), which is why a transport-level fault read as a content
152 // fault for days. overlay.js:307-315 waits peerWaitMs for a peer and bails if none ever attaches;
153 // otherwise it asks the attached peers and they may simply not hold it. Reporting `overlayPeers`
154 // separates those: 0 => the overlay channel never attached (a WIRING fault — look at attachProtocol);
155 // >0 => genuinely nobody serving that content (a DISTRIBUTION fault — look at the holder's index).
156 //
157 // Dedup is keyed on oid + peerCount, not oid alone. Deduping per-oid-per-process meant the line was
158 // emitted once at boot and never again, so a stale line was indistinguishable from a live failure and
159 // could not be used to judge whether a repair had worked. Keying on the peer count keeps the log quiet
160 // while still re-emitting the moment connectivity changes — which is exactly when the answer changes.
150161 _logFetchMiss (collection, rkey, oid, reason) {
151162 if (!this._fetchMisses) this._fetchMisses = new Set()
163 const peers = (this._overlay && typeof this._overlay.peerCount === 'number') ? this._overlay.peerCount : null
152164 const k = String(oid)
153 if (this._fetchMisses.has(k)) return
154 this._fetchMisses.add(k)
165 const dedupKey = k + ':' + peers
166 if (this._fetchMisses.has(dedupKey)) return
167 this._fetchMisses.add(dedupKey)
155168 try {
156169 console.error(JSON.stringify({
157 msg: 'overlay byte-resolution FAILED — pointer converged but bytes unavailable (blocks signing)',
158 did: this._did, key: `${collection}/${rkey}`, oid: k.slice(0, 16), reason
170 msg: peers === 0
171 ? 'overlay byte-resolution FAILED — NO overlay peers attached (transport/wiring fault, not content)'
172 : 'overlay byte-resolution FAILED — peers attached but none served this oid (distribution fault)',
173 did: this._did, key: `${collection}/${rkey}`, oid: k.slice(0, 16), overlayPeers: peers, reason
159174 }))
160175 } catch {}
161176 }
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-store",
3 "version": "0.0.13",
3 "version": "0.0.14",
44 "description": "Holepunch-native atproto repo store — records in a classic Hyperbee (collection/rkey -> DAG-CBOR). The P2P home; the atproto-repo module is the bridge serializer over it.",
55 "type": "module",
66 "main": "lib/repo-store.js",
test/fetch-miss-diagnostics.test.js +83 -0
……@@ -0,0 +1,83 @@
1// Byte-resolution failure diagnostics.
2//
3// `overlay.fetchFile` returns null for two ROOT-DISTINCT reasons, and the old log line asserted only
4// one of them ("no connected peer served this oid"). That single misleading string cost days: a
5// transport-level fault (the overlay channel never attached) read as a content fault (nobody holds the
6// bytes), sending the investigation at the holder's index instead of at the wiring.
7//
8// It also deduped per-oid-per-PROCESS, so the line appeared once at boot and never again — a stale line
9// was indistinguishable from a live failure, and could not be used to tell whether a repair had worked.
10//
11// These tests pin both properties, because this log line IS the diagnostic surface for a path that
12// gates signing.
13
14import test from 'brittle'
15import { mkdtempSync, rmSync } from 'node:fs'
16import { join } from 'node:path'
17import { tmpdir } from 'node:os'
18import { OverlayRepoStore } from '../lib/overlay-repo-store.js'
19
20function tmp (t) { const d = mkdtempSync(join(tmpdir(), 'fmd-')); t.teardown(() => rmSync(d, { recursive: true, force: true })); return d }
21
22// An overlay that always fails to fetch, with a settable peer count.
23function mkOverlay (peerCount) {
24 return {
25 peerCount,
26 async registerFile () { return { contentHash: 'deadbeef' } },
27 async fetchFile () { return null } // the failure under test
28 }
29}
30
31// Capture console.error for the duration of a block.
32async function capture (t, fn) {
33 const lines = []
34 const orig = console.error
35 console.error = (s) => lines.push(String(s))
36 t.teardown(() => { console.error = orig })
37 try { await fn() } finally { console.error = orig }
38 return lines.map((l) => { try { return JSON.parse(l) } catch { return { raw: l } } })
39}
40
41test('ZERO peers is reported as a transport/wiring fault, not a content fault', async (t) => {
42 const store = await new OverlayRepoStore(tmp(t), { overlay: mkOverlay(0), did: 'did:plc:x' }).ready()
43
44 const logs = await capture(t, async () => {
45 const r = await store.ensureLocal('app.bsky.feed.post', '3kaaa', 'abc123')
46 t.is(r, null, 'unresolvable record still returns null')
47 })
48
49 t.is(logs.length, 1, 'exactly one diagnostic emitted')
50 t.is(logs[0].overlayPeers, 0, 'the peer count is reported')
51 t.ok(/NO overlay peers attached/.test(logs[0].msg), 'classified as a transport/wiring fault')
52 t.absent(/none served this oid/.test(logs[0].msg), 'does NOT assert the content-fault cause')
53})
54
55test('peers attached but content missing is reported as a distribution fault', async (t) => {
56 const store = await new OverlayRepoStore(tmp(t), { overlay: mkOverlay(2), did: 'did:plc:x' }).ready()
57
58 const logs = await capture(t, async () => {
59 await store.ensureLocal('app.bsky.feed.post', '3kaaa', 'abc123')
60 })
61
62 t.is(logs.length, 1, 'exactly one diagnostic emitted')
63 t.is(logs[0].overlayPeers, 2, 'the peer count is reported')
64 t.ok(/none served this oid/.test(logs[0].msg), 'classified as a distribution fault')
65})
66
67test('dedup is keyed on peer count, so connectivity changes re-emit (staleness fix)', async (t) => {
68 const overlay = mkOverlay(0)
69 const store = await new OverlayRepoStore(tmp(t), { overlay, did: 'did:plc:x' }).ready()
70
71 const logs = await capture(t, async () => {
72 await store.ensureLocal('app.bsky.feed.post', '3kaaa', 'abc123')
73 await store.ensureLocal('app.bsky.feed.post', '3kaaa', 'abc123') // same oid, same peers → quiet
74 t.is(overlay.peerCount, 0, 'still no peers')
75 overlay.peerCount = 1 // a peer attaches
76 await store.ensureLocal('app.bsky.feed.post', '3kaaa', 'abc123') // → must re-emit
77 })
78
79 t.is(logs.length, 2, 'quiet on repeat, re-emits when the peer count changes')
80 t.is(logs[0].overlayPeers, 0, 'first line: no peers')
81 t.is(logs[1].overlayPeers, 1, 'second line: a peer attached and it STILL failed — the useful signal')
82 t.ok(/none served this oid/.test(logs[1].msg), 'and the classification flipped with the evidence')
83})