Skip to content

Instantly share code, notes, and snippets.

@gap777
Last active March 20, 2026 17:53
Show Gist options
  • Select an option

  • Save gap777/6cc10654606e8ed27c7d1105b9d9eca5 to your computer and use it in GitHub Desktop.

Select an option

Save gap777/6cc10654606e8ed27c7d1105b9d9eca5 to your computer and use it in GitHub Desktop.
Monkey-patch Rails ActionCable for deterministic behavior in tests
# 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