| … | … | @@ -0,0 +1,125 @@ |
| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | /** |
| 4 | * connection-reuse.test.js — Guards against the CogServer thread-stack leak |
| 5 | * |
| 6 | * CogServer (NetworkServer.cc:169) spawns a joinable std::thread per accepted |
| 7 | * connection and never joins it until shutdown, so glibc can never unmap that |
| 8 | * thread's 8MB stack. Every connection permanently costs the server ~2 VMAs, |
| 9 | * ~16KB RSS and ~8MB of address space. A client that opens one connection per |
| 10 | * Scheme expression therefore leaks the server to death. |
| 11 | * |
| 12 | * This test asserts two things: |
| 13 | * 1. CORRECTNESS — concurrent evalScheme() calls each get their own answer. |
| 14 | * (A shared connection must serialize requests; interleaving them on one |
| 15 | * socket would scramble the replies.) |
| 16 | * 2. REUSE — a burst of evals costs the server at most one connection. |
| 17 | * |
| 18 | * Usage: node atomspace/test/connection-reuse.test.js |
| 19 | * Requires: CogServer running on port 17001 |
| 20 | */ |
| 21 | |
| 22 | const fs = require('fs') |
| 23 | const { isCogServerUp, evalScheme } = require('../cogserver') |
| 24 | |
| 25 | // One connection = ~1.92 VMAs (8MB stack + guard page). Allow one connection |
| 26 | // plus slack for unrelated server activity during the run. |
| 27 | const MAX_LEAKED_VMAS = 6 |
| 28 | |
| 29 | // cogserver renames its main thread to "cogserv:loop" via prctl, so comm-based |
| 30 | // lookup (pgrep -x cogserver) finds nothing. Match on argv[0] instead. |
| 31 | function cogserverPid () { |
| 32 | for (const entry of fs.readdirSync('/proc')) { |
| 33 | if (!/^\d+$/.test(entry)) continue |
| 34 | try { |
| 35 | const argv0 = fs.readFileSync(`/proc/${entry}/cmdline`, 'utf8').split('\0')[0] |
| 36 | if (argv0 && argv0.split('/').pop() === 'cogserver') return entry |
| 37 | } catch { /* process exited, or not ours */ } |
| 38 | } |
| 39 | return null |
| 40 | } |
| 41 | |
| 42 | function vmaCount (pid) { |
| 43 | try { |
| 44 | return fs.readFileSync(`/proc/${pid}/maps`, 'utf8').split('\n').length - 1 |
| 45 | } catch { return null } |
| 46 | } |
| 47 | |
| 48 | // Distinct answers, so a scrambled reply cannot accidentally pass. |
| 49 | const CASES = [ |
| 50 | ['(+ 1 1)', '2'], |
| 51 | ['(* 6 7)', '42'], |
| 52 | ['(- 10 3)', '7'], |
| 53 | ['(+ 100 23)', '123'], |
| 54 | ['(* 11 11)', '121'], |
| 55 | ['(- 50 8)', '42'], |
| 56 | ['(+ 2 3)', '5'], |
| 57 | ['(* 9 9)', '81'], |
| 58 | ['(- 99 9)', '90'], |
| 59 | ['(+ 7 0)', '7'] |
| 60 | ] |
| 61 | |
| 62 | function extract (raw) { |
| 63 | // Guile echoes results as "$N = VALUE"; tolerate a bare value too. |
| 64 | if (raw === null || raw === undefined) return null |
| 65 | const m = String(raw).match(/\$\d+\s*=\s*(\S+)/) |
| 66 | return m ? m[1] : String(raw).trim() |
| 67 | } |
| 68 | |
| 69 | async function main () { |
| 70 | const pid = cogserverPid() |
| 71 | if (!pid) { |
| 72 | console.log('SKIP: cogserver not running') |
| 73 | process.exit(0) |
| 74 | } |
| 75 | if (!await isCogServerUp()) { |
| 76 | console.log('SKIP: cogserver not reachable on 17001') |
| 77 | process.exit(0) |
| 78 | } |
| 79 | |
| 80 | const before = vmaCount(pid) |
| 81 | let failures = 0 |
| 82 | |
| 83 | // Sequential calls. |
| 84 | for (const [code, want] of CASES.slice(0, 5)) { |
| 85 | const got = extract(await evalScheme(code)) |
| 86 | if (got !== want) { |
| 87 | console.log(` FAIL sequential ${code} -> got ${JSON.stringify(got)}, want ${want}`) |
| 88 | failures++ |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | // Concurrent calls — the case that breaks a naive shared socket. |
| 93 | const concurrent = CASES.slice(5) |
| 94 | const results = await Promise.all(concurrent.map(([code]) => evalScheme(code))) |
| 95 | concurrent.forEach(([code, want], i) => { |
| 96 | const got = extract(results[i]) |
| 97 | if (got !== want) { |
| 98 | console.log(` FAIL concurrent ${code} -> got ${JSON.stringify(got)}, want ${want}`) |
| 99 | failures++ |
| 100 | } |
| 101 | }) |
| 102 | |
| 103 | const after = vmaCount(pid) |
| 104 | const leaked = after - before |
| 105 | const evals = CASES.length |
| 106 | |
| 107 | console.log(`\ncogserver pid ${pid}`) |
| 108 | console.log(` evals issued : ${evals} (+1 isCogServerUp probe)`) |
| 109 | console.log(` VMAs before : ${before.toLocaleString()}`) |
| 110 | console.log(` VMAs after : ${after.toLocaleString()}`) |
| 111 | console.log(` VMAs leaked : ${leaked} (~${(leaked / 1.92).toFixed(1)} connections)`) |
| 112 | console.log(` budget : <= ${MAX_LEAKED_VMAS}`) |
| 113 | console.log(` correctness : ${failures === 0 ? 'all answers correct' : failures + ' wrong answer(s)'}`) |
| 114 | |
| 115 | const leakOk = leaked <= MAX_LEAKED_VMAS |
| 116 | if (!leakOk) console.log(`\nFAIL: leaked ${leaked} VMAs, expected <= ${MAX_LEAKED_VMAS} (one connection reused)`) |
| 117 | if (failures === 0 && leakOk) { |
| 118 | console.log('\nPASS') |
| 119 | process.exit(0) |
| 120 | } |
| 121 | console.log('\nFAIL') |
| 122 | process.exit(1) |
| 123 | } |
| 124 | |
| 125 | main().catch((e) => { console.error('ERROR', e); process.exit(1) }) |