4545import java .util .Map ;
4646import java .util .Optional ;
4747import java .util .Properties ;
48+ import java .util .concurrent .atomic .AtomicBoolean ;
4849
4950/**
5051 * Executes XQuery expressions for the /ws/eval endpoint with support for
@@ -56,6 +57,9 @@ public final class QueryExecutor {
5657
5758 private static final long PROGRESS_INTERVAL_MS = 500 ;
5859
60+ static final String WALL_CLOCK_TIMEOUT_MESSAGE =
61+ "The query exceeded the predefined timeout and has been killed." ;
62+
5963 private final BrokerPool pool ;
6064
6165 public QueryExecutor (final BrokerPool pool ) {
@@ -103,13 +107,21 @@ public void execute(final Session wsSession, final EvalSession evalSession,
103107 sendProgress (wsSession , msg .id (), EvalProtocol .PHASE_COMPILING , 0 ,
104108 System .currentTimeMillis () - startTime );
105109
106- // Set timeout via watchdog
107110 final XQueryWatchDog watchDog = context .getWatchDog ();
111+ watchDog .reset (); // before signal: prevents execute() from clearing a concurrent kill() via its internal watchdog.reset()
108112 if (msg .maxExecutionTime () > 0 ) {
109113 watchDog .setTimeout (msg .maxExecutionTime ());
110114 }
111115 evalSession .registerQuery (msg .id (), watchDog );
112116
117+ final AtomicBoolean terminalResponseSent = new AtomicBoolean (false );
118+ WallClockQueryTimeout wallClockTimeout = null ;
119+ if (msg .maxExecutionTime () > 0 ) {
120+ wallClockTimeout = new WallClockQueryTimeout ();
121+ wallClockTimeout .schedule (msg .maxExecutionTime (), () -> handleWallClockTimeout (
122+ wsSession , evalSession , msg , timing , startTime , watchDog , terminalResponseSent ));
123+ }
124+
113125 try {
114126 // Evaluate phase
115127 sendProgress (wsSession , msg .id (), EvalProtocol .PHASE_EVALUATING , 0 ,
@@ -120,16 +132,17 @@ public void execute(final Session wsSession, final EvalSession evalSession,
120132
121133 final Sequence result ;
122134 try {
123- result = xquery .execute (broker , compiled , null , new Properties (), true );
135+ result = xquery .execute (broker , compiled , null , new Properties (), false );
124136 } catch (final TerminatedException e ) {
125137 timing .evaluate = System .currentTimeMillis () - evalStart ;
126138 reportTerminationOrError (wsSession , evalSession , msg , timing , startTime ,
127- watchDog , ErrorInfo .of (e .getMessage (), e .getLine (), e .getColumn ()));
139+ watchDog , ErrorInfo .of (e .getMessage (), e .getLine (), e .getColumn ()),
140+ terminalResponseSent );
128141 return ;
129142 } catch (final XPathException e ) {
130143 timing .evaluate = System .currentTimeMillis () - evalStart ;
131144 reportTerminationOrError (wsSession , evalSession , msg , timing , startTime ,
132- watchDog , ErrorInfo .of (e ));
145+ watchDog , ErrorInfo .of (e ), terminalResponseSent );
133146 return ;
134147 }
135148
@@ -147,23 +160,46 @@ public void execute(final Session wsSession, final EvalSession evalSession,
147160 try {
148161 if (msg .stream ().enabled () && itemCount > msg .stream ().chunkSize ()) {
149162 streamResults (wsSession , msg .id (), broker , result , outputProperties ,
150- msg .stream ().chunkSize (), timing , startTime , watchDog );
163+ msg .stream ().chunkSize (), timing , startTime , watchDog ,
164+ terminalResponseSent , evalSession , msg );
151165 } else {
166+ if (watchDog .isTerminating ()) {
167+ timing .serialize = System .currentTimeMillis () - serStart ;
168+ timing .total = System .currentTimeMillis () - startTime ;
169+ reportTerminationOrError (wsSession , evalSession , msg , timing , startTime ,
170+ watchDog , ErrorInfo .of (WALL_CLOCK_TIMEOUT_MESSAGE ), terminalResponseSent );
171+ return ;
172+ }
173+ try {
174+ watchDog .proceed (null );
175+ } catch (final TerminatedException e ) {
176+ timing .serialize = System .currentTimeMillis () - serStart ;
177+ reportTerminationOrError (wsSession , evalSession , msg , timing , startTime ,
178+ watchDog , ErrorInfo .of (e .getMessage ()), terminalResponseSent );
179+ return ;
180+ }
152181 final String serialized = serializeAll (broker , result , outputProperties );
153182 timing .serialize = System .currentTimeMillis () - serStart ;
154183 timing .total = System .currentTimeMillis () - startTime ;
155184
156- sendResult (wsSession , msg .id (), 1 , serialized , false , timing , itemCount );
185+ if (terminalResponseSent .compareAndSet (false , true )) {
186+ sendResult (wsSession , msg .id (), 1 , serialized , false , timing , itemCount );
187+ QueryMonitorBroadcaster .broadcastEvent ("completed" , msg .id (), user , msg .query (),
188+ null , itemCount , timing .total );
189+ }
157190 }
158- QueryMonitorBroadcaster .broadcastEvent ("completed" , msg .id (), user , msg .query (),
159- null , itemCount , System .currentTimeMillis () - startTime );
160191 } catch (final SAXException | XPathException e ) {
161192 timing .serialize = System .currentTimeMillis () - serStart ;
162- reportError (wsSession , evalSession , msg , timing , startTime , ErrorInfo .of (e .getMessage ()));
193+ reportError (wsSession , evalSession , msg , timing , startTime , ErrorInfo .of (e .getMessage ()),
194+ terminalResponseSent );
163195 }
164196 } finally {
197+ if (wallClockTimeout != null ) {
198+ wallClockTimeout .cancel ();
199+ }
165200 evalSession .unregisterQuery (msg .id ());
166201 context .runCleanupTasks ();
202+ context .reset (); // needed: resetContext=false skipped this in xquery.execute()
167203 }
168204
169205 } catch (final EXistException | PermissionDeniedException e ) {
@@ -192,6 +228,16 @@ static ErrorInfo of(final String message) {
192228 private void reportError (final Session wsSession , final EvalSession evalSession ,
193229 final EvalProtocol .ClientMessage msg , final EvalProtocol .Timing timing ,
194230 final long startTime , final ErrorInfo error ) {
231+ reportError (wsSession , evalSession , msg , timing , startTime , error , null );
232+ }
233+
234+ private void reportError (final Session wsSession , final EvalSession evalSession ,
235+ final EvalProtocol .ClientMessage msg , final EvalProtocol .Timing timing ,
236+ final long startTime , final ErrorInfo error ,
237+ @ Nullable final AtomicBoolean terminalGate ) {
238+ if (terminalGate != null && !terminalGate .compareAndSet (false , true )) {
239+ return ;
240+ }
195241 timing .total = System .currentTimeMillis () - startTime ;
196242 if (error .xpe () != null ) {
197243 sendError (wsSession , msg .id (), error .xpe (), timing );
@@ -207,16 +253,57 @@ private void reportTerminationOrError(final Session wsSession, final EvalSession
207253 final EvalProtocol .Timing timing , final long startTime ,
208254 final XQueryWatchDog watchDog ,
209255 final ErrorInfo error ) {
210- if (watchDog .isTerminating ()) {
256+ reportTerminationOrError (wsSession , evalSession , msg , timing , startTime , watchDog , error , null );
257+ }
258+
259+ private void reportTerminationOrError (final Session wsSession , final EvalSession evalSession ,
260+ final EvalProtocol .ClientMessage msg ,
261+ final EvalProtocol .Timing timing , final long startTime ,
262+ final XQueryWatchDog watchDog ,
263+ final ErrorInfo error ,
264+ @ Nullable final AtomicBoolean terminalGate ) {
265+ if (watchDog .isTerminating () && !watchDog .isTimedOut ()) {
211266 timing .total = System .currentTimeMillis () - startTime ;
212- sendCancelled (wsSession , msg .id (), 0 , timing );
213- QueryMonitorBroadcaster .broadcastEvent ("cancelled" , msg .id (),
214- evalSession .getSubject ().getName (), msg .query (), null , 0 , timing .total );
267+ sendCancelledIfAbsent (wsSession , msg .id (), evalSession , msg , timing , 0 , terminalGate );
215268 } else {
216- reportError (wsSession , evalSession , msg , timing , startTime , error );
269+ reportError (wsSession , evalSession , msg , timing , startTime , error , terminalGate );
217270 }
218271 }
219272
273+ private void handleWallClockTimeout (final Session wsSession , final EvalSession evalSession ,
274+ final EvalProtocol .ClientMessage msg ,
275+ final EvalProtocol .Timing timing , final long startTime ,
276+ final XQueryWatchDog watchDog ,
277+ final AtomicBoolean terminalResponseSent ) {
278+ watchDog .killAsTimeout (0 );
279+ if (terminalResponseSent .compareAndSet (false , true )) {
280+ timing .total = System .currentTimeMillis () - startTime ;
281+ // Use async remote: this callback runs on the shared scheduler thread and must not block
282+ // on client I/O — a stalled connection would otherwise freeze every pending timeout.
283+ try {
284+ wsSession .getAsyncRemote ().sendText (
285+ EvalProtocol .errorMessage (msg .id (), null , WALL_CLOCK_TIMEOUT_MESSAGE , 0 , 0 , timing ));
286+ } catch (final IOException e ) {
287+ LOG .debug ("Failed to send wall-clock timeout error: {}" , e .getMessage ());
288+ }
289+ QueryMonitorBroadcaster .broadcastEvent ("error" , msg .id (),
290+ evalSession .getSubject ().getName (), msg .query (), null , 0 , timing .total );
291+ }
292+ }
293+
294+ private void sendCancelledIfAbsent (final Session wsSession , final String queryId ,
295+ final EvalSession evalSession ,
296+ final EvalProtocol .ClientMessage msg ,
297+ final EvalProtocol .Timing timing , final long items ,
298+ @ Nullable final AtomicBoolean terminalGate ) {
299+ if (terminalGate != null && !terminalGate .compareAndSet (false , true )) {
300+ return ;
301+ }
302+ sendCancelled (wsSession , queryId , items , timing );
303+ QueryMonitorBroadcaster .broadcastEvent ("cancelled" , queryId ,
304+ evalSession .getSubject ().getName (), msg .query (), null , items , timing .total );
305+ }
306+
220307 /**
221308 * Compile-check a query without executing it.
222309 */
@@ -273,7 +360,10 @@ private void streamResults(final Session wsSession, final String queryId,
273360 final DBBroker broker , final Sequence result ,
274361 final Properties outputProperties , final int chunkSize ,
275362 final EvalProtocol .Timing timing , final long startTime ,
276- final XQueryWatchDog watchDog ) {
363+ final XQueryWatchDog watchDog ,
364+ final AtomicBoolean terminalResponseSent ,
365+ final EvalSession evalSession ,
366+ final EvalProtocol .ClientMessage msg ) {
277367 final long serStart = System .currentTimeMillis ();
278368 final long totalItems = result .getItemCount ();
279369 int chunkNum = 0 ;
@@ -282,10 +372,11 @@ private void streamResults(final Session wsSession, final String queryId,
282372 try {
283373 final SequenceIterator iter = result .iterate ();
284374 while (iter .hasNext ()) {
285- if (watchDog .isTerminating ()) {
375+ if (watchDog .isTerminating () || terminalResponseSent . get () ) {
286376 timing .serialize = System .currentTimeMillis () - serStart ;
287377 timing .total = System .currentTimeMillis () - startTime ;
288- sendCancelled (wsSession , queryId , itemsSent , timing );
378+ reportTerminationOrError (wsSession , evalSession , msg , timing , startTime ,
379+ watchDog , ErrorInfo .of (WALL_CLOCK_TIMEOUT_MESSAGE ), terminalResponseSent );
289380 return ;
290381 }
291382
@@ -304,11 +395,16 @@ private void streamResults(final Session wsSession, final String queryId,
304395 if (!more ) {
305396 timing .serialize = System .currentTimeMillis () - serStart ;
306397 timing .total = System .currentTimeMillis () - startTime ;
398+ if (terminalResponseSent .compareAndSet (false , true )) {
399+ sendResult (wsSession , queryId , chunkNum , data , false , timing , totalItems );
400+ QueryMonitorBroadcaster .broadcastEvent ("completed" , queryId ,
401+ evalSession .getSubject ().getName (), msg .query (),
402+ null , totalItems , timing .total );
403+ }
404+ } else {
405+ sendResult (wsSession , queryId , chunkNum , data , true , null , totalItems );
307406 }
308407
309- sendResult (wsSession , queryId , chunkNum , data , more ,
310- more ? null : timing , totalItems );
311-
312408 // send progress during streaming
313409 if (more && (System .currentTimeMillis () - startTime ) % PROGRESS_INTERVAL_MS < 50 ) {
314410 sendProgress (wsSession , queryId , EvalProtocol .PHASE_SERIALIZING ,
@@ -318,7 +414,8 @@ private void streamResults(final Session wsSession, final String queryId,
318414 } catch (final XPathException | SAXException e ) {
319415 timing .serialize = System .currentTimeMillis () - serStart ;
320416 timing .total = System .currentTimeMillis () - startTime ;
321- sendError (wsSession , queryId , null , e .getMessage (), 0 , 0 , timing );
417+ reportError (wsSession , evalSession , msg , timing , startTime , ErrorInfo .of (e .getMessage ()),
418+ terminalResponseSent );
322419 }
323420 }
324421
0 commit comments