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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5d996717f45cb632e40028d722d05d3b5699a112a3325d4afe156b732544942
4
- data.tar.gz: 7e53b8ac90cee9b7239569bf4534519d0a8a73164016151e10d6f42101689859
3
+ metadata.gz: bfe0796114a228549fa545788256afa15a5497b9db09e6c4054ff88eaa99809b
4
+ data.tar.gz: 3d2d17280ddb2b577f45fe142e5747de8eec1954dab09b8054aba8dd38c529e2
5
5
  SHA512:
6
- metadata.gz: 757fbcc9c5b3716a5653c1110b3a98e48e9ea4730fe478bb62f84ab9b81708dd3f2229b09a268d855772186d46975c4bac70edbfb24a990bd043813468556d46
7
- data.tar.gz: b82d05a0737768a0ac5f6fb928fa7cad3ad412ae2566d1331e75fc0c1293e73e94427abafc89882fdc0cf534c94740c4249b8acabe38cb81cf692dca10df70b5
6
+ metadata.gz: 9c64e44c28cbc6dc2cfb35cd5264ee2ddd5f77020116ffbd84ccac78f10ba29f102b40ca40d20dafa3217d77b3b6fc714094dd4c56cf73165de2f2dc64e84fb3
7
+ data.tar.gz: b1dee5a5b5ef3aa84868306e2c34a89c5fa307002dd13f2cc07a48e12ff9251171e7f79f827cd00e3f87b67f66745de861600fe946c30536cf304c97bfe6c6db
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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 'streaming/index' # Page with streaming JavaScript
141
- get 'streaming/stream' # HTTP streaming endpoint
140
+ get "streaming/index" # Page with streaming JavaScript
141
+ get "streaming/stream" # HTTP streaming endpoint
142
142
  end
143
143
  ```
144
144
 
@@ -44,10 +44,10 @@ end
44
44
  Configure async-job queues in `config/initializers/async_job.rb`:
45
45
 
46
46
  ```ruby
47
- require 'async/job'
48
- require 'async/job/processor/aggregate'
49
- require 'async/job/processor/redis'
50
- require 'async/job/processor/inline'
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
@@ -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 'live'
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 'async/websocket/adapters/rails'
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('clock')
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) { builder.text("Live Counter") }
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
- 'content-type' => 'text/event-stream',
34
- 'cache-control' => 'no-cache',
35
- 'connection' => 'keep-alive'
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 'sse/index' # Page with EventSource JavaScript
112
- get 'sse/events' # SSE endpoint
111
+ get "sse/index" # Page with EventSource JavaScript
112
+ get "sse/events" # SSE endpoint
113
113
  end
114
114
  ```
115
115
 
@@ -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 'async/websocket/adapters/rails'
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
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
3
6
  # @namespace
4
7
  module Falcon
5
8
  # @namespace
6
9
  module Rails
7
- VERSION = "0.2.4"
10
+ VERSION = "0.3.0"
8
11
  end
9
12
  end
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.2.4
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: 3.6.9
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