@@ -163,7 +163,7 @@ public static QueryEvent decodeError(long payload, int payloadLen) {
163163 @ Override
164164 public void onBinaryMessage (long payloadPtr , int payloadLen ) {
165165 if (payloadLen < QwpConstants .HEADER_SIZE + 1 ) {
166- emitTerminalError ("server sent truncated frame" );
166+ emitTerminalTransportError ("server sent truncated frame" );
167167 // Stop the receive loop; the framing is broken and any further bytes
168168 // would be misinterpreted relative to the expected message boundary.
169169 currentQueryDone = true ;
@@ -183,16 +183,17 @@ public void onBinaryMessage(long payloadPtr, int payloadLen) {
183183 decodeAndEmitError (payloadPtr , payloadLen );
184184 currentQueryDone = true ;
185185 } else {
186- emitTerminalError ("unknown msg_kind 0x" + Integer .toHexString (msgKind & 0xFF ));
186+ emitTerminalTransportError ("unknown msg_kind 0x" + Integer .toHexString (msgKind & 0xFF ));
187187 currentQueryDone = true ;
188188 }
189189 }
190190
191191 @ Override
192192 public void onClose (int code , String reason ) {
193- notifyTerminalFailure ("server closed connection: code=" + code + " reason=" + reason );
193+ String msg = "server closed connection: code=" + code + " reason=" + reason ;
194+ notifyTerminalFailure (msg );
194195 if (!currentQueryDone ) {
195- emitError ( WebSocketResponse .STATUS_INTERNAL_ERROR , "server closed connection: code=" + code + " reason=" + reason );
196+ events . offer ( new QueryEvent (). asTransportError ( WebSocketResponse .STATUS_INTERNAL_ERROR , msg ) );
196197 currentQueryDone = true ;
197198 }
198199 }
@@ -261,8 +262,9 @@ public void run() {
261262 }
262263 }
263264 } catch (Throwable t ) {
264- notifyTerminalFailure ("I/O thread failure: " + t .getMessage ());
265- emitErrorBlocking ("I/O thread failure: " + t .getMessage ());
265+ String msg = "I/O thread failure: " + t .getMessage ();
266+ notifyTerminalFailure (msg );
267+ emitTransportErrorBlocking (WebSocketResponse .STATUS_INTERNAL_ERROR , msg );
266268 } finally {
267269 // Wake any user thread blocked on events.take(). Without this, a close()
268270 // (or any abnormal exit) while a user thread is mid-execute() would let
@@ -272,11 +274,15 @@ public void run() {
272274 String msg = shutdown
273275 ? "I/O thread shut down with query in flight"
274276 : "I/O thread terminated with query in flight" ;
275- if (!shutdown ) {
276- // Shutdown is a user-driven close; don't mark the client terminal for it.
277+ if (shutdown ) {
278+ // User-driven close. Emit a plain query error (KIND_ERROR): the
279+ // user initiated the shutdown, we do not want failover to fire on
280+ // their behalf.
281+ emitError (WebSocketResponse .STATUS_INTERNAL_ERROR , msg );
282+ } else {
277283 notifyTerminalFailure (msg );
284+ emitTransportErrorBlocking (WebSocketResponse .STATUS_INTERNAL_ERROR , msg );
278285 }
279- emitErrorBlocking (msg );
280286 currentQueryDone = true ;
281287 }
282288 }
@@ -335,7 +341,7 @@ private void decodeAndEmitExecDone(long payload, int payloadLen) {
335341 long p = payload + QwpConstants .HEADER_SIZE + 1 + 8 ;
336342 long limit = payload + payloadLen ;
337343 if (p + 1 > limit ) {
338- emitTerminalError ("EXEC_DONE frame truncated before op_type" );
344+ emitTerminalTransportError ("EXEC_DONE frame truncated before op_type" );
339345 return ;
340346 }
341347 byte opType = Unsafe .getUnsafe ().getByte (p ++);
@@ -347,7 +353,7 @@ private void decodeAndEmitExecDone(long payload, int payloadLen) {
347353 if ((b & 0x80 ) == 0 ) break ;
348354 shift += 7 ;
349355 if (shift > 63 ) {
350- emitTerminalError ("EXEC_DONE rows_affected varint overflow" );
356+ emitTerminalTransportError ("EXEC_DONE rows_affected varint overflow" );
351357 return ;
352358 }
353359 }
@@ -408,12 +414,13 @@ private void emitError(byte status, String message) {
408414 }
409415
410416 /**
411- * Like {@link #emitError} but retries until the event is enqueued. Used on
412- * shutdown and fatal-error paths where dropping the event would leave the
413- * user thread blocked on {@link #takeEvent} indefinitely.
417+ * Like {@link #emitError} but emits a {@code KIND_TRANSPORT_ERROR} event
418+ * rather than {@code KIND_ERROR}, and retries until the event is enqueued.
419+ * Used on transport-failure paths where the WebSocket is torn down and
420+ * the user thread needs to be woken so failover can kick in.
414421 */
415- private void emitErrorBlocking ( String message ) {
416- QueryEvent ev = new QueryEvent ().asError ( WebSocketResponse . STATUS_INTERNAL_ERROR , message );
422+ private void emitTransportErrorBlocking ( byte status , String message ) {
423+ QueryEvent ev = new QueryEvent ().asTransportError ( status , message );
417424 while (!events .offer (ev )) {
418425 // Events queue is bounded; the consumer side (user thread) drains
419426 // steadily under a cooperative shutdown. Yield between retries so
@@ -428,13 +435,20 @@ private void emitErrorBlocking(String message) {
428435 }
429436 }
430437
431- // Emits a per-query error event AND latches the client terminal-failure
432- // state. Use for transport- or protocol-level faults where the WebSocket
433- // is no longer usable; per-query QUERY_ERROR must still go through
434- // plain emitError, since the connection remains healthy after it.
435- private void emitTerminalError (String message ) {
438+ /**
439+ * Emits a {@code KIND_TRANSPORT_ERROR} event AND latches the client's
440+ * terminal-failure state. Use for transport- or protocol-level faults
441+ * where the WebSocket is no longer usable (server close, send/recv
442+ * exception, decoder out of sync). Per-query {@code QUERY_ERROR} still
443+ * goes through {@link #emitError}, since the connection remains healthy
444+ * after one. The event-kind split means {@code execute()} classifies
445+ * the failure by the event kind, not by peeking at the terminal-failure
446+ * latch -- the latch stays strictly for short-circuiting subsequent
447+ * {@code execute()} calls on a broken client.
448+ */
449+ private void emitTerminalTransportError (String message ) {
436450 notifyTerminalFailure (message );
437- emitError ( WebSocketResponse .STATUS_INTERNAL_ERROR , message );
451+ events . offer ( new QueryEvent (). asTransportError ( WebSocketResponse .STATUS_INTERNAL_ERROR , message ) );
438452 }
439453
440454 private void handleResultBatch (long payloadPtr , int payloadLen ) {
@@ -467,7 +481,7 @@ private void handleResultBatch(long payloadPtr, int payloadLen) {
467481 }
468482 // A decode failure leaves the client-side decoder out of step with
469483 // the server's byte stream: the next frame cannot be trusted.
470- emitTerminalError ("decode failure: " + e .getMessage ());
484+ emitTerminalTransportError ("decode failure: " + e .getMessage ());
471485 currentQueryDone = true ;
472486 return ;
473487 }
0 commit comments