Skip to content

Commit d8cc1c7

Browse files
duncdrumclaude
andcommitted
[bugfix] Fix watchdog.reset() race that silently dropped WebSocket
cancels on CI xquery.execute(resetContext=true) calls watchdog.reset() after the "evaluating" signal is sent. On slow CI machines, sendText() blocks long enough for the client cancel roundtrip to complete: kill() sets terminate=true, then watchdog.reset() immediately clears it to false, dropping the cancel silently. Fix: call watchdog.reset() manually before registerQuery()/sendProgress(), then pass resetContext=false to xquery.execute() so the internal reset is skipped. Add context.reset() to the outer finally block to cover what resetContext=false no longer handles in xquery.execute(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 807535d commit d8cc1c7

2 files changed

Lines changed: 7 additions & 4 deletions

File tree

exist-core/src/main/java/org/exist/http/ws/QueryExecutor.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ public void execute(final Session wsSession, final EvalSession evalSession,
103103
sendProgress(wsSession, msg.id(), EvalProtocol.PHASE_COMPILING, 0,
104104
System.currentTimeMillis() - startTime);
105105

106-
// Set timeout via watchdog
107106
final XQueryWatchDog watchDog = context.getWatchDog();
107+
watchDog.reset(); // before signal: prevents execute() from clearing a concurrent kill() via its internal watchdog.reset()
108108
if (msg.maxExecutionTime() > 0) {
109109
watchDog.setTimeout(msg.maxExecutionTime());
110110
}
@@ -120,7 +120,7 @@ public void execute(final Session wsSession, final EvalSession evalSession,
120120

121121
final Sequence result;
122122
try {
123-
result = xquery.execute(broker, compiled, null, new Properties(), true);
123+
result = xquery.execute(broker, compiled, null, new Properties(), false);
124124
} catch (final TerminatedException e) {
125125
timing.evaluate = System.currentTimeMillis() - evalStart;
126126
reportTerminationOrError(wsSession, evalSession, msg, timing, startTime,
@@ -164,6 +164,7 @@ public void execute(final Session wsSession, final EvalSession evalSession,
164164
} finally {
165165
evalSession.unregisterQuery(msg.id());
166166
context.runCleanupTasks();
167+
context.reset(); // needed: resetContext=false skipped this in xquery.execute()
167168
}
168169

169170
} catch (final EXistException | PermissionDeniedException e) {

exist-core/src/test/java/org/exist/http/ws/EvalWebSocketEndpointTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -658,10 +658,12 @@ public void onMessage(final String message) {
658658
}, createAdminConfig(), getWsUri());
659659

660660
try {
661-
// Query that should take longer than 2s timeout
661+
// GC-free query: return () avoids heap exhaustion on CI, ensuring the 2s
662+
// watchdog timeout fires reliably via proceed() rather than OOM killing
663+
// the thread before the timeout check runs.
662664
session.getBasicRemote().sendText(
663665
"{\"action\":\"eval\",\"id\":\"q-timeout\"," +
664-
"\"query\":\"let $x := for $i in 1 to 999999999 return string($i) return $x\"," +
666+
"\"query\":\"for $i in 1 to 999999999 return ()\"," +
665667
"\"max-execution-time\":2000}");
666668

667669
assertTrue("Should receive timeout error within 30s",

0 commit comments

Comments
 (0)