| … | … | @@ -12,29 +12,94 @@ const path = require('path') |
| 12 | 12 | |
| 13 | 13 | const file = path.join(__dirname, '..', 'node_modules', 'autobase', 'lib', 'apply-state.js') |
| 14 | 14 | |
| 15 | // NOTE: patch 1 must NOT process.exit() when it is already applied — patch 2 |
| 16 | // below still has to run. (It did exit, which is why the appendBatch bound |
| 17 | // silently never landed on an already-patched tree.) |
| 18 | let patch1Done = false |
| 19 | |
| 15 | 20 | try { |
| 16 | 21 | let s = fs.readFileSync(file, 'utf8') |
| 17 | | if (s.includes('GUARD (pgv3 patch)')) { process.exit(0) } // already patched |
| 18 | | const before = ' const view = this.views[k]\n view.core.download({ blocks })' |
| 19 | | const after = ' const view = this.views[k]\n' + |
| 20 | | ' // GUARD (pgv3 patch): a user view may not be materialized yet during\n' + |
| 21 | | ' // _drain warmup — its core can be null. This warmup pre-download is a\n' + |
| 22 | | ' // perf optimization, NOT correctness, so a null view.core must be SKIPPED,\n' + |
| 23 | | ' // never fatal. Before this guard the whole daemon SIGABRT-crash-looped on\n' + |
| 24 | | ' // boot (pear-git #3). Log the autobase key so the offending base is named.\n' + |
| 25 | | ' if (!view || !view.core) {\n' + |
| 26 | | ' try { console.error("[autobase][warmup] null view.core at view index " + k + " on base " + (this.base && this.base.key ? this.base.key.toString("hex").slice(0,16) : "?") + " — skipping (was fatal)") } catch {}\n' + |
| 27 | | ' continue\n' + |
| 28 | | ' }\n' + |
| 29 | | ' view.core.download({ blocks })' |
| 30 | | if (!s.includes(before)) { |
| 31 | | console.error('[patch-autobase-warmup] anchor not found (autobase version changed?) — SKIPPING; re-verify the warmup guard manually') |
| 32 | | process.exit(0) |
| 22 | if (s.includes('GUARD (pgv3 patch)')) { |
| 23 | patch1Done = true // already patched — fall through to patch 2, do NOT exit |
| 24 | } else { |
| 25 | const before = ' const view = this.views[k]\n view.core.download({ blocks })' |
| 26 | const after = ' const view = this.views[k]\n' + |
| 27 | ' // GUARD (pgv3 patch): a user view may not be materialized yet during\n' + |
| 28 | ' // _drain warmup — its core can be null. This warmup pre-download is a\n' + |
| 29 | ' // perf optimization, NOT correctness, so a null view.core must be SKIPPED,\n' + |
| 30 | ' // never fatal. Before this guard the whole daemon SIGABRT-crash-looped on\n' + |
| 31 | ' // boot (pear-git #3). Log the autobase key so the offending base is named.\n' + |
| 32 | ' if (!view || !view.core) {\n' + |
| 33 | ' try { console.error("[autobase][warmup] null view.core at view index " + k + " on base " + (this.base && this.base.key ? this.base.key.toString("hex").slice(0,16) : "?") + " — skipping (was fatal)") } catch {}\n' + |
| 34 | ' continue\n' + |
| 35 | ' }\n' + |
| 36 | ' view.core.download({ blocks })' |
| 37 | if (!s.includes(before)) { |
| 38 | console.error('[patch-autobase-warmup] anchor not found (autobase version changed?) — SKIPPING; re-verify the warmup guard manually') |
| 39 | } else { |
| 40 | s = s.replace(before, after) |
| 41 | fs.writeFileSync(file, s) |
| 42 | patch1Done = true |
| 43 | console.error('[patch-autobase-warmup] applied null-view.core warmup guard to autobase') |
| 44 | } |
| 33 | 45 | } |
| 34 | | s = s.replace(before, after) |
| 35 | | fs.writeFileSync(file, s) |
| 36 | | console.error('[patch-autobase-warmup] applied null-view.core warmup guard to autobase') |
| 37 | 46 | } catch (err) { |
| 38 | 47 | console.error('[patch-autobase-warmup] failed:', err && err.message) |
| 39 | 48 | // Never fail the install over the patch — the guard is defensive. |
| 40 | 49 | } |
| 50 | |
| 51 | // ── Patch 2: bound the _appendBatch flush loop ────────────────────────────── |
| 52 | // |
| 53 | // autobase/index.js _appendBatch spins until the appended nodes flush: |
| 54 | // |
| 55 | // while (this._appended < target && !this._interrupting) { |
| 56 | // await this._bump() |
| 57 | // if (runs++ >= 16 && this.localWriter && this.localWriter.idle()) break |
| 58 | // } |
| 59 | // |
| 60 | // The escape hatch requires BOTH runs >= 16 AND localWriter.idle(). A writer |
| 61 | // that can never flush never goes idle, so the break never fires and this spins |
| 62 | // forever — `await this._bump()` is a MICROtask chain into _advance, so the |
| 63 | // event loop is never handed back: no accept(), no timers, no SIGCHLD. |
| 64 | // |
| 65 | // Reached on a MIRROR whose writer key the origin has not enrolled yet. The |
| 66 | // 'Not writable' guard above only fires when localWriter is null or removed; |
| 67 | // stortron's writer EXISTS but cannot linearize, so it passes that check and |
| 68 | // then hangs here. |
| 69 | // |
| 70 | // Measured on stortron (mirror, 145 repos), caller stack captured from inside |
| 71 | // _advance at call #200,000 AND #800,000, identical both times: |
| 72 | // _doAnnounce -> registerMirrorWriters -> _appendBatch -> _bump -> _advance |
| 73 | // 800,000 _advance calls == 800,000 iterations of this one while loop. The |
| 74 | // daemon never printed its banner and never served a request. |
| 75 | // |
| 76 | // The fix makes the bound unconditional: 16 bumps, then stop. A short/failed |
| 77 | // append is recoverable; an unkillable daemon is not. |
| 78 | const idx = path.join(__dirname, '..', 'node_modules', 'autobase', 'index.js') |
| 79 | |
| 80 | try { |
| 81 | let s2 = fs.readFileSync(idx, 'utf8') |
| 82 | if (s2.includes('BOUND (pgv3 patch)')) { |
| 83 | console.error('[patch-autobase-warmup] _appendBatch flush bound already present') |
| 84 | return |
| 85 | } |
| 86 | const b2 = ' // safety\n' + |
| 87 | ' if (runs++ >= 16 && this.localWriter && this.localWriter.idle()) break' |
| 88 | const a2 = ' // BOUND (pgv3 patch): the original also required\n' + |
| 89 | ' // `this.localWriter.idle()`, so a writer that can never flush (a mirror\n' + |
| 90 | ' // whose key the origin has not enrolled) never satisfied the break and\n' + |
| 91 | ' // this loop spun forever. `await this._bump()` is a microtask chain into\n' + |
| 92 | ' // _advance, so the event loop was never handed back — no accept(), no\n' + |
| 93 | ' // timers, no SIGCHLD reaping. Bound it unconditionally: a short append is\n' + |
| 94 | ' // recoverable, a wedged daemon is not.\n' + |
| 95 | ' if (runs++ >= 16) break' |
| 96 | if (!s2.includes(b2)) { |
| 97 | console.error('[patch-autobase-warmup] appendBatch anchor not found (autobase version changed?) — SKIPPING; re-verify the flush bound manually') |
| 98 | process.exit(0) |
| 99 | } |
| 100 | s2 = s2.replace(b2, a2) |
| 101 | fs.writeFileSync(idx, s2) |
| 102 | console.error('[patch-autobase-warmup] applied _appendBatch flush bound to autobase') |
| 103 | } catch (err) { |
| 104 | console.error('[patch-autobase-warmup] appendBatch patch failed:', err && err.message) |
| 105 | } |