falcon-rails 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/http-streaming.md +3 -3
- data/context/job-processing.md +4 -4
- data/context/real-time-views.md +8 -8
- data/context/server-sent-events.md +6 -6
- data/context/websockets.md +3 -3
- data/lib/falcon/rails/version.rb +4 -1
- data/report.md +623 -0
- data/skills/falcon-rails-migration/SKILL.md +35 -0
- data/skills/falcon-rails-streaming-sse/SKILL.md +69 -0
- data/skills/falcon-rails-websockets/SKILL.md +60 -0
- data.tar.gz.sig +0 -0
- metadata +20 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfe0796114a228549fa545788256afa15a5497b9db09e6c4054ff88eaa99809b
|
|
4
|
+
data.tar.gz: 3d2d17280ddb2b577f45fe142e5747de8eec1954dab09b8054aba8dd38c529e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c64e44c28cbc6dc2cfb35cd5264ee2ddd5f77020116ffbd84ccac78f10ba29f102b40ca40d20dafa3217d77b3b6fc714094dd4c56cf73165de2f2dc64e84fb3
|
|
7
|
+
data.tar.gz: b1dee5a5b5ef3aa84868306e2c34a89c5fa307002dd13f2cc07a48e12ff9251171e7f79f827cd00e3f87b67f66745de861600fe946c30536cf304c97bfe6c6db
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/http-streaming.md
CHANGED
|
@@ -42,7 +42,7 @@ class StreamingController < ApplicationController
|
|
|
42
42
|
sleep 1
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
self.response = Rack::Response[200, {"content-type" => "text/plain"}, body]
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -137,8 +137,8 @@ Add routes to your `config/routes.rb`:
|
|
|
137
137
|
```ruby
|
|
138
138
|
Rails.application.routes.draw do
|
|
139
139
|
# Streaming Example:
|
|
140
|
-
get
|
|
141
|
-
get
|
|
140
|
+
get "streaming/index" # Page with streaming JavaScript
|
|
141
|
+
get "streaming/stream" # HTTP streaming endpoint
|
|
142
142
|
end
|
|
143
143
|
```
|
|
144
144
|
|
data/context/job-processing.md
CHANGED
|
@@ -44,10 +44,10 @@ end
|
|
|
44
44
|
Configure async-job queues in `config/initializers/async_job.rb`:
|
|
45
45
|
|
|
46
46
|
```ruby
|
|
47
|
-
require
|
|
48
|
-
require
|
|
49
|
-
require
|
|
50
|
-
require
|
|
47
|
+
require "async/job"
|
|
48
|
+
require "async/job/processor/aggregate"
|
|
49
|
+
require "async/job/processor/redis"
|
|
50
|
+
require "async/job/processor/inline"
|
|
51
51
|
|
|
52
52
|
Rails.application.configure do
|
|
53
53
|
config.async_job.define_queue "default" do
|
data/context/real-time-views.md
CHANGED
|
@@ -56,7 +56,7 @@ pin "live"
|
|
|
56
56
|
Create a `Live::View` class that handles the real-time logic:
|
|
57
57
|
|
|
58
58
|
```ruby
|
|
59
|
-
require
|
|
59
|
+
require "live"
|
|
60
60
|
|
|
61
61
|
class ClockTag < Live::View
|
|
62
62
|
def initialize(...)
|
|
@@ -86,7 +86,7 @@ class ClockTag < Live::View
|
|
|
86
86
|
def forward_event(name)
|
|
87
87
|
"event.preventDefault(); live.forwardEvent(#{JSON.dump(@id)}, event, {name: #{name.inspect}})"
|
|
88
88
|
end
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
def render(builder)
|
|
91
91
|
builder.tag(:div, class: "clock-container") do
|
|
92
92
|
builder.tag(:h2) {builder.text("Live Clock")}
|
|
@@ -110,13 +110,13 @@ end
|
|
|
110
110
|
Create a controller to handle the Live::View connection:
|
|
111
111
|
|
|
112
112
|
```ruby
|
|
113
|
-
require
|
|
113
|
+
require "async/websocket/adapters/rails"
|
|
114
114
|
|
|
115
115
|
class ClockController < ApplicationController
|
|
116
116
|
RESOLVER = Live::Resolver.allow(ClockTag)
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
def index
|
|
119
|
-
@tag = ClockTag.new(
|
|
119
|
+
@tag = ClockTag.new("clock")
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
skip_before_action :verify_authenticity_token, only: :live
|
|
@@ -200,7 +200,7 @@ end
|
|
|
200
200
|
class CounterTag < Live::View
|
|
201
201
|
def initialize(count: 0)
|
|
202
202
|
super
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
# @data is persisted on the tag in `data-` attributes.
|
|
205
205
|
@data["count"] = @data.fetch("count", count).to_i
|
|
206
206
|
end
|
|
@@ -230,10 +230,10 @@ class CounterTag < Live::View
|
|
|
230
230
|
def forward_event(name)
|
|
231
231
|
"event.preventDefault(); live.forwardEvent(#{JSON.dump(@id)}, event, {name: #{name.inspect}})"
|
|
232
232
|
end
|
|
233
|
-
|
|
233
|
+
|
|
234
234
|
def render(builder)
|
|
235
235
|
builder.tag(:div, class: "counter-container") do
|
|
236
|
-
builder.tag(:h2) {
|
|
236
|
+
builder.tag(:h2) {builder.text("Live Counter")}
|
|
237
237
|
|
|
238
238
|
builder.tag(:div, id: "counter", class: "counter-display") do
|
|
239
239
|
builder.text(@count.to_s)
|
|
@@ -30,11 +30,11 @@ class SseController < ApplicationController
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
EVENT_STREAM_HEADERS = {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
"content-type" => "text/event-stream",
|
|
34
|
+
"cache-control" => "no-cache",
|
|
35
|
+
"connection" => "keep-alive"
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
def events
|
|
39
39
|
body = proc do |stream|
|
|
40
40
|
while true
|
|
@@ -108,8 +108,8 @@ Add routes to your `config/routes.rb`:
|
|
|
108
108
|
```ruby
|
|
109
109
|
Rails.application.routes.draw do
|
|
110
110
|
# SSE Example:
|
|
111
|
-
get
|
|
112
|
-
get
|
|
111
|
+
get "sse/index" # Page with EventSource JavaScript
|
|
112
|
+
get "sse/events" # SSE endpoint
|
|
113
113
|
end
|
|
114
114
|
```
|
|
115
115
|
|
data/context/websockets.md
CHANGED
|
@@ -23,15 +23,15 @@ WebSockets provide full-duplex communication between client and server over a si
|
|
|
23
23
|
Create a controller that handles WebSocket connections:
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
-
require
|
|
26
|
+
require "async/websocket/adapters/rails"
|
|
27
27
|
|
|
28
28
|
class ChatController < ApplicationController
|
|
29
29
|
def index
|
|
30
30
|
# Render the page with WebSocket JavaScript
|
|
31
31
|
end
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
skip_before_action :verify_authenticity_token, only: :connect
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
def connect
|
|
36
36
|
self.response = Async::WebSocket::Adapters::Rails.open(request) do |connection|
|
|
37
37
|
Sync do
|
data/lib/falcon/rails/version.rb
CHANGED
data/report.md
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
# Toward Falcon as the Default Rails Server
|
|
2
|
+
|
|
3
|
+
This report assesses the work required to make Falcon a credible future default
|
|
4
|
+
application server for Ruby on Rails. It covers Rails, Rack, Falcon, Action Cable,
|
|
5
|
+
Active Record, streaming, Solid Queue, development reloading, and deployment.
|
|
6
|
+
|
|
7
|
+
The research snapshot is **2026-08-04**. At that point the latest stable releases
|
|
8
|
+
were Rails 8.1.3.1, Falcon 0.56.0, Rack 3.2.6, Async Cable 0.3.1, and Solid Queue
|
|
9
|
+
1.6.0. Rails `main` was developing Rails 8.2.
|
|
10
|
+
|
|
11
|
+
## Executive position
|
|
12
|
+
|
|
13
|
+
Falcon has a plausible path to becoming the Rails default, but it should not be
|
|
14
|
+
made the default yet.
|
|
15
|
+
|
|
16
|
+
The architectural blockers are falling away:
|
|
17
|
+
|
|
18
|
+
- Rails has supported fiber-scoped execution state for several releases and its
|
|
19
|
+
[configuration guide][rails-isolation-guide] tells fiber-based servers such as
|
|
20
|
+
Falcon to select it.
|
|
21
|
+
- Active Record ownership is keyed through `IsolatedExecutionState`, and modern
|
|
22
|
+
connection APIs allow connections to be borrowed for an individual operation
|
|
23
|
+
instead of pinned for an entire request or job.
|
|
24
|
+
- Rails merged [Action Cable server adapterization][rails-ac-adapter-pr] in May
|
|
25
|
+
2026. Async Cable now targets the resulting Rails 8.2 API instead of requiring
|
|
26
|
+
the temporary `actioncable-next` fork.
|
|
27
|
+
- Solid Queue 1.6.0 added [bounded fiber worker execution][solid-queue-fiber-pr]
|
|
28
|
+
in July 2026, while retaining process isolation and thread workers as defaults.
|
|
29
|
+
- Rack 3 defines callable streaming bodies, protocol upgrades, completion
|
|
30
|
+
callbacks, and HTTP/2-aware `rack.protocol` semantics.
|
|
31
|
+
|
|
32
|
+
The remaining work is less glamorous but decisive for a default: eliminate known
|
|
33
|
+
correctness failures, make development reloading reliable, make the Rails command
|
|
34
|
+
and generated deployment topology unsurprising, test the stock Rails stack rather
|
|
35
|
+
than only the optimized Async stack, and establish sustained production evidence.
|
|
36
|
+
|
|
37
|
+
The recommended progression is:
|
|
38
|
+
|
|
39
|
+
1. Make Falcon a first-class, continuously tested Rails server option.
|
|
40
|
+
2. Offer and document `rails new --server=falcon` without changing the default.
|
|
41
|
+
3. Ship a release-candidate period with large reference applications and public
|
|
42
|
+
compatibility results.
|
|
43
|
+
4. Change the generated default only after the acceptance gates in this report
|
|
44
|
+
have held across at least one stable Rails release cycle.
|
|
45
|
+
|
|
46
|
+
The relevant measure is not whether Falcon can serve a Rails request—it can—but
|
|
47
|
+
whether a newly generated, otherwise ordinary Rails application behaves correctly
|
|
48
|
+
without its author understanding fiber schedulers.
|
|
49
|
+
|
|
50
|
+
## What “the default” means
|
|
51
|
+
|
|
52
|
+
Rails currently expresses a server preference in several different places. A
|
|
53
|
+
successful proposal needs to address all of them deliberately.
|
|
54
|
+
|
|
55
|
+
| Surface | Current state | Required Falcon outcome |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| Generated bundle | The app generator hard-codes `puma >= 7.1` in [`web_server_gemfile_entry`][rails-app-generator]. | Generate Falcon, with Puma retained as an explicit escape hatch. |
|
|
58
|
+
| `bin/rails server` | Rails delegates to Rackup, but hard-codes Puma as its recommended missing server and lists Falcon as an available handler in [`ServerCommand`][rails-server-command]. | Falcon must work through the normal Rails command, flags, restart behavior, logging, and URL reporting. |
|
|
59
|
+
| Rackup discovery | Rackup tries Puma, then Falcon, then WEBrick when no handler is selected in [`Handler.default`][rackup-handler]. | Merely replacing the Gemfile entry can select Falcon, but discovery order and diagnostics should become intentional rather than incidental. |
|
|
60
|
+
| Development | Puma is the documented and tested development path. | Reloading, console output, debugger/system tests, HTTPS expectations, and interrupt/restart behavior must be reliable. |
|
|
61
|
+
| Generated production deployment | Rails generates Puma configuration and integrates it with Thruster, Docker, Kamal, and optionally the Solid Queue Puma plugin. | Generate a documented Falcon production service with equivalent environment-variable, health-check, signal, and proxy behavior. |
|
|
62
|
+
| Rails documentation | Rails calls Puma the default and its performance guide focuses on Puma. | Document both the compatibility baseline and the characteristics that differ under fiber concurrency. |
|
|
63
|
+
|
|
64
|
+
There is also an important distinction between Falcon's two launch paths:
|
|
65
|
+
|
|
66
|
+
- The [`Falcon::Rackup::Handler`][falcon-rackup-handler] used by `bin/rails
|
|
67
|
+
server` currently creates one HTTP/1 server in the current process. It is a
|
|
68
|
+
useful development bridge, but it is not Falcon's recommended production
|
|
69
|
+
architecture and ignores many Rails/Rackup options.
|
|
70
|
+
- Falcon recommends `falcon host` and a `falcon.rb` service definition for
|
|
71
|
+
production. That path provides worker processes, preloading, supervision, and
|
|
72
|
+
richer endpoint configuration, but is not generated or managed by Rails.
|
|
73
|
+
|
|
74
|
+
Before a default change, these paths should either converge or have an explicit
|
|
75
|
+
division: `bin/rails server` for development and a generated Falcon service for
|
|
76
|
+
production. Passing options that Falcon silently ignores is not acceptable for a
|
|
77
|
+
default.
|
|
78
|
+
|
|
79
|
+
## Compatibility overview
|
|
80
|
+
|
|
81
|
+
| Area | Position on 2026-08-04 | Risk before default |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| Ordinary Rack requests | Fundamentally compatible; Falcon adapts Protocol::HTTP to Rack 3. | Medium: request-body semantics and third-party middleware still expose differences. |
|
|
84
|
+
| Rails execution state | Rails supports `:fiber`; Falcon's Railtie selects it globally. | Medium: propagation into child fibers and activation/configuration semantics need a clear contract. |
|
|
85
|
+
| Active Record | Core ownership is fiber-aware and substantially improved. | Medium-high: pinned connections, driver behavior, pool sizing, roles/shards, and open reports need stress coverage. |
|
|
86
|
+
| Action Cable | Rails 8.2 has the required adapter seam; Async Cable is the reference native transport. | High until Rails 8.2 integration, pub/sub combinations, HTTP/1 and HTTP/2, and shutdown are comprehensively tested. |
|
|
87
|
+
| Response streaming/SSE | Falcon and Rack 3 have a strong native model. | Medium-high: Rails `ActionController::Live` remains thread-based and Rack middleware callable-body support is incomplete. |
|
|
88
|
+
| Request bodies/uploads | Network bodies are intentionally streamed and not universally rewindable. | High: common gems still assume Puma-like buffering; known open reports can lose or empty a body. |
|
|
89
|
+
| Development reload | Recent Rails fixes improve fiber ownership in the reloader. | High: a current open Falcon issue still reproduces a total stall on Rails `main`. |
|
|
90
|
+
| Solid Queue | Version 1.6.0 offers opt-in Async fiber workers. | Low for basic compatibility; medium for presenting fiber workers as a default or performance promise. |
|
|
91
|
+
| Active Storage | Normal operation needs a formal matrix; sharing a record across fibers exposes existing races. | Medium. |
|
|
92
|
+
| Operations | Falcon has supervision, metrics, HTTP/2, and preloading. | Medium-high: graceful restart/drain, configuration, logging defaults, and memory accounting remain adoption friction. |
|
|
93
|
+
| Ecosystem middleware | Rack 3 is the right common contract. | Medium-high: callable streaming bodies, locality assumptions, blocking native work, and rewind assumptions require auditing. |
|
|
94
|
+
|
|
95
|
+
## Detailed findings
|
|
96
|
+
|
|
97
|
+
### 1. Execution locality is supported, but it is a global application contract
|
|
98
|
+
|
|
99
|
+
Falcon's [Railtie][falcon-railtie] unconditionally sets:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
config.active_support.isolation_level = :fiber
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Rails [documents this as the right setting for Falcon][rails-isolation-guide]. Rails' current
|
|
106
|
+
[`IsolatedExecutionState`][rails-isolated-state] keys state by either `Thread` or
|
|
107
|
+
`Fiber`, and Active Record uses that context for connection ownership. This is a
|
|
108
|
+
major improvement over the historical state captured in [Rails #42271][rails-ar-fiber].
|
|
109
|
+
|
|
110
|
+
However, the setting changes the locality of `CurrentAttributes`, query caches,
|
|
111
|
+
connection leases, execution wrappers, error context, and application/library
|
|
112
|
+
state across the entire process. It is not merely a Falcon tuning switch.
|
|
113
|
+
|
|
114
|
+
Two behaviors need to be made explicit:
|
|
115
|
+
|
|
116
|
+
- A request fiber is isolated correctly from another request fiber.
|
|
117
|
+
- A child fiber created inside a request does not automatically inherit arbitrary
|
|
118
|
+
`IsolatedExecutionState`. Rails concluded in [#48279][rails-current-attributes]
|
|
119
|
+
that callers must propagate required state explicitly. Async libraries and Rails
|
|
120
|
+
APIs need one documented propagation mechanism for request ID, tenant, database
|
|
121
|
+
role/shard, `CurrentAttributes`, tracing, and error-report context.
|
|
122
|
+
|
|
123
|
+
The server choice should also own activation clearly. Requiring the Falcon gem can
|
|
124
|
+
load its Railtie even if the process is later launched with another server. For a
|
|
125
|
+
default-quality integration, Rails should set or validate the execution model
|
|
126
|
+
explicitly, or Falcon should activate it only through a well-defined integration
|
|
127
|
+
point. Hidden global behavior based on Gemfile presence is hard to diagnose.
|
|
128
|
+
|
|
129
|
+
### 2. Active Record is no longer an architectural blocker, but it remains a gate
|
|
130
|
+
|
|
131
|
+
Active Record leases connections against
|
|
132
|
+
`ActiveSupport::IsolatedExecutionState.context`. Its current API distinguishes:
|
|
133
|
+
|
|
134
|
+
- `with_connection`, which ordinarily returns a borrowed connection after the
|
|
135
|
+
block;
|
|
136
|
+
- `lease_connection`, which intentionally pins one for the request/job context;
|
|
137
|
+
- the legacy `connection` accessor, whose permanent checkout is being deprecated.
|
|
138
|
+
|
|
139
|
+
See [`connection_handling.rb`][rails-ar-connection-handling] and the pool
|
|
140
|
+
implementation in [`connection_pool.rb`][rails-ar-pool]. This model is compatible
|
|
141
|
+
with a fiber-per-request server and can use far fewer connections than the number
|
|
142
|
+
of in-flight fibers when application code does not pin a connection across waits.
|
|
143
|
+
|
|
144
|
+
The default-server standard must nevertheless cover:
|
|
145
|
+
|
|
146
|
+
- PostgreSQL, MySQL/mysql2, and SQLite under mixed concurrent I/O;
|
|
147
|
+
- transactions and savepoints across scheduler yields;
|
|
148
|
+
- roles, shards, `connected_to`, query cache, asynchronous queries, and schema
|
|
149
|
+
loading;
|
|
150
|
+
- disconnect/reconnect, network failure, cancellation, and executor cleanup;
|
|
151
|
+
- pool exhaustion with thousands of request fibers;
|
|
152
|
+
- libraries calling `connection`, `lease_connection`, raw connection methods, or
|
|
153
|
+
long-lived `with_connection` blocks;
|
|
154
|
+
- native extension behavior that releases the GVL but does not cooperate with a
|
|
155
|
+
fiber scheduler in the expected way.
|
|
156
|
+
|
|
157
|
+
Open [Rails #57926][rails-mysql-fiber] reports mysql2 detecting a connection used
|
|
158
|
+
by another fiber even with fiber isolation configured. It is not yet established
|
|
159
|
+
as a Rails defect, but it illustrates why ownership needs load tests and diagnostic
|
|
160
|
+
messages rather than assumptions.
|
|
161
|
+
|
|
162
|
+
`ActionController::Live` complicates the model further. Its
|
|
163
|
+
[implementation][rails-live-source] still
|
|
164
|
+
runs the action in a cached thread pool, copies thread-local values, and calls
|
|
165
|
+
`IsolatedExecutionState.share_with`; Rails' own source calls this “very much a
|
|
166
|
+
hack” and says streaming should be rethought. Longstanding [Rails #21209][rails-live-ar]
|
|
167
|
+
and the more recent [#52906][rails-live-connected-to] show the interaction with
|
|
168
|
+
Active Record connection and role state.
|
|
169
|
+
|
|
170
|
+
### 3. Action Cable now has the right upstream seam
|
|
171
|
+
|
|
172
|
+
Stock Action Cable historically owns its transport and concurrency:
|
|
173
|
+
|
|
174
|
+
- WebSockets use `websocket-driver` and Rack full hijack.
|
|
175
|
+
- A dedicated NIO event loop reads and writes sockets.
|
|
176
|
+
- Connection/channel work and internal pub/sub work use separate thread pools.
|
|
177
|
+
- Production subscription adapters run listener threads.
|
|
178
|
+
|
|
179
|
+
This means running stock Action Cable behind Falcon does not make Cable fiber
|
|
180
|
+
native, limits WebSockets to the HTTP/1 hijack model, and gives Falcon little
|
|
181
|
+
control over connection lifetime. The history and design constraints are well
|
|
182
|
+
captured in [Rails #35657][rails-ac-async]. Rack itself now notes that full hijack
|
|
183
|
+
only works with HTTP/1, while `rack.protocol` and callable streaming bodies are the
|
|
184
|
+
forward-compatible HTTP/2+ mechanisms; see the [Rack specification][rack-spec].
|
|
185
|
+
|
|
186
|
+
The major positive development is merged [Rails PR #50979][rails-ac-adapter-pr].
|
|
187
|
+
It separates the application connection from the low-level server socket and
|
|
188
|
+
narrows the interfaces for transport, worker execution, pub/sub, and timers.
|
|
189
|
+
|
|
190
|
+
[Async Cable][async-cable] uses that abstraction to accept WebSockets through
|
|
191
|
+
`async-websocket`, drive the connection in Async tasks, and reuse Rails channel
|
|
192
|
+
and connection code. Its gemspec now depends on `actioncable >= 8.2.0.alpha`, so
|
|
193
|
+
Rails 8.2 is the first upstream-native baseline.
|
|
194
|
+
|
|
195
|
+
Work still required:
|
|
196
|
+
|
|
197
|
+
- Run the Action Cable conformance suite and browser/system tests against both
|
|
198
|
+
stock and Async transports, Redis, PostgreSQL, Solid Cable, and the test/async
|
|
199
|
+
adapters.
|
|
200
|
+
- Verify HTTP/1 Upgrade and HTTP/2 Extended CONNECT, proxies, TLS, origin checks,
|
|
201
|
+
cookies/sessions, authentication middleware, reconnect, backpressure, slow
|
|
202
|
+
consumers, and large broadcasts.
|
|
203
|
+
- Make the executor and pub/sub scheduling contract complete. Rails' built-in
|
|
204
|
+
Redis and PostgreSQL listeners still start threads. Async Cable has just added
|
|
205
|
+
an `Async::Cable::Executor` on `main`, but its lifecycle and configuration need
|
|
206
|
+
to be wired and released coherently.
|
|
207
|
+
- Resolve compatibility reports such as [async-cable #3][async-cable-solid], in
|
|
208
|
+
which the Solid Cable adapter and Async Cable disagree about server state.
|
|
209
|
+
- Fix and soak development reloading, including open [Falcon #325][falcon-cable-reload].
|
|
210
|
+
- Define graceful close and drain behavior for deploys, server restarts, and Rails
|
|
211
|
+
reloads.
|
|
212
|
+
|
|
213
|
+
The desired endpoint is one Action Cable application API with selectable server
|
|
214
|
+
transports—not a permanent alternate fork of Action Cable.
|
|
215
|
+
|
|
216
|
+
### 4. Rails streaming should converge on Rack 3 callable bodies
|
|
217
|
+
|
|
218
|
+
Falcon's strongest use cases—SSE, generated downloads, LLM responses, and
|
|
219
|
+
real-time views—are also where Rails currently has overlapping abstractions:
|
|
220
|
+
|
|
221
|
+
- ordinary enumerable Rack bodies;
|
|
222
|
+
- Rack 3 callable streaming bodies (`body.call(stream)`);
|
|
223
|
+
- `ActionController::Live`, which introduces a producer thread and a queue;
|
|
224
|
+
- template streaming;
|
|
225
|
+
- WebSocket/hijack paths.
|
|
226
|
+
|
|
227
|
+
Rack 3 and Falcon can stream a callable body directly with natural backpressure
|
|
228
|
+
and cancellation. That should become the preferred Rails primitive. It avoids a
|
|
229
|
+
thread hop under Falcon and works across HTTP versions.
|
|
230
|
+
|
|
231
|
+
Compatibility is not complete across the middleware stack. Open [Rack #2470][rack-deflater]
|
|
232
|
+
shows `Rack::Deflater` calling `each` on a callable body, contrary to the Rack 3
|
|
233
|
+
contract. [Rails #23828][rails-template-streaming] documents longstanding and
|
|
234
|
+
surprising template-streaming behavior. Middleware for ETag, compression,
|
|
235
|
+
instrumentation, error pages, sessions, and proxies must be tested with both body
|
|
236
|
+
forms and client disconnects.
|
|
237
|
+
|
|
238
|
+
Rails should consider a scheduler-neutral controller streaming API implemented
|
|
239
|
+
on callable bodies. `ActionController::Live` can remain as a compatibility layer,
|
|
240
|
+
but its thread copying should not define the future model.
|
|
241
|
+
|
|
242
|
+
### 5. Request body rewind is an immediate correctness issue
|
|
243
|
+
|
|
244
|
+
A socket is not rewindable without buffering. Rack 3 therefore requires
|
|
245
|
+
`rack.input` to support `gets`, `each`, and `read`, but no longer requires
|
|
246
|
+
universal rewind. Falcon preserves streaming request bodies to avoid buffering
|
|
247
|
+
large uploads.
|
|
248
|
+
|
|
249
|
+
That correct design exposes a widespread ecosystem assumption: Puma commonly
|
|
250
|
+
presents a buffered/rewindable body, so middleware reads it, rewinds it, and lets
|
|
251
|
+
another layer read it again. Under Falcon that can become an empty body.
|
|
252
|
+
|
|
253
|
+
Relevant open reports include:
|
|
254
|
+
|
|
255
|
+
- [Falcon #302][falcon-request-body], where JSON input disappears before Rails or
|
|
256
|
+
Grape parses it;
|
|
257
|
+
- [Falcon #310][falcon-body-rewind], covering Grape and HMAC verification after an
|
|
258
|
+
earlier body read;
|
|
259
|
+
- [protocol-rack #33][protocol-rack-rewind], where `Input#rewind` falsely reported
|
|
260
|
+
success in a released version and a reverse proxy forwarded an empty body.
|
|
261
|
+
|
|
262
|
+
Current protocol-rack `main` includes a selective
|
|
263
|
+
[`Rewindable` middleware][protocol-rack-rewindable] for conventional form media
|
|
264
|
+
types, but its `Input#rewind` still ignores a false result from the underlying
|
|
265
|
+
body—the defect reported by #33. The broader compatibility policy also remains
|
|
266
|
+
unsettled. The Rails/Falcon integration needs to choose and document one of these
|
|
267
|
+
approaches:
|
|
268
|
+
|
|
269
|
+
1. Buffer media types and request sizes that Rails and common middleware
|
|
270
|
+
conventionally expect to be rewindable, with explicit memory/disk limits.
|
|
271
|
+
2. Keep all bodies streaming, make failed rewind unmistakable, and migrate the
|
|
272
|
+
ecosystem to single-pass or explicitly buffered APIs.
|
|
273
|
+
|
|
274
|
+
A pragmatic default may combine both: compatibility buffering for bounded form
|
|
275
|
+
and JSON requests, streaming for large/unknown bodies, and an application API to
|
|
276
|
+
opt in or out. This area needs security testing as well as functional testing:
|
|
277
|
+
signature verification, CSRF parsing, multipart uploads, reverse proxies, and
|
|
278
|
+
content-length handling must never silently operate on different bytes.
|
|
279
|
+
|
|
280
|
+
### 6. Development reloading remains a release blocker
|
|
281
|
+
|
|
282
|
+
Rails recently merged [PR #57423][rails-reloader-fiber] to key the reloader share
|
|
283
|
+
lock by fiber execution context and [PR #57425][rails-reloader-hijack] to release
|
|
284
|
+
the share around a hijacked response. These changes fix real fiber-concurrency
|
|
285
|
+
defects and are strong evidence of upstream progress.
|
|
286
|
+
|
|
287
|
+
However, [Falcon #359][falcon-reload-stall] reports that editing a controller in a
|
|
288
|
+
new Rails app causes every later request to stall, including on Rails 8.2 alpha
|
|
289
|
+
and a Rails `main` revision containing those fixes. [Falcon #325][falcon-cable-reload]
|
|
290
|
+
separately reports unloaded constants during Async Cable reconnects.
|
|
291
|
+
|
|
292
|
+
No server should become the Rails development default until a repeatable suite
|
|
293
|
+
can run reload cycles while ordinary requests, Cable connections, streaming
|
|
294
|
+
responses, and background tasks are active. The suite should detect both deadlock
|
|
295
|
+
and stale-class use.
|
|
296
|
+
|
|
297
|
+
### 7. Solid Queue fiber workers are a useful convergence signal
|
|
298
|
+
|
|
299
|
+
Solid Queue 1.6.0's [fiber worker mode][solid-queue-fiber-pr] runs a bounded number
|
|
300
|
+
of jobs as Async tasks on one reactor thread. The implementation is intentionally
|
|
301
|
+
separate from supervisor mode: the recommended supervisor still forks processes,
|
|
302
|
+
and each worker can choose either `threads: N` or `fibers: N`.
|
|
303
|
+
|
|
304
|
+
Important safeguards in the [Solid Queue documentation][solid-queue-readme]
|
|
305
|
+
include:
|
|
306
|
+
|
|
307
|
+
- fiber workers refuse to boot unless Rails isolation is `:fiber`;
|
|
308
|
+
- `threads` and `fibers` are mutually exclusive per worker;
|
|
309
|
+
- fiber mode is recommended for cooperative, mostly I/O-bound jobs;
|
|
310
|
+
- pinned Active Record connections and transactions increase required pool size;
|
|
311
|
+
- process isolation and bounded concurrency remain available.
|
|
312
|
+
|
|
313
|
+
This is a good model for the web-server transition: explicit, bounded, and
|
|
314
|
+
reversible. It is also very new. It should first expand the shared compatibility
|
|
315
|
+
matrix for Rails' execution state and Active Record; it should not be treated as
|
|
316
|
+
proof that arbitrary Rails applications are fiber-safe.
|
|
317
|
+
|
|
318
|
+
Falcon's default-server proposal should work with stock Solid Queue thread
|
|
319
|
+
workers. Fiber workers are an optional optimization. Similarly, `falcon-rails`
|
|
320
|
+
currently bundles the separate Async Job adapter, but becoming Rails' default
|
|
321
|
+
should not require replacing Rails' default job backend.
|
|
322
|
+
|
|
323
|
+
### 8. Active Storage and application object safety need concurrency guidance
|
|
324
|
+
|
|
325
|
+
[Rails #52660][rails-active-storage-async] reproduces failures when the same
|
|
326
|
+
Active Record instance and attachment proxy are mutated concurrently from fibers.
|
|
327
|
+
The same race can be reproduced with threads; using independently loaded/cloned
|
|
328
|
+
records avoids it. This is not uniquely a Falcon defect, but Falcon makes such
|
|
329
|
+
concurrency easier and therefore makes undocumented object-sharing assumptions
|
|
330
|
+
more visible.
|
|
331
|
+
|
|
332
|
+
The compatibility program should test standard direct uploads, proxy downloads,
|
|
333
|
+
streaming downloads, checksums, variants, and local/cloud services. Rails guides
|
|
334
|
+
should state that model instances and mutable attachment state are not safe to
|
|
335
|
+
share between concurrent tasks.
|
|
336
|
+
|
|
337
|
+
### 9. Early Hints and protocol features need contract tests
|
|
338
|
+
|
|
339
|
+
Rails exposes Early Hints through `env["rack.early_hints"]`. Falcon can send
|
|
340
|
+
interim responses through its underlying Protocol::HTTP request and advertises
|
|
341
|
+
Early Hints support in its documentation. However, current protocol-rack `main`
|
|
342
|
+
does not appear to populate `rack.early_hints`, while Falcon's current interim
|
|
343
|
+
response guide tells applications to use the non-Rack
|
|
344
|
+
`env["protocol.http.request"]` extension.
|
|
345
|
+
|
|
346
|
+
This documentation/implementation mismatch should be resolved before claiming
|
|
347
|
+
Rails feature parity. The same applies to HTTP/2 WebSockets, trailers, streaming
|
|
348
|
+
uploads, completion callbacks, and cancellation: each feature needs an executable
|
|
349
|
+
cross-server contract test, not only documentation.
|
|
350
|
+
|
|
351
|
+
### 10. Operations and packaging are part of compatibility
|
|
352
|
+
|
|
353
|
+
The current Falcon issue tracker shows several default-quality gaps:
|
|
354
|
+
|
|
355
|
+
- [#188][falcon-graceful-restart]: graceful restart can break in-flight requests.
|
|
356
|
+
- [#344][falcon-supervisor-memory]: preloaded supervisor memory is over-counted on
|
|
357
|
+
RSS-billed platforms and may require a different process topology.
|
|
358
|
+
- [#363][falcon-memory-footprint]: a production migration observed a higher memory
|
|
359
|
+
baseline and possible growth; the investigation needs PSS/process-tree and heap
|
|
360
|
+
data.
|
|
361
|
+
- [#127][falcon-configuration-docs]: users still struggle to discover production
|
|
362
|
+
configuration such as workers and Unix sockets.
|
|
363
|
+
- [#90][falcon-request-logging]: request logging behavior and verbosity are not
|
|
364
|
+
obvious.
|
|
365
|
+
- [falcon-rails #4][falcon-rails-logging]: the convenience gem replaces Rails'
|
|
366
|
+
logger and stops writing `log/development.log` without an explicit opt-in.
|
|
367
|
+
- [falcon-rails #3][falcon-rails-full-rails]: the convenience gem pulls the entire
|
|
368
|
+
Rails meta-gem and illustrates that it is broader than a server adapter.
|
|
369
|
+
|
|
370
|
+
For the Rails default, prefer a small core integration:
|
|
371
|
+
|
|
372
|
+
- `falcon` plus the minimum Rails/Rack bridge;
|
|
373
|
+
- no silent logger replacement;
|
|
374
|
+
- no mandatory alternate job, Cable, or live-view framework;
|
|
375
|
+
- production configuration generated by Rails;
|
|
376
|
+
- stable signal semantics, connection draining, health/readiness, metrics, and
|
|
377
|
+
documented memory accounting.
|
|
378
|
+
|
|
379
|
+
`falcon-rails` can remain a curated opt-in bundle for the fully asynchronous stack,
|
|
380
|
+
but its current behavior is too broad to be the package that Rails silently adds
|
|
381
|
+
as “the web server.”
|
|
382
|
+
|
|
383
|
+
## Proposed roadmap
|
|
384
|
+
|
|
385
|
+
### Phase 0: Define and minimize the integration contract
|
|
386
|
+
|
|
387
|
+
- Agree with Rails core on the meanings of default development server, generated
|
|
388
|
+
production server, and recommended deployment.
|
|
389
|
+
- Split server-essential integration from optional Async Cable, Async Job, Live,
|
|
390
|
+
limiter, and logging integrations.
|
|
391
|
+
- Decide who configures fiber isolation and when.
|
|
392
|
+
- Publish supported Ruby, Rails, Rack, and dependency versions as a tested matrix.
|
|
393
|
+
- Create a shared Rails/Falcon tracking project with owners on both sides.
|
|
394
|
+
|
|
395
|
+
### Phase 1: Make Falcon a first-class generated option
|
|
396
|
+
|
|
397
|
+
- Add `rails new --server=falcon` and a matching skip/selection mechanism.
|
|
398
|
+
- Generate the correct Gemfile, development command, production `falcon.rb`,
|
|
399
|
+
Docker/Kamal command, Thruster configuration, health check, and database-pool
|
|
400
|
+
guidance.
|
|
401
|
+
- Make `bin/rails server` flags either work or fail clearly under Falcon.
|
|
402
|
+
- Add official Rails guides for Falcon configuration and concurrency semantics.
|
|
403
|
+
- Keep Puma as the generated default during this phase.
|
|
404
|
+
|
|
405
|
+
### Phase 2: Establish a compatibility laboratory
|
|
406
|
+
|
|
407
|
+
- Run a generated Rails reference application continuously against Puma and
|
|
408
|
+
Falcon, comparing externally observable behavior rather than internal topology.
|
|
409
|
+
- Add Falcon jobs to relevant Rails component CI suites.
|
|
410
|
+
- Run Rack's specification/lint suite and callable-body tests against Falcon.
|
|
411
|
+
- Run Action Cable conformance, browser, and load tests with Async Cable.
|
|
412
|
+
- Run Solid Queue's thread and fiber worker suites with the same application code.
|
|
413
|
+
- Publish regressions, memory/process metrics, and performance results.
|
|
414
|
+
|
|
415
|
+
### Phase 3: Burn down correctness and lifecycle issues
|
|
416
|
+
|
|
417
|
+
Required before a candidate default:
|
|
418
|
+
|
|
419
|
+
- close the reload stall and stale-constant cases;
|
|
420
|
+
- settle request-body rewind/buffering semantics;
|
|
421
|
+
- complete Rails 8.2 Async Cable integration and common pub/sub adapters;
|
|
422
|
+
- fix callable streaming middleware failures;
|
|
423
|
+
- validate Active Record and driver behavior under pool pressure;
|
|
424
|
+
- provide graceful deploy/restart behavior for ordinary, streaming, and WebSocket
|
|
425
|
+
requests;
|
|
426
|
+
- make logging, errors, and diagnostics Rails-native and unsurprising.
|
|
427
|
+
|
|
428
|
+
### Phase 4: Production candidate program
|
|
429
|
+
|
|
430
|
+
- Recruit applications representing CRUD, high-throughput APIs, Cable/Turbo,
|
|
431
|
+
Active Storage, LLM streaming, multi-database/sharded deployments, and common
|
|
432
|
+
authentication/observability gems.
|
|
433
|
+
- Require multi-week mixed-load soaks and real deploy/restart cycles.
|
|
434
|
+
- Collect CPU, latency, throughput, RSS, PSS, private memory, connection counts,
|
|
435
|
+
scheduler stalls, queue depths, and disconnect/error rates.
|
|
436
|
+
- Document regressions as carefully as wins. Falcon need not win every workload,
|
|
437
|
+
but the default must be safe and predictable.
|
|
438
|
+
|
|
439
|
+
### Phase 5: Change the generated default
|
|
440
|
+
|
|
441
|
+
Only after all gates below are met:
|
|
442
|
+
|
|
443
|
+
- switch the app generator and Rails missing-server recommendation;
|
|
444
|
+
- retain `--server=puma` as an easy, supported choice;
|
|
445
|
+
- provide an upgrade guide that separates correctness requirements from optional
|
|
446
|
+
performance tuning;
|
|
447
|
+
- keep the comparative CI and reference applications permanently.
|
|
448
|
+
|
|
449
|
+
## Acceptance gates
|
|
450
|
+
|
|
451
|
+
### Correctness matrix
|
|
452
|
+
|
|
453
|
+
The matrix should cover at least:
|
|
454
|
+
|
|
455
|
+
| Dimension | Cases |
|
|
456
|
+
| --- | --- |
|
|
457
|
+
| Ruby | All Ruby versions supported by the target Rails release, with and without YJIT where relevant. |
|
|
458
|
+
| Protocol | HTTP/1.1 and HTTP/2; TLS directly and behind common reverse proxies. |
|
|
459
|
+
| Database | PostgreSQL, MySQL/mysql2, SQLite; pool exhaustion; roles and shards. |
|
|
460
|
+
| Request bodies | Empty, fixed, chunked/streamed, JSON, forms, multipart, large uploads, early rejection, read/rewind/re-read. |
|
|
461
|
+
| Response bodies | Enumerable, file, callable streaming, SSE, `send_stream`, errors before/after commit, client disconnect. |
|
|
462
|
+
| Cable | HTTP/1 and HTTP/2, Redis, PostgreSQL, Solid Cable, reconnect, slow client, broadcast fan-out, deploy drain. |
|
|
463
|
+
| Jobs | Solid Queue thread workers and fiber workers; transactions, retries, shutdown, recurring jobs. |
|
|
464
|
+
| Active Storage | Local and cloud services, direct upload, proxy/redirect download, variants, concurrent use. |
|
|
465
|
+
| Development | Reload during requests, streams, Cable connections and jobs; debugger, console, system tests. |
|
|
466
|
+
| Middleware | Sessions, cookies, CSRF, compression, ETag, authentication, request stores, tracing/APM, reverse proxy. |
|
|
467
|
+
|
|
468
|
+
### Reliability gates
|
|
469
|
+
|
|
470
|
+
- No known silent request/response data loss.
|
|
471
|
+
- No reproducible reload deadlock or stale-code execution.
|
|
472
|
+
- No cross-request leakage of identity, tenant, database role, transaction, query
|
|
473
|
+
cache, logging, tracing, or error context.
|
|
474
|
+
- Bounded memory and task growth through long-lived connections and repeated
|
|
475
|
+
reload/deploy cycles.
|
|
476
|
+
- Graceful shutdown completes or explicitly times out while reporting unfinished
|
|
477
|
+
work; it must not silently drop accepted work.
|
|
478
|
+
- Backpressure exists for request bodies, streaming responses, Cable output, and
|
|
479
|
+
job concurrency.
|
|
480
|
+
|
|
481
|
+
### Usability gates
|
|
482
|
+
|
|
483
|
+
- A fresh generated app works in development and production from Rails-owned
|
|
484
|
+
documentation.
|
|
485
|
+
- `PORT`, bind address, worker count, TLS/proxy mode, logging, PID/signal behavior,
|
|
486
|
+
health checks, and database pool sizing are discoverable.
|
|
487
|
+
- Diagnostics identify a blocking reactor, leaked/pinned database connection,
|
|
488
|
+
stuck task, and slow client without requiring an Async maintainer.
|
|
489
|
+
- Switching back to Puma is a documented one-line generator or Gemfile/config
|
|
490
|
+
choice.
|
|
491
|
+
|
|
492
|
+
## Suggested upstream work items
|
|
493
|
+
|
|
494
|
+
1. Add the Rails generator option and a minimal Falcon production template.
|
|
495
|
+
2. Add a shared Rails/Falcon reference app to CI before changing any default.
|
|
496
|
+
3. Resolve Falcon #359 and add the reproduction as a permanent Rails reload test.
|
|
497
|
+
4. Specify Rails request-body buffering policy and close Falcon #302/#310 plus
|
|
498
|
+
protocol-rack #33 with cross-server tests.
|
|
499
|
+
5. Finish Async Cable's Rails 8.2-native release, executor wiring, Solid Cable
|
|
500
|
+
compatibility, and conformance matrix.
|
|
501
|
+
6. Introduce or document a scheduler-neutral controller streaming API based on
|
|
502
|
+
Rack callable bodies; fix Rack #2470.
|
|
503
|
+
7. Add Active Record fiber stress tests for drivers, transactions, roles/shards,
|
|
504
|
+
cancellation, and pinned connections.
|
|
505
|
+
8. Generate a production lifecycle with graceful drain/restart and observable
|
|
506
|
+
worker/process memory.
|
|
507
|
+
9. Make Early Hints and HTTP/2 upgrade behavior executable compatibility tests.
|
|
508
|
+
10. Collect and publish production candidate evidence before proposing the default
|
|
509
|
+
flip to Rails core.
|
|
510
|
+
|
|
511
|
+
## Issue and change index
|
|
512
|
+
|
|
513
|
+
Status below is as of 2026-08-04.
|
|
514
|
+
|
|
515
|
+
### Rails
|
|
516
|
+
|
|
517
|
+
- [rails/rails#50979][rails-ac-adapter-pr] — Action Cable server adapterization;
|
|
518
|
+
merged 2026-05-28.
|
|
519
|
+
- [rails/rails#57423][rails-reloader-fiber] — fiber-aware reloader share-lock
|
|
520
|
+
ownership; merged 2026-05-21.
|
|
521
|
+
- [rails/rails#57425][rails-reloader-hijack] — release reloader share on hijack;
|
|
522
|
+
merged 2026-05-21.
|
|
523
|
+
- [rails/rails#42271][rails-ar-fiber] — fiber-safe Active Record connection pool;
|
|
524
|
+
closed after the core work landed.
|
|
525
|
+
- [rails/rails#57926][rails-mysql-fiber] — mysql2 connection owned by another
|
|
526
|
+
fiber; open.
|
|
527
|
+
- [rails/rails#21209][rails-live-ar] — Active Record and
|
|
528
|
+
`ActionController::Live` thread interaction; open.
|
|
529
|
+
- [rails/rails#52660][rails-active-storage-async] — concurrent Active Storage
|
|
530
|
+
attachment mutation; open.
|
|
531
|
+
- [rails/rails#48279][rails-current-attributes] — child-fiber semantics for
|
|
532
|
+
`CurrentAttributes`; closed as caller-managed propagation.
|
|
533
|
+
- [rails/rails#23828][rails-template-streaming] — surprising/broken template
|
|
534
|
+
streaming cases; open.
|
|
535
|
+
- [rails/rails#35657][rails-ac-async] — historical Async Action Cable design
|
|
536
|
+
discussion; closed after adapterization became available.
|
|
537
|
+
|
|
538
|
+
### Falcon and integration gems
|
|
539
|
+
|
|
540
|
+
- [socketry/falcon#359][falcon-reload-stall] — development reload stalls all
|
|
541
|
+
requests; open.
|
|
542
|
+
- [socketry/falcon#325][falcon-cable-reload] — constant loading failure during
|
|
543
|
+
development with Async Cable; open.
|
|
544
|
+
- [socketry/falcon#302][falcon-request-body] — JSON request body disappears;
|
|
545
|
+
open.
|
|
546
|
+
- [socketry/falcon#310][falcon-body-rewind] — non-rewindable request bodies and
|
|
547
|
+
Grape/HMAC workflows; open.
|
|
548
|
+
- [socketry/falcon#188][falcon-graceful-restart] — graceful restart breaks
|
|
549
|
+
in-flight requests; open.
|
|
550
|
+
- [socketry/falcon#344][falcon-supervisor-memory] — supervisor/preload memory
|
|
551
|
+
accounting and topology; open.
|
|
552
|
+
- [socketry/falcon#363][falcon-memory-footprint] — production memory regression
|
|
553
|
+
investigation; open.
|
|
554
|
+
- [socketry/falcon#127][falcon-configuration-docs] — production configuration
|
|
555
|
+
discoverability; open.
|
|
556
|
+
- [socketry/async-cable#3][async-cable-solid] — Async Cable and Solid Cable
|
|
557
|
+
incompatibility report; open.
|
|
558
|
+
- [socketry/falcon-rails#4][falcon-rails-logging] — unexpected replacement of
|
|
559
|
+
Rails file logging; open.
|
|
560
|
+
|
|
561
|
+
### Rack and Solid Queue
|
|
562
|
+
|
|
563
|
+
- [rack/rack#2470][rack-deflater] — `Rack::Deflater` fails on Rack 3 callable
|
|
564
|
+
bodies; open.
|
|
565
|
+
- [socketry/protocol-rack#33][protocol-rack-rewind] — false successful rewind and
|
|
566
|
+
empty forwarded request; open.
|
|
567
|
+
- [rails/solid_queue#728][solid-queue-fiber-pr] — bounded fiber worker execution;
|
|
568
|
+
merged and released in Solid Queue 1.6.0.
|
|
569
|
+
|
|
570
|
+
## Conclusion
|
|
571
|
+
|
|
572
|
+
Falcon's case is strategically strong: Ruby and Rails now expose the execution
|
|
573
|
+
locality and server abstraction needed for fibers, while Rack 3 supplies a sound
|
|
574
|
+
streaming and protocol-upgrade foundation. The Rails 8.2 Action Cable work and
|
|
575
|
+
Solid Queue 1.6.0 fiber workers turn the proposal from a parallel ecosystem into
|
|
576
|
+
a credible upstream direction.
|
|
577
|
+
|
|
578
|
+
The next milestone should not be “make Falcon the default.” It should be “make a
|
|
579
|
+
stock Rails application continuously indistinguishable in correctness under
|
|
580
|
+
Falcon, then make the operational differences explicit and well supported.” Once
|
|
581
|
+
that is true—and demonstrated in CI and production—the generator flip becomes a
|
|
582
|
+
small policy change rather than a high-risk architectural bet.
|
|
583
|
+
|
|
584
|
+
[async-cable]: https://github.com/socketry/async-cable/tree/dddef54c29be190f8289225420a681a7c196da12
|
|
585
|
+
[async-cable-solid]: https://github.com/socketry/async-cable/issues/3
|
|
586
|
+
[falcon-body-rewind]: https://github.com/socketry/falcon/issues/310
|
|
587
|
+
[falcon-cable-reload]: https://github.com/socketry/falcon/issues/325
|
|
588
|
+
[falcon-configuration-docs]: https://github.com/socketry/falcon/issues/127
|
|
589
|
+
[falcon-graceful-restart]: https://github.com/socketry/falcon/issues/188
|
|
590
|
+
[falcon-memory-footprint]: https://github.com/socketry/falcon/issues/363
|
|
591
|
+
[falcon-rackup-handler]: https://github.com/socketry/falcon/blob/16965984b1c4bed02b788fd383d982587448a56c/lib/falcon/rackup/handler.rb
|
|
592
|
+
[falcon-railtie]: https://github.com/socketry/falcon/blob/16965984b1c4bed02b788fd383d982587448a56c/lib/falcon/railtie.rb
|
|
593
|
+
[falcon-reload-stall]: https://github.com/socketry/falcon/issues/359
|
|
594
|
+
[falcon-request-body]: https://github.com/socketry/falcon/issues/302
|
|
595
|
+
[falcon-request-logging]: https://github.com/socketry/falcon/issues/90
|
|
596
|
+
[falcon-supervisor-memory]: https://github.com/socketry/falcon/issues/344
|
|
597
|
+
[falcon-rails-full-rails]: https://github.com/socketry/falcon-rails/issues/3
|
|
598
|
+
[falcon-rails-logging]: https://github.com/socketry/falcon-rails/issues/4
|
|
599
|
+
[rack-deflater]: https://github.com/rack/rack/issues/2470
|
|
600
|
+
[rack-spec]: https://github.com/rack/rack/blob/b48e0303a6468eb96e8fc01dfeda6284870e562f/SPEC.rdoc
|
|
601
|
+
[rackup-handler]: https://github.com/rack/rackup/blob/f3fa1d6ada90e9e7aa1f712488ddde87ea2a2075/lib/rackup/handler.rb
|
|
602
|
+
[rails-ac-adapter-pr]: https://github.com/rails/rails/pull/50979
|
|
603
|
+
[rails-ac-async]: https://github.com/rails/rails/issues/35657
|
|
604
|
+
[rails-active-storage-async]: https://github.com/rails/rails/issues/52660
|
|
605
|
+
[rails-app-generator]: https://github.com/rails/rails/blob/f5ae04bef6d47a2ccbbd15a9075622ce6e84116a/railties/lib/rails/generators/app_base.rb#L294-L296
|
|
606
|
+
[rails-ar-connection-handling]: https://github.com/rails/rails/blob/f5ae04bef6d47a2ccbbd15a9075622ce6e84116a/activerecord/lib/active_record/connection_handling.rb#L290-L336
|
|
607
|
+
[rails-ar-fiber]: https://github.com/rails/rails/issues/42271
|
|
608
|
+
[rails-ar-pool]: https://github.com/rails/rails/blob/f5ae04bef6d47a2ccbbd15a9075622ce6e84116a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
|
|
609
|
+
[rails-current-attributes]: https://github.com/rails/rails/issues/48279
|
|
610
|
+
[rails-isolated-state]: https://github.com/rails/rails/blob/f5ae04bef6d47a2ccbbd15a9075622ce6e84116a/activesupport/lib/active_support/isolated_execution_state.rb
|
|
611
|
+
[rails-isolation-guide]: https://github.com/rails/rails/blob/f5ae04bef6d47a2ccbbd15a9075622ce6e84116a/guides/source/configuring.md#configactivesupportisolation_level
|
|
612
|
+
[rails-live-ar]: https://github.com/rails/rails/issues/21209
|
|
613
|
+
[rails-live-connected-to]: https://github.com/rails/rails/issues/52906
|
|
614
|
+
[rails-live-source]: https://github.com/rails/rails/blob/f5ae04bef6d47a2ccbbd15a9075622ce6e84116a/actionpack/lib/action_controller/metal/live.rb
|
|
615
|
+
[rails-mysql-fiber]: https://github.com/rails/rails/issues/57926
|
|
616
|
+
[rails-reloader-fiber]: https://github.com/rails/rails/pull/57423
|
|
617
|
+
[rails-reloader-hijack]: https://github.com/rails/rails/pull/57425
|
|
618
|
+
[rails-server-command]: https://github.com/rails/rails/blob/f5ae04bef6d47a2ccbbd15a9075622ce6e84116a/railties/lib/rails/commands/server/server_command.rb
|
|
619
|
+
[rails-template-streaming]: https://github.com/rails/rails/issues/23828
|
|
620
|
+
[solid-queue-fiber-pr]: https://github.com/rails/solid_queue/pull/728
|
|
621
|
+
[solid-queue-readme]: https://github.com/rails/solid_queue/blob/86f3d92f1dd68547ec0ebe960fc9933c203d9e51/README.md#fork-vs-async-mode
|
|
622
|
+
[protocol-rack-rewind]: https://github.com/socketry/protocol-rack/issues/33
|
|
623
|
+
[protocol-rack-rewindable]: https://github.com/socketry/protocol-rack/blob/a58592b81672ad22a81b52d370a541663c41e872/lib/protocol/rack/rewindable.rb
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: falcon-rails-migration
|
|
3
|
+
description: Migrate an existing Rails application from Puma or another Rack server to Falcon while preserving development and production behavior. Use for migration planning, implementation, or review; not for new Rails applications or feature-specific streaming work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Falcon Rails Migration
|
|
7
|
+
|
|
8
|
+
Treat the migration as a server and concurrency-model change, not just a Gemfile edit. Preserve the existing deployment contract until Falcon has been verified.
|
|
9
|
+
|
|
10
|
+
## Migration Approach
|
|
11
|
+
|
|
12
|
+
1. Establish a working baseline with the current server and tests.
|
|
13
|
+
2. Inventory the current server contract: commands, bind address, port, TLS termination, worker count, preload behavior, timeouts, health checks, graceful shutdown, and deployment manifests.
|
|
14
|
+
3. Identify features sensitive to the server runtime, including streaming responses, WebSockets or Action Cable, request-local state, background jobs, and long-running requests.
|
|
15
|
+
4. Add `falcon-rails` and boot Falcon locally while retaining the existing server as a fallback.
|
|
16
|
+
5. Exercise ordinary requests and every server-sensitive feature before changing production configuration.
|
|
17
|
+
6. Audit code for assumptions that break under fiber concurrency: thread-local request state, shared mutable objects, unbounded task creation, blocking native or CPU-heavy work, and resources held across waits.
|
|
18
|
+
7. Translate the production entrypoint to `falcon host` and `falcon.rb`, preserving the deployment contract rather than copying a generic configuration. Do not use `falcon serve` for production.
|
|
19
|
+
8. Remove the previous server and its configuration only after the Falcon path is covered by tests and deployment checks.
|
|
20
|
+
|
|
21
|
+
## Additional Context
|
|
22
|
+
|
|
23
|
+
Install the relevant context:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle exec bake agent:context:install --gem falcon-rails
|
|
27
|
+
bundle exec bake agent:context:install --gem falcon
|
|
28
|
+
bundle exec bake agent:context:install --gem async
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Read `.context/falcon-rails/getting-started.md`, `.context/falcon/rails-integration.md`, and `.context/falcon/deployment.md`. Read the Async best-practices and thread-safety context when reviewing application compatibility.
|
|
32
|
+
|
|
33
|
+
## Verification
|
|
34
|
+
|
|
35
|
+
Run the full application suite, then test the application through Falcon. Verify readiness and liveness checks, proxy behavior, database pool use, graceful shutdown, and representative concurrent load. If the application uses streaming or WebSockets, use the corresponding Falcon Rails skill for protocol-specific checks.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: falcon-rails-streaming-sse
|
|
3
|
+
description: Implement or review HTTP streaming and Server-Sent Events in a Rails application running on Falcon. Use for progressive responses and server-to-client event streams; not for bidirectional WebSocket communication.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Falcon Rails Streaming and SSE
|
|
7
|
+
|
|
8
|
+
Choose ordinary HTTP streaming for finite progressive output or custom framing. Choose SSE for a long-lived, server-to-client event channel with browser-managed reconnection.
|
|
9
|
+
|
|
10
|
+
## Controller Patterns
|
|
11
|
+
|
|
12
|
+
For a finite streaming response, assign a `Rack::Response` with a callable body:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class ExportController < ApplicationController
|
|
16
|
+
def show
|
|
17
|
+
body = proc do |stream|
|
|
18
|
+
each_record do |record|
|
|
19
|
+
stream.write("#{JSON.generate(record)}\n")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
self.response = Rack::Response[200, {"content-type" => "application/x-ndjson"}, body]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For SSE, use the same response shape with event-stream headers and SSE framing:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class EventsController < ApplicationController
|
|
32
|
+
def index
|
|
33
|
+
body = proc do |stream|
|
|
34
|
+
event_source.each do |event|
|
|
35
|
+
stream.write("data: #{JSON.generate(event)}\n\n")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
self.response = Rack::Response[200, {
|
|
40
|
+
"content-type" => "text/event-stream",
|
|
41
|
+
"cache-control" => "no-cache",
|
|
42
|
+
}, body]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Adapt `each_record` and `event_source` to the application's producer. The callable body owns that producer's lifetime.
|
|
48
|
+
|
|
49
|
+
## Lifecycle Requirements
|
|
50
|
+
|
|
51
|
+
- Keep authentication and authorization in the normal Rails request path before starting the response body.
|
|
52
|
+
- On completion, failure, or client disconnect, stop producers and release subscriptions promptly.
|
|
53
|
+
- Preserve backpressure. Do not place an unbounded queue between producers and a slow client, and do not accumulate the complete response in memory.
|
|
54
|
+
- Avoid holding an Active Record transaction or checked-out connection while waiting for future events.
|
|
55
|
+
- For SSE, decide whether reconnecting clients need event IDs, replay, retry timing, or heartbeats.
|
|
56
|
+
|
|
57
|
+
## Additional Context
|
|
58
|
+
|
|
59
|
+
Install the Falcon Rails context:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bundle exec bake agent:context:install --gem falcon-rails
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Read `.context/falcon-rails/http-streaming.md` and `.context/falcon-rails/server-sent-events.md` for additional examples, client code, routing, and framing details.
|
|
66
|
+
|
|
67
|
+
## Verification
|
|
68
|
+
|
|
69
|
+
Test through Falcon with a client that consumes incrementally. Verify headers and framing, first-byte latency, normal completion, client cancellation, producer failure, and cleanup. For SSE, also test reconnection and any replay or duplicate-event behavior. Check the production proxy for response buffering and idle timeouts.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: falcon-rails-websockets
|
|
3
|
+
description: Implement or review WebSocket endpoints in a Rails application running on Falcon. Use for bidirectional persistent connections, including raw WebSockets or existing Action Cable integrations; not for one-way SSE or finite HTTP streaming.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Falcon Rails WebSockets
|
|
7
|
+
|
|
8
|
+
Determine whether the application already uses Action Cable or raw WebSockets. Extend the established abstraction unless the requested change requires a migration.
|
|
9
|
+
|
|
10
|
+
## Controller Pattern
|
|
11
|
+
|
|
12
|
+
For a raw WebSocket endpoint, use the Rails adapter to assign the upgraded response:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require "async/websocket/adapters/rails"
|
|
16
|
+
|
|
17
|
+
class ChatController < ApplicationController
|
|
18
|
+
skip_before_action :verify_authenticity_token, only: :connect
|
|
19
|
+
|
|
20
|
+
def connect
|
|
21
|
+
return head(:unauthorized) unless current_user
|
|
22
|
+
|
|
23
|
+
self.response = Async::WebSocket::Adapters::Rails.open(request) do |connection|
|
|
24
|
+
Sync do
|
|
25
|
+
while message = connection.read
|
|
26
|
+
payload = JSON.parse(message.buffer)
|
|
27
|
+
connection.send_text(JSON.generate(handle_message(payload)))
|
|
28
|
+
connection.flush
|
|
29
|
+
end
|
|
30
|
+
rescue Protocol::WebSocket::ClosedError
|
|
31
|
+
# The client disconnected.
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Keep authentication and authorization before accepting the connection. Adapt message handling to the application's protocol, including validation and error responses.
|
|
39
|
+
|
|
40
|
+
## Connection Requirements
|
|
41
|
+
|
|
42
|
+
- Define the handshake contract: route, authentication, authorization, origin policy, and any subprotocol. Scope CSRF exemptions narrowly.
|
|
43
|
+
- Define a versionable message contract with explicit handling for malformed, unknown, and oversized messages.
|
|
44
|
+
- Give one connection handler clear ownership of the socket and its child tasks. Coordinate concurrent producers and serialize writes.
|
|
45
|
+
- Bound outbound buffering and define what happens when a client is slow. Do not hold Active Record connections or transactions while waiting for messages.
|
|
46
|
+
- On close, error, cancellation, or server shutdown, stop child tasks and release subscriptions.
|
|
47
|
+
|
|
48
|
+
## Additional Context
|
|
49
|
+
|
|
50
|
+
Install the Falcon Rails context:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bundle exec bake agent:context:install --gem falcon-rails
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Read `.context/falcon-rails/websockets.md` for additional client code, routing, adapter details, and a complete example.
|
|
57
|
+
|
|
58
|
+
## Verification
|
|
59
|
+
|
|
60
|
+
Test with a real WebSocket client through Falcon. Cover successful upgrade, authentication and origin rejection, valid and invalid messages, slow consumers, abrupt disconnects, multiple concurrent clients, and graceful server shutdown. Confirm that reconnect behavior does not leak tasks or duplicate application effects.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: falcon-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -122,6 +122,20 @@ dependencies:
|
|
|
122
122
|
- - ">="
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: falcon-limiter
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
type: :runtime
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
125
139
|
- !ruby/object:Gem::Dependency
|
|
126
140
|
name: live
|
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -166,6 +180,10 @@ files:
|
|
|
166
180
|
- license.md
|
|
167
181
|
- readme.md
|
|
168
182
|
- releases.md
|
|
183
|
+
- report.md
|
|
184
|
+
- skills/falcon-rails-migration/SKILL.md
|
|
185
|
+
- skills/falcon-rails-streaming-sse/SKILL.md
|
|
186
|
+
- skills/falcon-rails-websockets/SKILL.md
|
|
169
187
|
homepage: https://github.com/socketry/falcon-rails
|
|
170
188
|
licenses:
|
|
171
189
|
- MIT
|
|
@@ -186,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
186
204
|
- !ruby/object:Gem::Version
|
|
187
205
|
version: '0'
|
|
188
206
|
requirements: []
|
|
189
|
-
rubygems_version:
|
|
207
|
+
rubygems_version: 4.0.16
|
|
190
208
|
specification_version: 4
|
|
191
209
|
summary: Easy Falcon and Rails integration.
|
|
192
210
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|