Skip to content

Commit 6366db6

Browse files
committed
stop websockets on idle
1 parent c809689 commit 6366db6

2 files changed

Lines changed: 59 additions & 39 deletions

File tree

src/main/resources/static/js/dashboard.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Starts CPU stress simulation
1010
*/
1111
async function startCpuStress() {
12+
SocketClient.ensureWebSocket();
1213
const durationSeconds = parseInt(document.getElementById('cpuDuration').value) || 30;
1314
const intensity = document.getElementById('cpuIntensity').value || 'HIGH';
1415

@@ -64,6 +65,7 @@ async function stopThreadStarvation() {
6465
let isAllocating = false;
6566

6667
async function allocateMemory() {
68+
SocketClient.ensureWebSocket();
6769
// Prevent double-submission
6870
if (isAllocating) {
6971
console.log('[Dashboard] Allocation already in progress, ignoring');
@@ -114,6 +116,7 @@ async function releaseAllMemory() {
114116
* Server spawns N internal requests to block Tomcat servlet threads.
115117
*/
116118
async function startThreadStarvation() {
119+
SocketClient.ensureWebSocket();
117120
const threadCount = parseInt(document.getElementById('starvationCount').value) || 50;
118121
const durationSeconds = parseInt(document.getElementById('starvationDuration').value) || 30;
119122

@@ -136,6 +139,7 @@ async function startThreadStarvation() {
136139
* Starts connection pool exhaustion simulation
137140
*/
138141
async function triggerConnectionPool() {
142+
SocketClient.ensureWebSocket();
139143
const poolSize = parseInt(document.getElementById('poolSize').value) || 10;
140144
const queryDurationSeconds = parseInt(document.getElementById('queryDuration').value) || 30;
141145
const concurrentQueries = parseInt(document.getElementById('concurrentQueries').value) || 20;
@@ -179,6 +183,7 @@ async function stopConnectionPool() {
179183
* Starts failed requests simulation (generates HTTP 5xx errors)
180184
*/
181185
async function triggerFailedRequests() {
186+
SocketClient.ensureWebSocket();
182187
const numberOfRequests = parseInt(document.getElementById('numberOfFailedRequests').value) || 10;
183188

184189
try {
@@ -199,6 +204,7 @@ async function triggerFailedRequests() {
199204
* Triggers a crash simulation
200205
*/
201206
async function triggerCrash() {
207+
SocketClient.ensureWebSocket();
202208
const type = document.getElementById('crashType').value || 'EXCEPTION';
203209

204210
if (!confirm(`⚠️ Are you sure you want to trigger a ${type} crash? This may terminate the application.`)) {
@@ -411,20 +417,18 @@ const Dashboard = (function() {
411417

412418
// Update connection status based on idle state changes
413419
if (event.event === 'GOING_IDLE') {
414-
// Lock the status indicator before updating it, so that any
415-
// WebSocket reconnect/disconnect events that fire while the server
416-
// is idle cannot overwrite the Idle indicator.
417-
SocketClient.setServerIdle(true);
418420
const statusEl = document.getElementById('connection-status');
419421
if (statusEl) {
420422
statusEl.classList.remove('status-connected', 'status-disconnected', 'status-reconnecting', 'status-idle');
421423
statusEl.classList.add('status-idle');
422424
statusEl.textContent = 'Idle';
423425
}
426+
// Intentionally close WebSocket to prevent reconnect-induced status flicker
427+
SocketClient.closeForIdle();
424428
} else if (event.event === 'WAKING_UP') {
425-
// Unlock the status indicator before updating it, restoring normal
426-
// WebSocket status reporting now that the server is active again.
427-
SocketClient.setServerIdle(false);
429+
// WebSocket will have already reconnected (via ensureWebSocket on button click)
430+
// and onConnect will have set the status to Connected. This is a belt-and-
431+
// suspenders update for any edge cases where the event arrives first.
428432
const statusEl = document.getElementById('connection-status');
429433
if (statusEl) {
430434
statusEl.classList.remove('status-connected', 'status-disconnected', 'status-reconnecting', 'status-idle');
@@ -928,6 +932,13 @@ const Dashboard = (function() {
928932
})();
929933

930934
// Initialize dashboard when DOM is ready
931-
document.addEventListener('DOMContentLoaded', function() {
935+
// recordActivity fires an HTTP request first so the server wakes before the
936+
// WebSocket connects — ensuring the first broadcast arrives with is_idle: false.
937+
document.addEventListener('DOMContentLoaded', async function() {
938+
try {
939+
await SocketClient.recordActivity();
940+
} catch (e) {
941+
// Non-blocking — WS will retry on its own
942+
}
932943
Dashboard.init();
933944
});

src/main/resources/static/js/socket-client.js

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ const SocketClient = (function() {
1818
let disconnectTimer = null;
1919
let disconnectTime = null;
2020

21-
// Idle state tracking
22-
let activityRecorded = false;
23-
let serverIsIdle = false;
21+
// When true, the WebSocket was closed intentionally (idle transition).
22+
// Suppresses disconnect status updates and prevents auto-reconnect.
23+
let intentionalDisconnect = false;
2424

2525
// Callbacks
2626
const callbacks = {
@@ -58,11 +58,8 @@ const SocketClient = (function() {
5858

5959
/**
6060
* Updates the connection status UI.
61-
* While the server is idle, all status transitions are suppressed — only
62-
* dashboard.js (via setServerIdle) may change the indicator in that state.
6361
*/
6462
function updateConnectionStatus(status) {
65-
if (serverIsIdle) return;
6663
const statusEl = document.getElementById('connection-status');
6764
if (statusEl) {
6865
// Remove all status classes
@@ -94,12 +91,6 @@ const SocketClient = (function() {
9491
*/
9592
function connect() {
9693
updateConnectionStatus('connecting');
97-
98-
// Record activity only on initial page load, not on reconnections
99-
// Reconnections should NOT reset the idle timeout - only explicit user activity should
100-
if (firstConnection) {
101-
recordActivity();
102-
}
10394

10495
// Create SockJS connection
10596
const socket = new SockJS('/ws');
@@ -121,6 +112,7 @@ const SocketClient = (function() {
121112
console.log('[SocketClient] Connected to WebSocket server');
122113
connected = true;
123114
reconnectAttempts = 0;
115+
intentionalDisconnect = false;
124116
updateConnectionStatus('connected');
125117

126118
// Clear any pending disconnect timer
@@ -160,6 +152,13 @@ const SocketClient = (function() {
160152
stompClient.onWebSocketClose = function(event) {
161153
console.log('[SocketClient] WebSocket closed');
162154
connected = false;
155+
156+
// Intentional close (idle transition) — do not update the status
157+
// indicator or schedule a reconnect. Just let the connection sit closed.
158+
if (intentionalDisconnect) {
159+
return;
160+
}
161+
163162
updateConnectionStatus('disconnected');
164163
trigger('onDisconnect', event);
165164

@@ -270,18 +269,37 @@ const SocketClient = (function() {
270269
return connected;
271270
}
272271

272+
/**
273+
* Intentionally closes the WebSocket during idle transition.
274+
* Sets intentionalDisconnect so the onclose handler does not update the
275+
* status indicator or schedule a reconnect.
276+
*/
277+
function closeForIdle() {
278+
intentionalDisconnect = true;
279+
if (stompClient && stompClient.active) {
280+
stompClient.deactivate();
281+
connected = false;
282+
}
283+
}
284+
285+
/**
286+
* Ensures the WebSocket is connected. If the STOMP client is not active
287+
* (e.g., after an intentional idle disconnect), reconnects immediately.
288+
* Called at the top of every simulation trigger so clicking a button while
289+
* idle automatically re-establishes the connection before the API call.
290+
*/
291+
function ensureWebSocket() {
292+
if (!stompClient || !stompClient.active) {
293+
connect();
294+
}
295+
}
296+
273297
/**
274298
* Records activity with the server to prevent idle timeout.
275-
* Should only be called on:
276-
* - Initial page load (not reconnections)
277-
* - Explicit user activity events
278-
* Wakes the app from idle state if necessary.
299+
* Called once on page load (before WS init) to wake the server so the
300+
* first WebSocket broadcast arrives with is_idle: false.
279301
*/
280302
async function recordActivity() {
281-
if (activityRecorded) {
282-
return; // Only record once per page load to avoid spam
283-
}
284-
285303
try {
286304
const response = await fetch('/api/health/activity', {
287305
method: 'POST',
@@ -290,7 +308,6 @@ const SocketClient = (function() {
290308

291309
if (response.ok) {
292310
const result = await response.json();
293-
activityRecorded = true;
294311

295312
if (result.wokeFromIdle) {
296313
console.log('[SocketClient] App woken from idle state');
@@ -307,15 +324,6 @@ const SocketClient = (function() {
307324
}
308325
}
309326

310-
/**
311-
* Marks the server as idle or active, controlling whether WebSocket
312-
* reconnect/disconnect events may update the status indicator.
313-
* Call with true on GOING_IDLE, false on WAKING_UP.
314-
*/
315-
function setServerIdle(idle) {
316-
serverIsIdle = idle;
317-
}
318-
319327
// Public API
320328
return {
321329
connect,
@@ -324,6 +332,7 @@ const SocketClient = (function() {
324332
on,
325333
isConnected,
326334
recordActivity,
327-
setServerIdle
335+
closeForIdle,
336+
ensureWebSocket
328337
};
329338
})();

0 commit comments

Comments
 (0)