OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / tools / probe-room.mjs

71 lines · javascript ref:
// probe-room.mjs — classify ONE room's StateBase health (run one-per-process so
// a corruption abort is isolated). Opens via the SAME router path the game uses
// (so roomSeed/encryptionKey/membership wired identically), then reports the
// signals that distinguish the failure classes:
//   • opens clean vs throws (on-disk corestore corruption)
//   • base.writable + writer/indexer count
//   • __members__ projection, and whether SELF is in it (deadlock signature)
//   • entry count
//
//   bare tools/probe-room.mjs --storage <root> --room <roomId>
import { startDaemon } from '../index.js'
import b4a from 'b4a'

const args = Bare.argv.slice(2)
const gi = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null }
const storage = gi('--storage'); let roomId = gi('--room'); const title = gi('--title')
if (!storage || (!roomId && !title)) { console.log('usage: bare tools/probe-room.mjs --storage <root> (--room <roomId> | --title <name>)'); Bare.exit(1) }

setTimeout(() => { console.error('VERDICT: TIMEOUT(90s) — possible admit-loop/hang'); Bare.exit(2) }, 90000)
Bare.on('unhandledRejection', (err) => { console.error('VERDICT: CRASH(unhandledRejection):', err && err.message ? err.message : String(err)); Bare.exit(7) })

const d = await startDaemon({ storage, socketPath: '/tmp/keet-probe.sock', pidFile: '/tmp/keet-probe.pid', debug: false })
await d.core.backendAPI.ready()

if (!roomId && title) {
  const rooms = await d.router._rooms()
  const hit = rooms.find(r => r.title === title)
  if (!hit) { console.error('no room titled', JSON.stringify(title)); Bare.exit(1) }
  roomId = hit.roomId
}

let sb
try {
  sb = await d.router._getStateBase(roomId)
} catch (err) {
  console.error('VERDICT: OPEN_THREW:', err && err.message ? err.message : String(err))
  Bare.exit(6)
}

const self = sb._selfKey ? sb._selfKey.toString('hex') : null
let writers = '?'; let indexers = '?'
try { const b = sb.base; writers = (b.activeWriters && b.activeWriters.size) ?? '?'; indexers = (b.linearizer && b.linearizer.indexers && b.linearizer.indexers.length) ?? '?' } catch {}

let members = null
try {
  const all = await sb.list('')
  if (all.__members__ !== undefined) members = typeof all.__members__ === 'string' ? JSON.parse(all.__members__) : all.__members__
  var entryCount = Object.keys(all).filter(k => k !== '__members__').length
} catch (e) { console.error('list() threw:', e.message) }

const memberKeys = members ? Object.keys(members) : []
const selfInMembers = self && members ? (members[self] != null || members[self.toLowerCase()] != null) : false

console.log('================ PROBE: %s ================', roomId.slice(0, 16))
console.log('self key     :', self ? self.slice(0, 16) : '(none)')
console.log('opened       : OK')
console.log('writable     :', sb.writable)
console.log('writers/idx  :', writers, '/', indexers)
console.log('entries      :', entryCount ?? '?')
console.log('__members__  :', memberKeys.length, 'keys:', memberKeys.map(k => k.slice(0, 8)).join(','))
console.log('SELF in mem  :', selfInMembers)

// Classification
let verdict
if (!members) verdict = 'NO_MEMBERS_PROJECTION (never had a writer project __members__)'
else if (!selfInMembers) verdict = 'MEMBERSHIP_DEADLOCK_RISK (self NOT in __members__ → member set() loops, never commits)'
else if (sb.writable) verdict = 'HEALTHY (self in members + writable)'
else verdict = 'ADMISSION_NOT_LANDED (self in members but not writable → optimistic admit not acked by indexer)'
console.log('VERDICT      :', verdict)
Bare.exit(0)