Make keepalive registration self-healing
| @@ -55,6 +55,8 @@ let jobRegistered = false; | ||
| 55 | 55 | let refreshPromise = null; |
| 56 | 56 | let refreshRequested = false; |
| 57 | 57 | let refreshTimer = null; |
| 58 | +let generationRefreshTimer = null; | |
| 59 | +let retryTimer = null; | |
| 58 | 60 | let preparingJob = false; |
| 59 | 61 | let lifecycleVersion = 0; |
| 60 | 62 | // Whether keepalive is "armed" for the CURRENT chat. It stays dormant until the user sends a new |
| @@ -111,6 +113,7 @@ async function postKeepaliveBackend(path, body) { | ||
| 111 | 113 | async function stopBackendJob() { |
| 112 | 114 | lifecycleVersion++; |
| 113 | 115 | jobRegistered = false; |
| 116 | + clearTimeout(retryTimer); | |
| 114 | 117 | try { |
| 115 | 118 | await postKeepaliveBackend('stop', {}); |
| 116 | 119 | } catch (error) { |
| @@ -119,7 +122,14 @@ async function stopBackendJob() { | ||
| 119 | 122 | } |
| 120 | 123 | |
| 121 | 124 | async function sendBackendHeartbeat(activity = false) { |
| 122 | 125 | if (!jobRegistered || !armed || !isKeepaliveEnabledForActive()) { |
| 126 | + return; | |
| 127 | + } | |
| 128 | + // Registration is normally created as soon as a response finishes. If that one-shot event | |
| 129 | + // was missed (or registration failed), the regular heartbeat must self-heal instead of doing | |
| 130 | + // nothing forever. | |
| 131 | + if (!jobRegistered) { | |
| 132 | + queueJobRefresh(); | |
| 123 | 133 | return; |
| 124 | 134 | } |
| 125 | 135 | try { |
| @@ -278,11 +288,13 @@ async function registerBackendJob() { | ||
| 278 | 288 | await stopBackendJob(); |
| 279 | 289 | return; |
| 280 | 290 | } |
| 291 | + clearTimeout(retryTimer); | |
| 281 | 292 | jobRegistered = true; |
| 282 | 293 | } catch (error) { |
| 283 | 294 | jobRegistered = false; |
| 284 | 295 | console.debug('[Keepalive] Could not register backend job:', error); |
| 285 | 296 | setTimeoutclearTimeout(queueJobRefresh, RETRY_INTERVAL_MSretryTimer); |
| 297 | + retryTimer = setTimeout(queueJobRefresh, RETRY_INTERVAL_MS); | |
| 286 | 298 | } |
| 287 | 299 | } |
| 288 | 300 | |
| @@ -306,6 +318,15 @@ function scheduleJobRefresh() { | ||
| 306 | 318 | refreshTimer = setTimeout(queueJobRefresh, SETTINGS_REFRESH_DELAY_MS); |
| 307 | 319 | } |
| 308 | 320 | |
| 321 | +function scheduleGenerationJobRefresh() { | |
| 322 | + clearTimeout(generationRefreshTimer); | |
| 323 | + generationRefreshTimer = setTimeout(() => { | |
| 324 | + if (armed && isKeepaliveEnabledForActive()) { | |
| 325 | + queueJobRefresh(); | |
| 326 | + } | |
| 327 | + }, SETTINGS_REFRESH_DELAY_MS); | |
| 328 | +} | |
| 329 | + | |
| 309 | 330 | function setupHeartbeatLoop() { |
| 310 | 331 | clearInterval(heartbeatTimer); |
| 311 | 332 | heartbeatTimer = setInterval(() => void sendBackendHeartbeat(), HEARTBEAT_INTERVAL_MS); |
| @@ -441,9 +462,12 @@ function onGenerationStarted(_type, _options, dryRun) { | ||
| 441 | 462 | |
| 442 | 463 | function onGenerationEnded() { |
| 443 | 464 | markActivity(); |
| 444 | - if (armed && isKeepaliveEnabledForActive()) { | |
| 465 | + scheduleGenerationJobRefresh(); | |
| 445 | - queueJobRefresh(); | |
| 466 | +} | |
| 446 | - } | |
| 467 | + | |
| 468 | +function onMessageReceived() { | |
| 469 | + markActivity(); | |
| 470 | + scheduleGenerationJobRefresh(); | |
| 447 | 471 | } |
| 448 | 472 | |
| 449 | 473 | function onChatContentChanged() { |
| @@ -475,7 +499,7 @@ async function init() { | ||
| 475 | 499 | // Activity resets the backend deadline but never arms keepalive on its own. A completed |
| 476 | 500 | // generation or a chat edit also refreshes the stored request so the server always replays |
| 477 | 501 | // the latest prompt assembled by the UI. |
| 478 | 502 | eventSource.on(event_types.MESSAGE_RECEIVED, markActivityonMessageReceived); |
| 479 | 503 | eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted); |
| 480 | 504 | eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded); |
| 481 | 505 | eventSource.on(event_types.MESSAGE_EDITED, onChatContentChanged); |
| @@ -152,6 +152,7 @@ async function dispatchKeepaliveRequest(job) { | ||
| 152 | 152 | const timeout = setTimeout(() => controller.abort(new Error('Keepalive request timed out')), 120000); |
| 153 | 153 | timeout.unref?.(); |
| 154 | 154 | try { |
| 155 | + console.info(`[Keepalive] Dispatching job ${job.id} to ${job.endpoint}`); | |
| 155 | 156 | const response = await fetch(`${job.request.origin}${job.endpoint}`, { |
| 156 | 157 | method: 'POST', |
| 157 | 158 | headers: job.request.headers, |
| @@ -163,6 +164,7 @@ async function dispatchKeepaliveRequest(job) { | ||
| 163 | 164 | if (!response.ok) { |
| 164 | 165 | throw new Error(`HTTP ${response.status}`); |
| 165 | 166 | } |
| 167 | + console.info(`[Keepalive] Job ${job.id} completed successfully`); | |
| 166 | 168 | } finally { |
| 167 | 169 | clearTimeout(timeout); |
| 168 | 170 | } |
| @@ -234,6 +236,7 @@ router.post('/register', (request, response) => { | ||
| 234 | 236 | delayMs: request.body.delayMs, |
| 235 | 237 | request: getReplayRequest(request), |
| 236 | 238 | }); |
| 239 | + console.info(`[Keepalive] Registered job ${id}; first run in ${job.nextRun - Date.now()}ms, interval ${job.intervalMs}ms`); | |
| 237 | 240 | return response.send({ ok: true, nextRun: job.nextRun, leaseMs: FRONTEND_LEASE_MS }); |
| 238 | 241 | } catch (error) { |
| 239 | 242 | return response.status(400).send({ error: String(error.message || error) }); |