Skip to content

Commit d30c9c1

Browse files
authored
Merge pull request rails#52731 from tgwizard/thread-pool-for-live-action-controller
Use thread pool for ActionController::Live
2 parents 863cb00 + 5d08107 commit d30c9c1

File tree

4 files changed

+59
-8
lines changed

4 files changed

+59
-8
lines changed

actionpack/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Update `ActionController::Live` to use a thread-pool to reuse threads across requests.
2+
3+
*Adam Renberg Tamm*
4+
15
* Introduce safer, more explicit params handling method with `params#expect` such that
26
`params.expect(table: [ :attr ])` replaces `params.require(:table).permit(:attr)`
37

actionpack/lib/action_controller/metal/live.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,10 @@ def process(name)
304304
error = e
305305
end
306306
ensure
307+
# Ensure we clean up any thread locals we copied so that the thread can reused.
308+
ActiveSupport::IsolatedExecutionState.clear
309+
locals.each { |k, _| t2[k] = nil }
310+
307311
@_response.commit!
308312
end
309313
end
@@ -368,11 +372,15 @@ def send_stream(filename:, disposition: "attachment", type: nil)
368372
# data from the response bodies. Nobody should call this method except in Rails
369373
# internals. Seriously!
370374
def new_controller_thread # :nodoc:
371-
Thread.new {
375+
ActionController::Live.live_thread_pool_executor.post do
372376
t2 = Thread.current
373377
t2.abort_on_exception = true
374378
yield
375-
}
379+
end
380+
end
381+
382+
def self.live_thread_pool_executor
383+
@live_thread_pool_executor ||= Concurrent::CachedThreadPool.new(name: "action_controller.live")
376384
end
377385

378386
def log_error(exception)

actionpack/lib/action_controller/test_case.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ module Live
2222
# database on the main thread, so they could open a txn, then the controller
2323
# thread will open a new connection and try to access data that's only visible
2424
# to the main thread's txn. This is the problem in #23483.
25+
alias_method :original_new_controller_thread, :new_controller_thread
26+
2527
silence_redefinition_of_method :new_controller_thread
2628
def new_controller_thread # :nodoc:
2729
yield

actionpack/test/controller/live_stream_test.rb

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,24 @@ def thread_locals
228228
response.stream.close
229229
end
230230

231+
def no_thread_locals
232+
tc.assert_nil Thread.current[:setting]
233+
234+
response.headers["Content-Type"] = "text/event-stream"
235+
%w{ hello world }.each do |word|
236+
response.stream.write word
237+
end
238+
response.stream.close
239+
end
240+
231241
def isolated_state
232242
render plain: CurrentState.id.inspect
233243
end
234244

245+
def raw_isolated_state
246+
render plain: ActiveSupport::IsolatedExecutionState[:raw_isolated_state].inspect
247+
end
248+
235249
def with_stale
236250
render plain: "stale" if stale?(etag: "123", template: false)
237251
end
@@ -344,7 +358,7 @@ def setup
344358
super
345359

346360
def @controller.new_controller_thread(&block)
347-
Thread.new(&block)
361+
original_new_controller_thread(&block)
348362
end
349363
end
350364

@@ -525,6 +539,34 @@ def test_isolated_state_get_copied
525539
assert_stream_closed
526540
end
527541

542+
def test_thread_locals_get_reset
543+
@controller.tc = self
544+
545+
Thread.current[:originating_thread] = Thread.current.object_id
546+
Thread.current[:setting] = "aaron"
547+
548+
get :thread_locals
549+
550+
Thread.current[:setting] = nil
551+
552+
get :no_thread_locals
553+
end
554+
555+
def test_isolated_state_get_reset
556+
@controller.tc = self
557+
ActiveSupport::IsolatedExecutionState[:raw_isolated_state] = "buffy"
558+
559+
get :raw_isolated_state
560+
assert_equal "buffy".inspect, response.body
561+
assert_stream_closed
562+
563+
ActiveSupport::IsolatedExecutionState.clear
564+
565+
get :isolated_state
566+
assert_equal nil.inspect, response.body
567+
assert_stream_closed
568+
end
569+
528570
def test_live_stream_default_header
529571
get :default_header
530572
assert response.headers["Content-Type"]
@@ -565,14 +607,9 @@ def test_exception_handling_plain_text
565607
end
566608

567609
def test_exception_callback_when_committed
568-
current_threads = Thread.list
569-
570610
capture_log_output do |output|
571611
get :exception_with_callback, format: "text/event-stream"
572612

573-
# Wait on the execution of all threads
574-
(Thread.list - current_threads).each(&:join)
575-
576613
assert_equal %(data: "500 Internal Server Error"\n\n), response.body
577614
assert_match "An exception occurred...", output.rewind && output.read
578615
assert_stream_closed

0 commit comments

Comments
 (0)