Make keepalive registration self-healing

d4a0852ee81d3a22b6339426124c3783e2c37481

permissionBRICK <40219477+permissionBRICK@users.noreply.github.com>

2 files changed, +32 -5Showing whitespace changes
public/scripts/extensions/keepalive/index.js+29 -5
@@ -55,6 +55,8 @@ let jobRegistered = false;
55let refreshPromise = null;55let refreshPromise = null;
56let refreshRequested = false;56let refreshRequested = false;
57let refreshTimer = null;57let refreshTimer = null;
58let generationRefreshTimer = null;
59let retryTimer = null;
58let preparingJob = false;60let preparingJob = false;
59let lifecycleVersion = 0;61let lifecycleVersion = 0;
60// Whether keepalive is "armed" for the CURRENT chat. It stays dormant until the user sends a new62// 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) {
111async function stopBackendJob() {113async function stopBackendJob() {
112 lifecycleVersion++;114 lifecycleVersion++;
113 jobRegistered = false;115 jobRegistered = false;
116 clearTimeout(retryTimer);
114 try {117 try {
115 await postKeepaliveBackend('stop', {});118 await postKeepaliveBackend('stop', {});
116 } catch (error) {119 } catch (error) {
@@ -119,7 +122,14 @@ async function stopBackendJob() {
119}122}
120123
121async function sendBackendHeartbeat(activity = false) {124async function sendBackendHeartbeat(activity = false) {
122 if (!jobRegistered || !armed || !isKeepaliveEnabledForActive()) {125 if (!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 return;133 return;
124 }134 }
125 try {135 try {
@@ -278,11 +288,13 @@ async function registerBackendJob() {
278 await stopBackendJob();288 await stopBackendJob();
279 return;289 return;
280 }290 }
291 clearTimeout(retryTimer);
281 jobRegistered = true;292 jobRegistered = true;
282 } catch (error) {293 } catch (error) {
283 jobRegistered = false;294 jobRegistered = false;
284 console.debug('[Keepalive] Could not register backend job:', error);295 console.debug('[Keepalive] Could not register backend job:', error);
285 setTimeout(queueJobRefresh, RETRY_INTERVAL_MS);296 clearTimeout(retryTimer);
297 retryTimer = setTimeout(queueJobRefresh, RETRY_INTERVAL_MS);
286 }298 }
287}299}
288300
@@ -306,6 +318,15 @@ function scheduleJobRefresh() {
306 refreshTimer = setTimeout(queueJobRefresh, SETTINGS_REFRESH_DELAY_MS);318 refreshTimer = setTimeout(queueJobRefresh, SETTINGS_REFRESH_DELAY_MS);
307}319}
308320
321function scheduleGenerationJobRefresh() {
322 clearTimeout(generationRefreshTimer);
323 generationRefreshTimer = setTimeout(() => {
324 if (armed && isKeepaliveEnabledForActive()) {
325 queueJobRefresh();
326 }
327 }, SETTINGS_REFRESH_DELAY_MS);
328}
329
309function setupHeartbeatLoop() {330function setupHeartbeatLoop() {
310 clearInterval(heartbeatTimer);331 clearInterval(heartbeatTimer);
311 heartbeatTimer = setInterval(() => void sendBackendHeartbeat(), HEARTBEAT_INTERVAL_MS);332 heartbeatTimer = setInterval(() => void sendBackendHeartbeat(), HEARTBEAT_INTERVAL_MS);
@@ -441,9 +462,12 @@ function onGenerationStarted(_type, _options, dryRun) {
441462
442function onGenerationEnded() {463function onGenerationEnded() {
443 markActivity();464 markActivity();
444 if (armed && isKeepaliveEnabledForActive()) {465 scheduleGenerationJobRefresh();
445 queueJobRefresh();
446}466}
467
468function onMessageReceived() {
469 markActivity();
470 scheduleGenerationJobRefresh();
447}471}
448472
449function onChatContentChanged() {473function onChatContentChanged() {
@@ -475,7 +499,7 @@ async function init() {
475 // Activity resets the backend deadline but never arms keepalive on its own. A completed499 // Activity resets the backend deadline but never arms keepalive on its own. A completed
476 // generation or a chat edit also refreshes the stored request so the server always replays500 // generation or a chat edit also refreshes the stored request so the server always replays
477 // the latest prompt assembled by the UI.501 // the latest prompt assembled by the UI.
478 eventSource.on(event_types.MESSAGE_RECEIVED, markActivity);502 eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
479 eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);503 eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);
480 eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded);504 eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded);
481 eventSource.on(event_types.MESSAGE_EDITED, onChatContentChanged);505 eventSource.on(event_types.MESSAGE_EDITED, onChatContentChanged);
src/endpoints/keepalive.js+3 -0
@@ -152,6 +152,7 @@ async function dispatchKeepaliveRequest(job) {
152 const timeout = setTimeout(() => controller.abort(new Error('Keepalive request timed out')), 120000);152 const timeout = setTimeout(() => controller.abort(new Error('Keepalive request timed out')), 120000);
153 timeout.unref?.();153 timeout.unref?.();
154 try {154 try {
155 console.info(`[Keepalive] Dispatching job ${job.id} to ${job.endpoint}`);
155 const response = await fetch(`${job.request.origin}${job.endpoint}`, {156 const response = await fetch(`${job.request.origin}${job.endpoint}`, {
156 method: 'POST',157 method: 'POST',
157 headers: job.request.headers,158 headers: job.request.headers,
@@ -163,6 +164,7 @@ async function dispatchKeepaliveRequest(job) {
163 if (!response.ok) {164 if (!response.ok) {
164 throw new Error(`HTTP ${response.status}`);165 throw new Error(`HTTP ${response.status}`);
165 }166 }
167 console.info(`[Keepalive] Job ${job.id} completed successfully`);
166 } finally {168 } finally {
167 clearTimeout(timeout);169 clearTimeout(timeout);
168 }170 }
@@ -234,6 +236,7 @@ router.post('/register', (request, response) => {
234 delayMs: request.body.delayMs,236 delayMs: request.body.delayMs,
235 request: getReplayRequest(request),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 return response.send({ ok: true, nextRun: job.nextRun, leaseMs: FRONTEND_LEASE_MS });240 return response.send({ ok: true, nextRun: job.nextRun, leaseMs: FRONTEND_LEASE_MS });
238 } catch (error) {241 } catch (error) {
239 return response.status(400).send({ error: String(error.message || error) });242 return response.status(400).send({ error: String(error.message || error) });