Add Rails comparison: Action Cable / Solid Cable / AsyncCable / AnyCable#2
Open
irinanazarova wants to merge 16 commits into
Open
Add Rails comparison: Action Cable / Solid Cable / AsyncCable / AnyCable#2irinanazarova wants to merge 16 commits into
irinanazarova wants to merge 16 commits into
Conversation
Target apps and harness for the Rails WebSocket adapter comparison behind
anycable.io/compare/rails-actioncable.
Targets:
- cable-bench/ Rails 8.1 app, BENCH_MODE selects Action Cable (Redis)
or Solid Cable (database); also the AnyCable RPC backend
- cable-bench-falcon/ same app booted on Falcon via actioncable-next +
async-cable (the AsyncCable target)
Harness:
- idle-multi.ts: forward CHANNEL/AC_PROTOCOL and send the bench-runner auth
token, so idle/capacity runs can target a real Rails channel
- jitter-multi.ts: forward CHANNEL + AC_PROTOCOL for Rails targets
- idle-runner.ts / jitter-runners.ts / server.ts: channel + acProtocol params
- tests-manifest.ts: Rails latency/jitter/idle/avalanche/capacity specs
Results (sharded, one shared-tenant Railway window):
- backend/results/rails-sharded-2026-06-28.json latency/jitter/10K/idle/avalanche
- backend/results/rails-capacity-break-2026-06-28.json idle-to-break per adapter
Deep dive in docs/rails-comparison.md; summary in README.
Encode each stream payload once per channel identifier instead of once per subscriber (~2x faster broadcasts). Stock Action Cable has no equivalent, so this measures the optimized actioncable-next path on the Async::Cable/Falcon target.
actioncable-next fastlane sends pre-encoded frames via Socket#raw_transmit, which async-cable 0.3.1's Socket does not implement; without this shim every fastlane broadcast raises NoMethodError and delivery is 0%.
throughput.ts / bench-runner /bench-throughput-anycable / throughput-multi.ts now accept channel + acProtocol, mirroring the jitter path, so the throughput suite can target a Rails BenchmarkChannel over the base protocol instead of only anycable-go $pubsub over the extended protocol.
The runner reads req.query.intervalMs; throughput-multi sent 'interval', so every run silently used the 100ms default and ignored the requested rate. Coordinator-only fix; no runner rebuild needed.
Pin async-cable to @27181dff1 (native Socket#raw_transmit, Rails 8.1 compatible) and drop the raw_transmit shim. Vendor Async::Cable::Executor (from async-cable dddef54c, whose released form requires edge Rails 8.2) and install it via ActionCable::Server::Base#executor, so broadcast-delivery callbacks (SubscriberMap::Async#invoke_callback -> executor.post) run on the reactor instead of bouncing through Action Cable's thread pool. This is the documented fix for Falcon broadcast latency; re-measure vs the 0.3.1 numbers.
Stock Rails puma.rb omits the workers directive, so WEB_CONCURRENCY was ignored and the Action Cable target ran a single Puma process regardless of the env var. Set it explicitly so Puma's process count matches the Falcon target's falcon --count for a matched WS-engine comparison.
…/cableUrl) Was hardcoded to /bench-avalanche-socketio with no Rails param passthrough. Now selects /bench-avalanche-<protocol> (defaults to anycable for rails-* services) and forwards channel/acProtocol/cableUrl, so the deploy-survival test can drive Action Cable / Solid Cable / Async::Cable / AnyCable.
Adapts avalanche-multi-uws.ts to the /bench-avalanche-anycable endpoint so the post-redeploy reconnect storm is generated across many bench-runners (~250 clients each) instead of one Node process, removing the load-generator limit on the deploy-survival test. Fires one serviceInstanceRedeploy, aggregates time-to-95%-reconnect across shards. prearm/recovery tuned under Railway's 5-min proxy timeout.
The @anycable/core client's default reconnect backoff is multi-second, so the resume-tail p99 after a transient drop is dominated by reconnect wait, not server delivery. Add reconnectBaseMs (job param + RECONNECT_BASE_MS env): first reconnect fires in ~base ms (then x2 up to 5s). Set ~200 to collapse the tail.
The AnyCable jitter loop force-closed the socket then waited jitterDurationMs, during which the client's Monitor reconnected on its backoff -> the offline period was the backoff delay, not a fixed outage, so delivery depended on the client's reconnect config. Now re-terminate any reconnect until the window elapses, so it measures a standard 2s drop; the client backoff only governs recovery speed after the outage.
Re-terminating fought the Monitor (flapping reconnects + backoff escalation). Instead cable.disconnect() emits close -> Monitor cancels reconnect, client stays cleanly offline for the outage window (sid retained), then connect() reconnects once and AnyCable resumes. Outage length is now fixed and backoff-independent, a true standard network drop.
Add @rails/actioncable and a clientLib=actioncable path in the jitter runner so Action Cable / Solid Cable / Async::Cable are driven by the official Rails client (base protocol, its own reconnect monitor, no resume), while AnyCable keeps @anycable/core (extended protocol, resume). Realistic per-server client instead of using @anycable/core for everything.
Its ConnectionMonitor calls addEventListener/removeEventListener and reads document.visibilityState, which don't exist in Node -> ReferenceError. Provide no-op stubs before the client loads.
… Node shim Jitter: Action Cable family now drops the socket uncleanly and recovers on the official client's own poll-based monitor (native, seconds) rather than a forced immediate reconnect. Avalanche: same clientLib branch so deploy-survival uses each server's real client. Extract the Node WebSocket+globals shim to a shared module.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Target apps, harness changes, and results for the Rails WebSocket adapter comparison behind anycable.io/compare/rails-actioncable.
Target apps
cable-bench/— Rails 8.1 app;BENCH_MODEselects Action Cable (Redis) or Solid Cable (database). Also serves as the AnyCable gRPC RPC backend.cable-bench-falcon/— the same app booted on Falcon viaactioncable-next+async-cable(the AsyncCable target).Harness
idle-multi.ts: forwardCHANNEL/AC_PROTOCOLand send the bench-runner auth token, so idle/capacity runs can target a real Rails channeljitter-multi.ts: forwardCHANNEL+AC_PROTOCOLfor Rails targetsidle-runner.ts/jitter-runners.ts/server.ts:channel+acProtocolparamstests-manifest.ts: Rails latency/jitter/idle/avalanche/capacity specsResults (sharded, one shared-tenant Railway window)
backend/results/rails-sharded-2026-06-28.json— latency, jitter, 10K, idle, avalanchebackend/results/rails-capacity-break-2026-06-28.json— idle-to-break per adapter (AnyCable held 600K with 0 failures)Deep dive in
docs/rails-comparison.md; summary section inREADME.md.