Skip to content

Commit c8f9852

Browse files
committed
feat(clsi): add compile heartbeat to avoid CDN timeout
1 parent 1be911e commit c8f9852

File tree

2 files changed

+50
-0
lines changed

2 files changed

+50
-0
lines changed

services/clsi/app/js/CompileController.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,56 @@ const Errors = require('./Errors')
99
const { notifyCLSICacheAboutBuild } = require('./CLSICacheHandler')
1010

1111
let lastSuccessfulCompileTimestamp = 0
12+
const COMPILE_HEARTBEAT_INTERVAL_MS =
13+
Settings.clsi?.compileHeartbeatMs ?? 10 * 1000
1214

1315
function timeSinceLastSuccessfulCompile() {
1416
return Date.now() - lastSuccessfulCompileTimestamp
1517
}
1618

19+
// Send interim 102 responses so upstream proxies/CDNs don't drop long-running compile requests.
20+
// We use writeProcessing() because it does not lock in the final HTTP status code.
21+
function startCompileHeartbeat(res) {
22+
const writeProcessing = res.writeProcessing?.bind(res)
23+
if (!writeProcessing || COMPILE_HEARTBEAT_INTERVAL_MS <= 0) {
24+
return () => {}
25+
}
26+
27+
let stopped = false
28+
let timer
29+
30+
const stop = () => {
31+
if (stopped) return
32+
stopped = true
33+
if (timer) clearInterval(timer)
34+
if (typeof res.removeListener === 'function') {
35+
res.removeListener('close', stop)
36+
res.removeListener('finish', stop)
37+
}
38+
}
39+
40+
const ping = () => {
41+
if (stopped || res.writableEnded) {
42+
stop()
43+
return
44+
}
45+
try {
46+
writeProcessing()
47+
} catch (err) {
48+
logger.debug({ err }, 'failed to send compile heartbeat')
49+
stop()
50+
}
51+
}
52+
53+
ping()
54+
timer = setInterval(ping, COMPILE_HEARTBEAT_INTERVAL_MS)
55+
if (typeof res.on === 'function') {
56+
res.on('close', stop)
57+
res.on('finish', stop)
58+
}
59+
return stop
60+
}
61+
1762
function compile(req, res, next) {
1863
const timer = new Metrics.Timer('compile-request')
1964
RequestParser.parse(req.body, function (error, request) {
@@ -31,13 +76,15 @@ function compile(req, res, next) {
3176
if (error) {
3277
return next(error)
3378
}
79+
const stopHeartbeat = startCompileHeartbeat(res)
3480
const stats = {}
3581
const timings = {}
3682
CompileManager.doCompileWithLock(
3783
request,
3884
stats,
3985
timings,
4086
(error, result) => {
87+
stopHeartbeat()
4188
let { buildId, outputFiles } = result || {}
4289
let code, status
4390
if (outputFiles == null) {

services/clsi/config/settings.defaults.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ module.exports.clsi = {
121121
optimiseInDocker: true,
122122
expireProjectAfterIdleMs: 24 * 60 * 60 * 1000,
123123
checkProjectsIntervalMs: 10 * 60 * 1000,
124+
// Interval (ms) to send HTTP 102 Processing during long compiles to keep proxies/CDNs alive
125+
compileHeartbeatMs:
126+
parseInt(process.env.COMPILE_HEARTBEAT_MS, 10) || 10 * 1000,
124127
// Validate host sandboxed compiles path from Docker daemon perspective when the
125128
// configured host path is not visible inside this container. This is useful in
126129
// development setups where host paths are mounted differently; set to 'false'

0 commit comments

Comments
 (0)