module ActionController::Live
Контроллер действий Live
Подключите этот модуль к своему контроллеру, и все действия в этом контроллере смогут передавать данные клиенту по мере их записи.
class MyController < ActionController::Base
include ActionController::Live
def stream
response.headers['Content-Type'] = 'text/event-stream'
100.times {
response.stream.write "hello world\n"
sleep 1
}
ensure
response.stream.close
end
end
У этого модуля есть несколько особенностей. После отправки ответа вы не можете записывать заголовки (Response#committed? вернёт истинное значение). Вызов write или close для потока ответа приведёт к отправке объекта ответа. Убедитесь, что все заголовки заданы до вызова write или close для потока.
После завершения работы вы должны вызвать close для потока, иначе сокет может остаться открытым навсегда.
И наконец, действия выполняются в отдельном потоке, а не в основном. Убедитесь, что ваши действия потокобезопасны, и проблем не возникнет (не используйте общее состояние в нескольких потоках и т. д.).
Обратите внимание, что Rails по умолчанию включает Rack::ETag, который буферизует ответ. В результате потоковые ответы могут работать некорректно с Rack 2.2.x, и в приложении может понадобиться реализовать обходные решения. Чтобы решить эту проблему, можно задать заголовки ответа ETag или Last-Modified либо удалить Rack::ETag из стека промежуточного ПО.
Вот пример задания заголовка Last-Modified, если используется Rack версии 2.2.x:
def stream response.headers["Content-Type"] = "text/event-stream" response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x ... end
Streaming и состояние выполнения
При потоковой передаче действие выполняется в отдельном потоке. По умолчанию этот поток использует общее состояние выполнения родительского потока.
С помощью настройки config.action_controller.live_streaming_excluded_keys можно указать, какие ключи состояния выполнения не должны использоваться совместно:
# config/application.rb config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack]
Это полезно, если вы используете ActionController::Live внутри блока connected_to. Например, если родительский запрос читает данные из реплики с помощью connected_to(role: :reading), вы можете захотеть, чтобы потоковый поток использовал собственный контекст соединения, а не наследовал контекст только для чтения:
# Без настройки потоковый поток наследует соединение только для чтения ActiveRecord::Base.connected_to(role: :reading) do @posts = Post.all render stream: true # Поток Streaming не может записывать данные в базу данных end
# С настройкой потоковый поток получает новый контекст соединения # config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack] ActiveRecord::Base.connected_to(role: :reading) do @posts = Post.all render stream: true # Поток Streaming при необходимости может записывать данные в базу данных end
Ключи, которые может быть полезно исключить: - :active_record_connected_to_stack - Маршрутизация подключений к базе данных и роли - :active_record_prohibit_shard_swapping - Ограничения на смену шарда
По умолчанию ключи не исключаются, чтобы сохранить обратную совместимость.
Общедоступные методы класса
# File actionpack/lib/action_controller/metal/live.rb, line 417 def self.live_thread_pool_executor @live_thread_pool_executor ||= Concurrent::CachedThreadPool.new(name: "action_controller.live") end
Общедоступные методы экземпляра
# File actionpack/lib/action_controller/metal/live.rb, line 307
def process(name)
t1 = Thread.current
locals = t1.keys.map { |key| [key, t1[key]] }
error = nil
# This processes the action in a child thread. It lets us return the response
# code and headers back up the Rack stack, and still process the body in
# parallel with sending data to the client.
new_controller_thread do
ActiveSupport::Dependencies.interlock.running do
t2 = Thread.current
# Since we're processing the view in a different thread, copy the thread locals
# from the main thread to the child thread. :'(
locals.each { |k, v| t2[k] = v }
ActiveSupport::IsolatedExecutionState.share_with(t1, except: self.class.live_streaming_excluded_keys) do
super(name)
rescue => e
if @_response.committed?
begin
@_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
@_response.stream.call_on_error
rescue => exception
log_error(exception)
ensure
log_error(e)
@_response.stream.close
end
else
error = e
end
ensure
clean_up_thread_locals(locals, t2)
@_response.commit!
end
end
end
@_response.await_commit
raise error if error
end # File actionpack/lib/action_controller/metal/live.rb, line 352 def response_body=(body) super response.close if response end
# File actionpack/lib/action_controller/metal/live.rb, line 382
def send_stream(filename:, disposition: "attachment", type: nil)
payload = { filename: filename, disposition: disposition, type: type }
ActiveSupport::Notifications.instrument("send_stream.action_controller", payload) do
response.headers["Content-Type"] =
(type.is_a?(Symbol) ? Mime[type].to_s : type) ||
Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete("."))&.to_s ||
"application/octet-stream"
response.headers["Content-Disposition"] =
ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
yield response.stream
end
ensure
response.stream.close
end Отправляет поток данных в браузер. Это удобно при создании экспортов и передаче других динамически формируемых данных, когда не нужно сначала буферизовать весь файл в памяти. Аналогично send_data, но данные генерируются в реальном времени.
Параметры:
-
:filename— предлагает имя файла, которое будет использовать браузер. -
:type— указывает тип содержимого HTTP. Можно указать строку или символ для зарегистрированного типа с помощьюMime::Type.register, например :json. Если параметр не указан, тип определяется по расширению файла, заданному в:filename. Если для этого расширения не зарегистрирован тип содержимого, будет использоваться тип по умолчанию ‘application/octet-stream’. -
:disposition— указывает, будет ли файл отображаться в браузере или скачиваться. Допустимые значения: ‘inline’ и ‘attachment’ (по умолчанию).
Пример создания экспорта в формате CSV:
send_stream(filename: "subscribers.csv") do |stream|
stream.write "email_address,updated_at\n"
@subscribers.find_each do |subscriber|
stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
end
end
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.