Skip to content

Propagate cancellation through HTTP/2 output - #250

Merged
samuel-williams-shopify merged 2 commits into
mainfrom
mitigate-shutdown-hang
Sep 10, 2026
Merged

Propagate cancellation through HTTP/2 output#250
samuel-williams-shopify merged 2 commits into
mainfrom
mitigate-shutdown-hang

Conversation

@tavianator

@tavianator tavianator commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

The ensure block in Output#passthrough calls close_write(error), but a cancelled task previously left error as nil: a bare rescue => error catches StandardError, while Async::Cancel inherits directly from Exception.

As a result, cancellation was presented to finish_output as orderly completion. Explicitly rescue Async::Cancel alongside StandardError, preserving the cancellation object for body and stream cleanup before re-raising it.

This change intentionally does not add timeout or forced transport-abort policy. It only corrects cancellation propagation.

Types of Changes

  • Bug fix.

Contribution

@tavianator
tavianator force-pushed the mitigate-shutdown-hang branch from 448a670 to 92b8590 Compare September 9, 2026 14:40
The `ensure` block in `passthrough` attempts to close the stream with
`self.close_write(error)`, but there are two issues:

- For a cancelled task, `error` will be `nil` since it won't get set by
  the `rescue => error` block, as `Async::Cancel` isn't a `StandardError`

- More seriously, the actual `write()` can block indefinitely, stalling
  reactor shutdown

This led to backtraces like this during Falcon worker shutdown, forcing
Falcon to `SIGKILL` the worker process after the timeout:

    async-2.45.1   async/scheduler.rb:382   IO::Event::Selector::URing#io_write
    async-2.45.1   async/scheduler.rb:382   Async::Scheduler#io_write
    io-stream-0.11 io/stream/buffered.rb:112 IO#write
    io-stream-0.11 io/stream/buffered.rb:112 IO::Stream::Buffered#syswrite
    io-stream-0.11 io/stream/writable.rb:99  IO::Stream::Writable#drain
    io-stream-0.11 io/stream/writable.rb:47  block in IO::Stream::Writable#flush
    io-stream-0.11 io/stream/writable.rb:46  Thread::Mutex#synchronize
    io-stream-0.11 io/stream/writable.rb:46  IO::Stream::Writable#flush
    protocol-http2 protocol/http2/framer.rb:58      Protocol::HTTP2::Framer#flush
    protocol-http2 protocol/http2/connection.rb:253 Protocol::HTTP2::Connection#write_frame
    protocol-http2 protocol/http2/stream.rb:115     Protocol::HTTP2::Stream#write_frame
    protocol-http2 protocol/http2/stream.rb:203     Protocol::HTTP2::Stream#write_data
    protocol-http2 protocol/http2/stream.rb:213     Protocol::HTTP2::Stream#send_data
    async-http-0.94.2 http2/stream.rb:140  Async::HTTP::Protocol::HTTP2::Stream#finish_output
    async-http-0.94.2 http2/output.rb:68   Async::HTTP::Protocol::HTTP2::Output#close_write
    async-http-0.94.2 http2/output.rb:120  Async::HTTP::Protocol::HTTP2::Output#passthrough
    async-2.45.1      async/task.rb:225    block in Async::Task#run
    async-2.45.1      async/task.rb:523    block in Async::Task#schedule

Fix this with `rescue Exception => error`, and add a 1-second timeout
around `close_write` if `error` is non-`nil`.
@tavianator
tavianator force-pushed the mitigate-shutdown-hang branch from 92b8590 to 1aed35c Compare September 9, 2026 15:25
Signed-off-by: Samuel Williams <samuel.williams@shopify.com>
Assisted-By: devx/2fb5639b-d636-44ad-a666-9088d8c41cc1
@samuel-williams-shopify samuel-williams-shopify changed the title Prevent Output#passthrough from hanging on cancellation Propagate cancellation through HTTP/2 output Sep 10, 2026
@samuel-williams-shopify
samuel-williams-shopify merged commit 8999f0f into main Sep 10, 2026
34 of 40 checks passed
@samuel-williams-shopify
samuel-williams-shopify deleted the mitigate-shutdown-hang branch September 10, 2026 02:38
@samuel-williams-shopify samuel-williams-shopify added this to the v0.104.0 milestone Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants