#!/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) })