armeria icon indicating copy to clipboard operation
armeria copied to clipboard

Add streamTimeout option to WebSocketService/WebSocketClient; send close frame on inbound failure; clean up related resources

Open sjy982 opened this issue 4 months ago • 2 comments

Motivation

  • Instead of asking each handler to wrap the inbound stream with StreamMessage.timeout(..., UNTIL_NEXT), expose it as an option on WebSocketServiceBuilder / WebSocketClientBuilder so both server and client can enable it consistently and easily.
  • When the inbound completes abnormally (cancel/abort/timeout, etc.), further outbound sends are meaningless. We should immediately tidy up outbound and the stream/channel. If the connection is still valid and the termination is initiated locally, prefer sending a WebSocket close frame (with an appropriate status + reason) over a transport reset, for better reason propagation, interoperability, and observability.

Modifications

  • Add WebSocketServiceBuilder#streamTimeout(Duration).

    • When set, wrap inbound in TimeoutStreamMessage with StreamTimeoutMode.UNTIL_NEXT inside DefaultWebSocketService#serve(...).
  • Update DefaultWebSocketService#serve(...):

    • If inbound completes with an error, call outbound.abort(mappedCause) so recoverAndResume sends a CloseWebSocketFrame.
    • Map CancelledSubscriptionException / AbortedStreamException to InboundCompleteException to avoid being skipped by the recovery logic.
  • Add WebSocketClientBuilder#streamTimeout(Duration).

    • When set, wrap inbound in TimeoutStreamMessage with StreamTimeoutMode.UNTIL_NEXT inside DefaultWebSocketClient#connect(...).
  • Update WebSocketSession#setOutbound(...):

    • Attach recoverAndResume(...) to outbound so that on send failure it emits a CloseWebSocketFrame and records the exception in RequestLog (endRequest(cause) / endResponse(cause)). For ClosedStreamException, do not send a close frame—just abort.
    • When inbound completes exceptionally, abort(...) outbound with the mapped cause so the recovery stream kicks in.
  • Add InboundCompleteException extends CancellationException:

    • Signals that inbound completed due to cancellation/abort.
  • Add a client-side newCloseWebSocketFrame(Throwable) in WebSocketUtil.

Result

  • WebSocketService / WebSocketClient can apply StreamMessage.timeout(..., UNTIL_NEXT) to inbound via the simple streamTimeout(Duration) option.
  • When inbound completes with an error:
    • If the channel is still usable, abort outbound with the mapped cause so a close frame is sent (skip sending for ClosedStreamException).
    • Record request/response causes in RequestLog.
    • On HTTP/1.x, close the channel; on HTTP/2, clean up the stream.

sjy982 avatar Aug 17 '25 10:08 sjy982

🔍 Build Scan® (commit: 7315e5204428faa2651ae60d1989bfa1997cb2d2)

Job name Status Build Scan®
build-ubicloud-standard-16-jdk-8 https://ge.armeria.dev/s/bm43lhrrpegnw
build-ubicloud-standard-16-jdk-21-snapshot-blockhound https://ge.armeria.dev/s/wcgst5gh5tzzm
build-ubicloud-standard-16-jdk-17-min-java-17-coverage ❌ (failure) https://ge.armeria.dev/s/4s265c27yqdgc
build-ubicloud-standard-16-jdk-17-min-java-11 https://ge.armeria.dev/s/35mkbdpex4q76
build-ubicloud-standard-16-jdk-17-leak ❌ (failure) https://ge.armeria.dev/s/vswshzqvpsino
build-ubicloud-standard-16-jdk-11 https://ge.armeria.dev/s/nu3wjct2nb57c
build-macos-latest-jdk-21 https://ge.armeria.dev/s/2uuqvzlyy2bia

github-actions[bot] avatar Aug 17 '25 11:08 github-actions[bot]

Codecov Report

:x: Patch coverage is 80.00000% with 19 lines in your changes missing coverage. Please review. :white_check_mark: Project coverage is 74.16%. Comparing base (8150425) to head (2184b0c). :warning: Report is 215 commits behind head on main.

Files with missing lines Patch % Lines
...rp/armeria/common/stream/TimeoutStreamMessage.java 73.68% 5 Missing :warning:
...orp/armeria/client/websocket/WebSocketSession.java 77.77% 2 Missing and 2 partials :warning:
...corp/armeria/common/logging/DefaultRequestLog.java 82.35% 1 Missing and 2 partials :warning:
...ecorp/armeria/common/InboundCompleteException.java 50.00% 2 Missing :warning:
...ommon/websocket/WebSocketIdleTimeoutException.java 50.00% 2 Missing :warning:
...meria/client/websocket/WebSocketClientBuilder.java 80.00% 0 Missing and 1 partial :warning:
.../linecorp/armeria/common/stream/StreamMessage.java 0.00% 0 Missing and 1 partial :warning:
...eria/server/websocket/WebSocketServiceBuilder.java 80.00% 0 Missing and 1 partial :warning:
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6357      +/-   ##
============================================
- Coverage     74.46%   74.16%   -0.30%     
- Complexity    22234    23035     +801     
============================================
  Files          1963     2064     +101     
  Lines         82437    86188    +3751     
  Branches      10764    11317     +553     
============================================
+ Hits          61385    63922    +2537     
- Misses        15918    16859     +941     
- Partials       5134     5407     +273     

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov[bot] avatar Aug 28 '25 15:08 codecov[bot]