Last active
March 20, 2026 17:53
-
-
Save gap777/6cc10654606e8ed27c7d1105b9d9eca5 to your computer and use it in GitHub Desktop.
Monkey-patch Rails ActionCable for deterministic behavior in tests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # frozen_string_literal: true | |
| # Make ActionCable stream delivery synchronous so that Turbo broadcast_replace_to | |
| # calls complete before Capybara assertions run in system tests. | |
| # | |
| # There are two async hops between broadcast_replace_to and the DOM update: | |
| # | |
| # Hop 1 — AsyncSubscriberMap#invoke_callback (the test adapter extends async) | |
| # posts each subscriber callback onto the StreamEventLoop's thread pool | |
| # ("ActionCable-streamer") via @event_loop.post { super }. This fires before | |
| # worker_pool_stream_handler runs, so the entire delivery is off-thread. | |
| # Fix: override invoke_callback to call super directly, skipping the post. | |
| # | |
| # Hop 2 — worker_pool_stream_handler dispatches the decoded message via | |
| # connection.worker_pool.async_invoke, posting onto another ThreadPoolExecutor. | |
| # Fix: redefine to call invoke (synchronous) instead — invoke calls work() | |
| # directly on the calling thread, bypassing the executor entirely. | |
| # | |
| # With this setup, the entire broadcast_replace_to flow runs synchronously in the test thread, | |
| # so that Capybara assertions can reliably observe the DOM updates: | |
| # | |
| ActionCable::SubscriptionAdapter::Async::AsyncSubscriberMap.module_eval do | |
| def invoke_callback(*) | |
| super | |
| end | |
| end | |
| ActionCable::Channel::Streams.module_eval do | |
| private | |
| def worker_pool_stream_handler(broadcasting, user_handler, coder: nil) | |
| handler = stream_handler(broadcasting, user_handler, coder: coder) | |
| lambda do |message| | |
| now_ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) | |
| now_iso = Time.at(now_ns / 1_000_000_000r).in_time_zone.iso8601(9) | |
| Rails.logger.debug { "Invoking ActionCable stream handler synchronously for message: #{message.inspect} at #{now_iso} (epoch_ns=#{now_ns})" } | |
| connection.worker_pool.invoke handler, :call, message, connection: connection | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment