OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room / test / phase2-nonblocking.test.js

89 lines · javascript ref:
// Phase-2 admission-durability mechanism test (hyper-social #6).
//
// Deterministic proof that the sign/read/admission path NEVER hangs on a stalled base.update() —
// which is the live tabertron failure (get-state hung >8s; the sign loop's first read hung, so the
// re-admission doorbell never re-rang). We simulate the stall by replacing base.update() with a
// promise that NEVER resolves, then assert every liveness-critical call still returns quickly and
// the doorbell block is still appended. This isolates the FIX from the (hard-to-reproduce) exact
// convergence fork that causes the real stall — the guarantee we ship is "a stalled drain can't
// freeze these paths", and that is what this proves.
import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import b4a from 'b4a'
import { StateBaseV3 } from '../lib/state-base.js'
import { Lease } from '../lib/lease.js'
import { RepoBase } from '../lib/repo-base.js'
import { makeAttest, makeVerifyAttest, attestDeviceChain, deviceKeetKeyPair, rootIdentityPublicKey } from '../lib/membership.js'

const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DEVA = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
const DEVB = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const tmp = () => { const d = '/tmp/hpp-phase2-nb-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }

// Resolve p, but fail the assertion if it takes longer than ms (i.e. it HUNG).
async function within (t, label, p, ms) {
  const HUNG = Symbol('hung'); const t0 = Date.now()
  const r = await Promise.race([p.then(v => ({ v })).catch(e => ({ err: e })), new Promise(res => setTimeout(() => res(HUNG), ms))])
  const dt = Date.now() - t0
  t.ok(r !== HUNG, `${label} returned in ${dt}ms (< ${ms}ms) — did not hang`)
  return r === HUNG ? undefined : (r.err ? { threw: r.err } : r.v)
}

test('a stalled base.update() cannot freeze the read / lease / doorbell path', { timeout: 60000 }, async (t) => {
  const rootPub = await rootIdentityPublicKey(ROOT)
  const da = await deviceKeetKeyPair(DEVA); const pa = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: da.publicKey })
  const db = await deviceKeetKeyPair(DEVB); const pb = await attestDeviceChain({ rootMnemonic: ROOT, devicePublicKey: db.publicKey })
  // member = the NOT-lowest key, so it opens writable=false (member, no indexer present) — exactly
  // the un-converged plain-writer posture tabertron reboots into.
  let A = da, B = db
  if (b4a.compare(A.publicKey, B.publicKey) > 0) { const t2 = A; A = B; B = t2 } // ensure A<B so B = higher key => member (opens writable=false)
  const members = [
    { key: b4a.toString(A.publicKey, 'hex'), role: 1 },
    { key: b4a.toString(B.publicKey, 'hex'), role: 1 }
  ]
  const roomSeed = crypto.randomBytes(32); const roomKey = crypto.randomBytes(32); const encryptionKey = crypto.randomBytes(32)
  const sb = new StateBaseV3(tmp(), roomKey, {
    roomSeed, encryptionKey, network: false, // no swarm — isolated, so it can never converge (writable stays false)
    selfKey: B.publicKey, attest: makeAttest(B, pb), verifyAttest: makeVerifyAttest(rootPub),
    getMembers: async () => members
  })
  await sb.ready()
  t.teardown(async () => { try { await sb.close() } catch {} })
  t.absent(sb.base.writable, 'B opens writable=false (un-converged plain writer)')

  // ── HARD-STALL base.update(): replace it with a promise that never resolves. Any code path that
  //    AWAITS base.update() directly (the pre-fix bug) now hangs forever; the fix must route through
  //    _updateBounded / snapshot reads so these calls still return.
  const realUpdate = sb.base.update.bind(sb.base)
  sb.base.update = () => new Promise(() => {}) // never resolves
  t.teardown(() => { try { sb.base.update = realUpdate } catch {} })

  // control: the explicitly non-blocking snapshot reads (already shipped) must be instant
  await within(t, 'sb.getNow()', sb.getNow('nope'), 2000)
  await within(t, 'sb.listNow()', sb.listNow(''), 2000)

  // the sign-loop first read: Lease.current() → must be snapshot-based, not base.update()
  const lease = new Lease(sb, { self: b4a.toString(B.publicKey, 'hex') })
  await within(t, 'lease.current()', lease.current(), 2000)
  await within(t, 'lease.holds()', lease.holds(), 2000)

  // get-state's record read: RepoBase.listRecords({ now:true }) → snapshot, not base.update()
  const repo = new RepoBase(sb, { did: 'did:plc:test' })
  await within(t, 'repo.listRecords({now:true})', repo.listRecords(null, { now: true }), 2000)

  // THE DOORBELL: requestWriter must (a) append the content-free optimistic admit block and
  // (b) RETURN within its timeout even though base.update() is hung — so the sign loop keeps
  // re-ringing instead of freezing. It will not become writable (isolated, no indexer), so it
  // loops the full timeoutMs then returns.
  const lenBefore = sb.base.local.length
  const r = await within(t, 'requestWriter({timeoutMs:1500})', sb.requestWriter({ timeoutMs: 1500 }), 8000)
  t.ok(r && r.writable === false, 'requestWriter returned writable=false (no indexer) — but returned, not hung')
  t.ok(sb.base.local.length > lenBefore, `doorbell optimistic block was appended (local.length ${lenBefore} -> ${sb.base.local.length})`)

  // ensureAdmitted (the boot re-admission trigger) must also return, and be guarded (2nd concurrent
  // call is a no-op that returns immediately).
  await within(t, 'ensureAdmitted()', sb.ensureAdmitted({ timeoutMs: 1200 }), 8000)
})