1
0
mirror of https://github.com/sasjs/server.git synced 2026-07-23 21:25:29 +00:00

fix(api): harden mock SAS executable and timeouts for CI

- wrap the file read/write in a small retry() helper instead of
  letting an uncaught exception (e.g. a rename racing an existsSync
  check) crash the process with a non-zero exit indistinguishable
  from a genuine SAS failure
- bump the internal give-up deadline from 1500ms to 8000ms
- bump the corresponding Jest test timeouts from 15000ms to 30000ms to match
This commit is contained in:
YuryShkoda
2026-07-14 08:41:35 +03:00
parent f84af4ac06
commit 67fce475a3
2 changed files with 45 additions and 10 deletions
+2 -2
View File
@@ -89,7 +89,7 @@ describe('code', () => {
expect(response.text).toEqual( expect(response.text).toEqual(
expect.stringContaining('mock SAS execution') expect.stringContaining('mock SAS execution')
) )
}, 15000) }, 30000)
it('returns a prompt 400 (not a hang) with the complete log when the SAS session fails (%abort;)', async () => { it('returns a prompt 400 (not a hang) with the complete log when the SAS session fails (%abort;)', async () => {
const response = await request(app) const response = await request(app)
@@ -105,7 +105,7 @@ describe('code', () => {
expect(response.body.log).toEqual( expect(response.body.log).toEqual(
expect.stringContaining('mock SAS execution') expect.stringContaining('mock SAS execution')
) )
}, 15000) }, 30000)
}) })
}) })
+43 -8
View File
@@ -26,11 +26,15 @@ const arg = (flag) => {
const sysin = arg('-SYSIN') const sysin = arg('-SYSIN')
const logPath = arg('-LOG') const logPath = arg('-LOG')
// give up waiting for the real program after this long, so an unused // Give up waiting for the real program after this long, so an unused
// pre-warmed session (sasjs pools up to 3 ready sessions) doesn't linger // pre-warmed session (sasjs pools up to 3 ready sessions) doesn't linger
// forever and keep a test process alive. Short, since in tests the real // forever and keep a test process alive. Generous rather than tight: CI
// signal (if this session is the one actually used) arrives near-instantly. // runners are frequently slower/more contended than a local dev machine
const GIVE_UP_AFTER_MS = 1500 // (shared CPU, coverage instrumentation on the Node side slowing down the
// round trip this process is waiting on), and this cost is only ever paid
// by sessions nothing is actually waiting on - the session actually used
// by a request gets its real code written far sooner than this in practice.
const GIVE_UP_AFTER_MS = 8000
const sleepSync = (ms) => { const sleepSync = (ms) => {
const until = Date.now() + ms const until = Date.now() + ms
@@ -40,8 +44,30 @@ const sleepSync = (ms) => {
} }
} }
// Any of the filesystem calls below can legitimately race against
// processProgram's own write-then-rename (e.g. existsSync sees the file,
// then a rename mid-flight makes the following read miss). Treat that as
// "not ready yet" and retry a few times, rather than letting an uncaught
// exception crash this process with a non-zero exit - which would be
// indistinguishable, to the Node side, from a genuine SAS failure.
const retry = (fn, attempts = 5, delayMs = 20) => {
for (let i = 0; i < attempts; i++) {
try {
return { ok: true, value: fn() }
} catch (err) {
if (i === attempts - 1) return { ok: false, error: err }
sleepSync(delayMs)
}
}
}
// 1. remove the dummy SYSIN, signalling "session ready" to waitForSession() // 1. remove the dummy SYSIN, signalling "session ready" to waitForSession()
if (sysin && fs.existsSync(sysin)) fs.unlinkSync(sysin) if (sysin) {
retry(() => {
if (fs.existsSync(sysin)) fs.unlinkSync(sysin)
})
}
// 2. wait for the real program to be written back to the same path // 2. wait for the real program to be written back to the same path
const deadline = Date.now() + GIVE_UP_AFTER_MS const deadline = Date.now() + GIVE_UP_AFTER_MS
@@ -54,12 +80,21 @@ while (!sysin || !fs.existsSync(sysin)) {
// small settle delay, mirroring the real autoexec's sleep(0.01,1) after // small settle delay, mirroring the real autoexec's sleep(0.01,1) after
// detecting the file, so a fast-moving rename isn't read mid-write // detecting the file, so a fast-moving rename isn't read mid-write
sleepSync(20) sleepSync(50)
const code = fs.readFileSync(sysin, 'utf-8') const readResult = retry(() => fs.readFileSync(sysin, 'utf-8'))
if (!readResult.ok) {
process.stderr.write(
`mockSas.js: failed to read ${sysin}: ${readResult.error}\n`
)
process.exit(1)
}
const code = readResult.value
if (logPath) { if (logPath) {
fs.writeFileSync(logPath, `NOTE: mock SAS execution\n${code}\n`) retry(() => fs.writeFileSync(logPath, `NOTE: mock SAS execution\n${code}\n`))
} }
if (code.includes('%abort;')) { if (code.includes('%abort;')) {