OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / test / connection-reuse.test.js

126 lines · javascript ref:
#!/usr/bin/env node

/**
 * connection-reuse.test.js — Guards against the CogServer thread-stack leak
 *
 * CogServer (NetworkServer.cc:169) spawns a joinable std::thread per accepted
 * connection and never joins it until shutdown, so glibc can never unmap that
 * thread's 8MB stack. Every connection permanently costs the server ~2 VMAs,
 * ~16KB RSS and ~8MB of address space. A client that opens one connection per
 * Scheme expression therefore leaks the server to death.
 *
 * This test asserts two things:
 *   1. CORRECTNESS — concurrent evalScheme() calls each get their own answer.
 *      (A shared connection must serialize requests; interleaving them on one
 *      socket would scramble the replies.)
 *   2. REUSE — a burst of evals costs the server at most one connection.
 *
 * Usage: node atomspace/test/connection-reuse.test.js
 * Requires: CogServer running on port 17001
 */

const fs = require('fs')
const { isCogServerUp, evalScheme } = require('../cogserver')

// One connection = ~1.92 VMAs (8MB stack + guard page). Allow one connection
// plus slack for unrelated server activity during the run.
const MAX_LEAKED_VMAS = 6

// cogserver renames its main thread to "cogserv:loop" via prctl, so comm-based
// lookup (pgrep -x cogserver) finds nothing. Match on argv[0] instead.
function cogserverPid () {
  for (const entry of fs.readdirSync('/proc')) {
    if (!/^\d+$/.test(entry)) continue
    try {
      const argv0 = fs.readFileSync(`/proc/${entry}/cmdline`, 'utf8').split('\0')[0]
      if (argv0 && argv0.split('/').pop() === 'cogserver') return entry
    } catch { /* process exited, or not ours */ }
  }
  return null
}

function vmaCount (pid) {
  try {
    return fs.readFileSync(`/proc/${pid}/maps`, 'utf8').split('\n').length - 1
  } catch { return null }
}

// Distinct answers, so a scrambled reply cannot accidentally pass.
const CASES = [
  ['(+ 1 1)', '2'],
  ['(* 6 7)', '42'],
  ['(- 10 3)', '7'],
  ['(+ 100 23)', '123'],
  ['(* 11 11)', '121'],
  ['(- 50 8)', '42'],
  ['(+ 2 3)', '5'],
  ['(* 9 9)', '81'],
  ['(- 99 9)', '90'],
  ['(+ 7 0)', '7']
]

function extract (raw) {
  // Guile echoes results as "$N = VALUE"; tolerate a bare value too.
  if (raw === null || raw === undefined) return null
  const m = String(raw).match(/\$\d+\s*=\s*(\S+)/)
  return m ? m[1] : String(raw).trim()
}

async function main () {
  const pid = cogserverPid()
  if (!pid) {
    console.log('SKIP: cogserver not running')
    process.exit(0)
  }
  if (!await isCogServerUp()) {
    console.log('SKIP: cogserver not reachable on 17001')
    process.exit(0)
  }

  const before = vmaCount(pid)
  let failures = 0

  // Sequential calls.
  for (const [code, want] of CASES.slice(0, 5)) {
    const got = extract(await evalScheme(code))
    if (got !== want) {
      console.log(`  FAIL sequential ${code} -> got ${JSON.stringify(got)}, want ${want}`)
      failures++
    }
  }

  // Concurrent calls — the case that breaks a naive shared socket.
  const concurrent = CASES.slice(5)
  const results = await Promise.all(concurrent.map(([code]) => evalScheme(code)))
  concurrent.forEach(([code, want], i) => {
    const got = extract(results[i])
    if (got !== want) {
      console.log(`  FAIL concurrent ${code} -> got ${JSON.stringify(got)}, want ${want}`)
      failures++
    }
  })

  const after = vmaCount(pid)
  const leaked = after - before
  const evals = CASES.length

  console.log(`\ncogserver pid ${pid}`)
  console.log(`  evals issued  : ${evals} (+1 isCogServerUp probe)`)
  console.log(`  VMAs before   : ${before.toLocaleString()}`)
  console.log(`  VMAs after    : ${after.toLocaleString()}`)
  console.log(`  VMAs leaked   : ${leaked}  (~${(leaked / 1.92).toFixed(1)} connections)`)
  console.log(`  budget        : <= ${MAX_LEAKED_VMAS}`)
  console.log(`  correctness   : ${failures === 0 ? 'all answers correct' : failures + ' wrong answer(s)'}`)

  const leakOk = leaked <= MAX_LEAKED_VMAS
  if (!leakOk) console.log(`\nFAIL: leaked ${leaked} VMAs, expected <= ${MAX_LEAKED_VMAS} (one connection reused)`)
  if (failures === 0 && leakOk) {
    console.log('\nPASS')
    process.exit(0)
  }
  console.log('\nFAIL')
  process.exit(1)
}

main().catch((e) => { console.error('ERROR', e); process.exit(1) })