前面章节,我们已经分析了rails render stack的里里外外,你已经了解到当一个请求到达控制器,控制器 收集请求信息给需要渲染的模板, 模板从解析器中得到,然后编译渲染,嵌入到布局文件中, 最后,你得到了ruby字符串形式的模板,这个字符串设置到http response中,返回给客户端
这种工作方式对大多数程序都还不错。然而,有一些情况,我们需要发送response以较小的字节片段方式,有时这些字节片段,可能是无限的不间断的,我们需要一直需要发送,直到服务端和客户端链接中断为止。
无论什么时候我们发送一个response以字节片段方式,我们都叫做服务端数据流到客户端,因为rails以更传统的请求响应场景构建,流服务的支持被添加并且不断被改良,这章我们来探究一下
为了探究streaming是如何工作的,我们编写一个rails plugin,当我们的css样式改变时,发送数据给浏览器端,浏览器将会使用这些信息重新加载当前页面的样式, 允许开发者看到当他们修改页面资源样式时,页面同时改变,不需要手动刷新页面。
因为这个插件有自己的控制器,asserts,routes和其他,我们将基于rails engines提供的强大能力,添加功能作为rails application一部分,另一方面打包成gem分享到其他项目
rails 引擎允许我们的插件有自己的控制器,模型,帮助方法,试图,资源,和路由,就像一个符合规则的rails application.让我们生成一个叫做live_assets的插件,使用rails 插件生成器,但是这次 我们传递--full 标记,用于生成model 控制器和路由目录
$ rails plugin new live_assets --full
除了生成器给我们创建的常规文件,--full标签也生成下面这些文件
-
一个app目录,里面有controller,models,和其他在一个rails application里能看到的目录
-
一个config/routes.rb文件用于路由
-
一个空的test/integration/navigation_test.rb文件,用于添加我们的测试
最重要的文件是lib/live_assets/engine.rb.让我们仔细看一下这个文件。
live_assets/lib/live_assets/engine.rb
module LiveAssets
class Engine < ::Rails::Engine
end
end
创建了一个engine类,我们需要继承Rails::Engine并且确保我们的新engine尽可能快的被加载, 这个生成器已经替我们做了在lib/live_assets.rb中添加
live_assets/lib/live_assets.rb
require "live_assets/engine"
module LiveAssets
end
创建一个Rails::Engine十分类似创建一个Rails::Railtie.因为Rails::Engine相比较Rails::Railtie只不过多了一些默认初始化设置,和paths程序接口 我们接下来看看
一个Rails::Engine没有硬编码路径,这意味着我们不需要将我们的models和controllers放在app/目录下,我们可以把他们放到任何位置。例如。我们配置我们的engine读取我们的控制器从lib/controllers目录,替代app/controllers,如下
module LiveAssets
class Engine < Rails::Engine
paths["app/controllers"] = ["lib/controllers"]
end
end
我们也可以让rails读取我们的controllers从app/controllers和lib/controllers两个目录里读取
module LiveAssets
class Engine < Rails::Engine
paths["app/controllers"] << "lib/controllers"
end
end
这些路径和在rails application里的路径有一样的语义,如果你有一个控制器叫做LiveAssetsController在app/controllers/live_assets_controller.rb里,或者在lib/controllers/live_assets_controller.rb里,这个控制器都会被自动加载,当你需要这个控制器的时候, 不如要显示的required
现在,我们遵守约定的路径,粘贴我们的控制器到app/controllers,所以不需要使用前面的修改路径方法,通过查看rails源码,我们可以检查所有自定义路径
上面的代码片段展示了哪些指定的路径应该被热加载,哪个不被热加载,添加列表路径到 locales,migrations等等,然而声明一个路径是不够的,还要用路径做些事情
一个engine有几个初始化程序,负责启动engine, 这些初始化器相当底层,不会与你application的config/initializers下的任何一个混淆。 让我们看一个例子
rails/railties/lib/rails/engine.rb
initializer :add_view_paths do
views = paths["app/views"].existent
unless views.empty?
ActiveSupport.on_load(:action_controller){ prepend_view_path(views) }
ActiveSupport.on_load(:action_mailer){ prepend_view_path(views) }
end
end
初始化器负责添加我们的engine views,通常定义在app/views里,ActionController::Base 和 ActionMailer::Base一被加载 允许一个rails application使用engine中定义的模板, 可以看一下engine中的全部初始器,我们可以打开一个控制台,在test/dummy下,输入下面
Rails::Engine.initializers.map(&:name) # =>
[:set_load_path, :set_autoload_paths, :add_routing_paths,
:add_locales, :add_view_paths, :load_environment_config,
:append_assets_path, :prepend_helpers_path,
:load_config_initializers, :engines_blank_point]
使用engine和使用rails application十分类似,我们都知道怎样构建实现我们的流插件
看一下streaming如何工作,让我们创建一个控制器叫做LiveAssetsController,文件位置app/controllers/live_assets_controller.rb,引入了ActionController::Live功能,发送hello world不间断。
live_assets/1_live/app/controllers/live_assets_controller.rb
class LiveAssetsController < ActionController::Base
include ActionController::Live
def hello
while true
response.stream.write "Hello World\n"
sleep 1
end
rescue IOError
response.stream.close
end
end
我们的控制器提供了一个action叫做hello(),每秒发送一个Hello world, 如果有任何原因,导致链接在server和client中断,response.stream.write会失败抛出IOError. 我们需要捕获它,关闭流
我们需要一个路由配置
live_assets/1_live/config/routes.rb
Rails.application.routes.draw do
get "/live_assets/:action", to: "live_assets"
end
我们准备尝试发送流到客户端,然而,因为rails engine不能运行自己,我们需要启动它在test/dummy中,此外流功能不会工作在webrick中,webrick是ruby和rails使用默认服务器,webrick将缓存我们发送到客户端响应, 所以我们不会看到任何东西,对于这个原因,我们使用puma,添加到我们的gemspec作为开发依赖
最后。我们进入test/dummy目录,执行rail s ,rails现在启动 替代了webrick
Booting Puma
Rails 4.0.0 application starting in development on http://0.0.0.0:3000
Call with -d to detach
Ctrl-C to shutdown server
大多数浏览器会尝试缓存流相应,或者需要一段时间,他们决定是否要展示我们的内容, 所以测试我们的流发送到末端,我们使用curl 通过命令行
$ curl -v localhost:3000/live_assets/hello
> GET /live_assets/hello HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0)
> Host: localhost:3000
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-UA-Compatible: chrome=1
< Cache-Control: no-cache
< Content-Type: text/html; charset=utf-8
< X-Request-Id: f21f8c0d-d496-4bfa-944c-cd01b44b87ee
< X-Runtime: 0.003120
< Transfer-Encoding: chunked
<
Hello World
Hello World
每秒,你都会看到Hello world 出现在屏幕上,这意味着流推送正在工作, 按住CTRL+C中断传输,我们进一步学习一个更复杂的例子
开发者总是需要在浏览器里收到服务端的更新,很长一段时间里,轮询是最通用的解决这个问题的技术方案。在轮询的时候,浏览器频繁发送请求到服务器端,询问是否有新数据,如果没有新数据,服务端返回一个空响应,浏览器再开始新的请求,根据频率,浏览器最终向服务器发送许多请求,产生大量开销。
随着不断发展,长轮询技术出现,使用这个技术,浏览器定期的发送请求给服务端,如果没有更新服务器端在发送空响应之前,等待一段时间,虽然比传统的轮询执行的好一些,浏览器之间存在交叉兼容性问题。 此外,许多代理和服务端如果一段时间没有通讯就会发生链接丢失,这种方法就失效了
为了解决开发者的需求,html标准引入了两个api, Server Sent Events (SSE) 和 WebSockets,WebSockets允许客户端和服务器端交换信息在同一个连接上,但是因为是新协议,或许需要改变你的开发栈来支持,Server sent Event,是一个单向通讯通道。从服务端到客户端,可以使用任何web服务器,只要能够支持流响应(stream response),基于这些原因sse使我们这章节选择的方案。
sse基础就是event stream format,下面是一个对http请求的事件流响应
HTTP/1.1 200 OK
Content-Type: text/event-stream
event: some_channel
data: {"hello":"world"}
event: other_channel
data: {"another":"message"}
数据的界定通过两个新行,每个信息有一个event和他关联的数据,在这个例子中, 数据是json格式,但它也可以是文本,当流推送的时候,我们需要从服务端返回一个格式, 让我们创建一个新的action叫做sse在我们的LiveAssetsController里,发送一个reloadcss事件,每秒钟发送一次
live_assets/1_live/app/controllers/live_assets_controller.rb
def sse
response.headers["Cache-Control"] = "no-cache"
response.headers["Content-Type"] = "text/event-stream"
while true
response.stream.write "event: reloadCSS\ndata: {}\n\n"
sleep 1
end
rescue IOError
response.stream.close
end
类似我们第一个action,除了现在我们需要设置适当的响应内容类型,并且关闭缓存,服务端已经准备好。我们来写客户端,使用js:
live_assets/1_live/app/assets/javascripts/live_assets/application.js
window.onload = function() {
// 1. Connect to our event-stream
var source = new EventSource('/live_assets/sse');
// 2. This callback will be triggered on every reloadCSS event
source.addEventListener('reloadCSS', function(e) {
// 3. Load all CSS entries
var sheets = document.querySelectorAll("[rel=stylesheet]");
var forEach = Array.prototype.forEach;
// 4. For each entry, clone it, add it to the
//document and remove the original after
forEach.call(sheets, function(sheet){
var clone = sheet.cloneNode();
clone.addEventListener('load', function() {
sheet.parentNode.removeChild(sheet);
});
document.head.appendChild(clone);
});
});
};
我们的javascript文件链接我们的后端,监听每个reloadcss事件,在页面重新加载所有的样式,我们的资源文件定义在,app/assets/live_assets/application.js,这个文件被引入,因为rails仅仅预编译资源文件匹配application.*。因为他们是仅有的被预编译的文件,这样的文件通常被引入所有存在的文件里, 那就是为什么叫做manifests.
最后我们创建一个帮助方法,让application读取我们资源更方便
live_assets/1_live/app/helpers/live_assets_helper.rb
module LiveAssetsHelper
def live_assets
javascript_include_tag "live_assets/application"
end
end
使用我们的server sent events机制,我们到test/dummy创建一个控制器
live_assets/1_live/test/dummy/app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
render text: "Hello", layout: true
end
end
live_assets/1_live/test/dummy/config/routes.rb
Dummy::Application.routes.draw do
root to: "home#index"
end
修改我们的布局引入engine资源 但是仅在开发模式
live_assets/test/dummy/app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Dummy</title>
<%= stylesheet_link_tag "application", media: "all" %>
<%= javascript_include_tag "application" %>
<%= live_assets if Rails.env.development? %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
重启虚拟app(dummp目录下的app),使用浏览器浏览localhost:3000,如果你的浏览器有网络面板, 可以看http请求,通过浏览器发送的,你或许期望每个样式表每秒钟都被重新加载,但是没有发生,在下图
即使puma是一个多线程服务器,rails允许仅有一个线程在一个时间点运行,我们需要改变虚拟程序允许并行计算
live_assets/test/dummy/config/application.rb
config.allow_concurrency = true
因为浏览器与web服务器链接,然后等待服务器相应请求,我们需要关闭浏览器之前重启web服务器,关闭浏览器,重新启动服务器,然后重新打开地址loalhost:3000;我们可以看到样式表没秒都在重新加载 为了验证我们的样式表被重新加载,我们编辑test/dummy/app/assets/stylesheets/application.css文件,观察发生的改变,不需要刷新页面,尝试设置文本颜色如下
body { color: red; }
如你所见,我们的server-sent events推送流工作了,然而,我们还是可以做一些改善,首先,我们想仅仅修改样式仅当文件系统上的文件内容发生改变时,而不是每秒都重新加载,观察这些变化应该是会提高效率的,如果我们有5个页面,我们不想为我们打开的所有页面都去查询文件系统,我们将有一个主文件系统侦听器实体,每个请求都可以订阅。
第二个问题是我们的代码目前没有任何测试,这种特性其实很难编写测试,因为流发送无限的数据,为了可以直接从控制器测试,我们需要将存在的组件变得更小,更可测试
最后,因为我们使用了config.allow_concurrency,我们需要理解这样的设置如何影响基于stremaing部署的applications,所以我们有很多工作要做
一个rails程序默认产生三个资源目录,app/assets,lib/assets,和vendor/assets.我们的资源应该被分割到这些目录使用和我们分割代码一样的方式,app目录应该包含直接和我们程序相关的资源,lib目录包含独立js或者css组件,组件使用远超我们的application. vendor目录包含第三方文件
我们想监视这些目录上的文件的改变,一种选择是每秒或更少地手动检查每个目录中每个文件的修改时间。这就是文件系统轮询,轮询或许是个好的开始点,但是资源文件不断增长,会变得非常耗费CPU
幸运的是,大多数系统提供一个通知机制,为文件系统改变,我们简单传递操作系统所有我们想监视的目录,并且如果一个文件被添加,移除,修改,我们的代码将会被通知,这个listen gem提供了所有主流系统通知机制的api调用,考虑我们的需求有一个实体监视文件系统,我们的请求可以订阅,让我们在一个线程里包装所有监听功能,在请求时并发运行,打开lib/live_assets.rb实现它
live_assets//lib/live_assets.rb
require "live_assets/engine"
require "thread"
require "listen"
module LiveAssets
mattr_reader :subscribers
@@subscribers = []
# Subscribe to all published events.
def self.subscribe(subscriber)
subscribers << subscriber
end
# Unsubscribe an existing subscriber.
def self.unsubscribe(subscriber)
subscribers.delete(subscriber)
end
# Start a listener for the following directories.
# Every time a change happens, publish the given
# event to all subscribers available.
def self.start_listener(event, directories)
Thread.new do
Listen.to(*directories, latency: 0.5) do |_modified, _added, _removed|
subscribers.each { |s| s << event }
end
end
end
end
我们的代码原理是在一个线程里监听, 监听一组给定的目录,每次发生改变,推送注册的事件给每个订阅者, 因为我们使用listen gem,让我们添加到gemspec,
live_assets/2_listener/live_assets.gemspec
s.add_dependency "listen"
虽然我们不能编写集成测试给action因为流更新是无限的,我们监听功能从流系统剥离出来,允许我们独立测试,让我们编写一个测试,开启监听,然后验证一个事件推送给我们的订阅者,当test/tmp目录发生变化时。
live_assets/2_listener/test/live_assets_test.rb
require "test_helper"
require "fileutils"
class LiveAssetsTest < ActiveSupport::TestCase
setup do
FileUtils.mkdir_p "test/tmp"
end
teardown do
FileUtils.rm_rf "test/tmp"
end
test "can subscribe to listener events" do
# Create a listener
l = LiveAssets.start_listener(:reload, ["test/tmp"])
# Our subscriber is a simple array
subscriber = []
LiveAssets.subscribe(subscriber)
begin
while subscriber.empty?
# Trigger changes in a file until we get an event
File.write("test/tmp/sample", SecureRandom.hex(20))
end
# Assert we got the event
assert_includes subscriber, :reload
ensure
# Clean up
LiveAssets.unsubscribe(subscriber)
l.kill
end
end
end
不错,看起来我们的监听器工作如预期,当你运行测试的时候,你可能得到一些来自Listen gem的警告,因为它使用文件系统轮询,除非你安装了一个gem针对你的操作系统使用文件系统提醒。想更自由可以添加一个这样的gem到你的gemfile.不是gemspec,因为对于我们的插件listen这样的gem不是必须的依赖
最后我们需要确保监听我们的资源目录,当我们application启动就开始监视,然后推动一个:reloadcss时间,当发生改变的时候,让我们写一个测试
live_assets/2_listener/test/live_assets_test.rb
test "can subscribe to existing reloadCSS events" do
subscriber = []
LiveAssets.subscribe(subscriber)
begin
while subscriber.empty?
FileUtils.touch("test/dummy/app/assets/stylesheets/application.css")
end
assert_includes subscriber, :reloadCSS
ensure
LiveAssets.unsubscribe(subscriber)
end
end
我们的测试假设监听已经可以使用,为了确保测试可以使用,我们定义一个初始化器在我们的engine里,类似我们前面章节看到的,启动监听,传递资源目录作为参数
live_assets/2_listener/lib/live_assets/engine.rb
module LiveAssets
class Engine < ::Rails::Engine
initializer "live_assets.start_listener" do |app|
paths = app.paths["app/assets"].existent +
app.paths["lib/assets"].existent +
app.paths["vendor/assets"].existent
paths = paths.select { |p| p =~ /stylesheets/ }
if app.config.assets.compile
LiveAssets.start_listener :reloadCSS, paths
end
end
end
end
注意我们开始监听只有在资源被动态编译时。这样防止在生产版本中也监听,在生产版本资源编译配置和与编译配置,通常设置为false。
现在我们的监听器已经启动,并且准备推送事件给订阅者,每次访问/live_assets/sse,我们需要创建一个新的订阅者,添加他到订阅列表,然后等一个新的事件推送我们的订阅者.一旦事件达到,我们就发送一个 server-sent evnet给浏览器,如下图所示
图中最棘手的部分就是等待: 我们想每个请求都是空闲状态,直到一个事件到达,在循环中检查新事件,和我们在测试中做的一样,这不是一个好的选择,因为会导致CPU使用率过高,我们通过休眠一定的时间来解决这个问题,例如半秒,然后检查事件,但是这样也效果一般,最完美的效果是,我们想让让他睡眠,当事件到达时自动醒来。
ruby有一个完美的解决方案在标准库中,使用Queue类,让我们看一下
队列是先进先出的结构, 通过require thread,我们可以实现一个队列访问任何ruby代码,它提供了一个简单的api
require "thread"
q = Queue.new
t = Thread.new do
while last = q.pop
sleep(1) # simulate cost
puts last
end
end
q << :foo
sleep(1)
$stdout.flush
上面代码,创建了队列Queue,和一个Thread, 在线程里是一个循环,调用Queue#pop()方法,如果队列里没有任何项,线程就会阻塞,直到新的项被添加到队列, 最后三行,我们将一个符号Push到队里了,将会唤醒线程,一秒后,我们flush ,写入$sdout,会看到foo
这意味着队列对于我们是完美的结构用来作为订阅者,直到请求事件到达,队列一直时空的并且是睡眠状态,然后我们推送这个新事件之后,然后进入睡眠,我们创建一个类叫做LiveAssets::SSESubscriber,用来接收这些时间,以server-sent stream format格式输出,如下测试
live_assets/2_listener/test/live_assets/subscriber_test.rb
require "test_helper"
require "thread"
class LiveAssets::SubscriberTest < ActiveSupport::TestCase
test "yields server sent events from the queue" do
# Let's start our queue with some events
queue = Queue.new
queue << :reloadCSS
queue << :ping
queue << nil
# And create a subscriber on top of it
subscriber = LiveAssets::SSESubscriber.new(queue)
stream = []
subscriber.each do |msg|
stream << msg
end
assert_equal 2, stream.length
assert_includes stream, "event: reloadCSS\ndata: {}\n\n"
assert_includes stream, "event: ping\ndata: {}\n\n"
end
end
我们的测试创建了一个队列,将它传递给订阅者,然后通过消耗事件提醒订阅者,注意。当我们添加nil到队列中,表示我们没有产生事件和消耗事件, 让我们实现一个订阅者
live_assets/2_listener/lib/live_assets/sse_subscriber.rb
require "thread"
module LiveAssets
class SSESubscriber
def initialize(queue = Queue.new)
@queue = queue
LiveAssets.subscribe(@queue)
end
def each
while event = @queue.pop
yield "event: #{event}\ndata: {}\n\n"
end
end
def close
LiveAssets.unsubscribe(@queue)
end
end
end
然后自动加载他
autoload :SSESubscriber, "live_assets/sse_subscriber"
最后我们编写一个live_assets#sse的action 确保使用我们新的订阅者
live_assets/2_listener/app/controllers/live_assets_controller.rb
def sse
response.headers["Cache-Control"] = "no-cache"
response.headers["Content-Type"] = "text/event-stream"
sse = LiveAssets::SSESubscriber.new
sse.each { |msg| response.stream.write msg }
rescue IOError
sse.close
response.stream.close
end
再一次,我们在test/dummy里面,重启puma服务器,验证仅仅只有修改app/assets/stylesheets/application.css 才推送事件流,反映到页面的改变。这次我们修改字体
body { font-size: 32px; }
我们基本上已经实现完成了,但是还有最后一个问题需要我们解决,当样式长时间没有改变。我们会有一段长时间没有推送任何事件给浏览器, 这或许会引起浏览器服务端或者和代理之间的链接关闭
###### Timer
为了确保连接不会再长时间空闲时被关闭,我们需要一个定时器,负责解决推送一个 ping时间给订阅者,10秒一次,让我们开始测试
test "receives timer notifications" do
# Create a timer
l = LiveAssets.start_timer(:ping, 0.5)
# Our subscriber is a simple array
subscriber = []
LiveAssets.subscribe(subscriber)
begin
# Wait until we get an event
true while subscriber.empty?
assert_includes subscriber, :ping
ensure
# Clean up
LiveAssets.unsubscribe(subscriber)
end
end
我们的听时期将运行在自己的线程上,推送事件给订阅者
live_assets/3_final/lib/live_assets.rb
def self.start_timer(event, time)
Thread.new do
while true
subscribers.each { |s| s << event }
sleep(time)
end
end
end
上面的实现确保我们测试通过,最后添加到engine里面,另一个初始化器负责启动定时器
live_assets/3_final/lib/live_assets/engine.rb
initializer "live_assets.start_timer" do |app|
if app.config.assets.compile
LiveAssets.start_timer :ping, 10
end
end
重启puma web服务器,现在ping事件应该每10秒发送一次,我们没有注册任何回调函数在js那端。但是如果我们想也可以,js eventSource对象也有open 和close事件 ,当连接打开和关闭的时候。mozilla开发网有详细说明,你可以去浏览
在整个实现过程中,细节中的一个两点就是需要设置config.allow_concurrency 为 true,现在远离live-assets的实现,我们有更好的机会去讨论他
为了理解为什么我们需要直接打开allow_concurrency选项,我们需要分析ruby和rails的代码加载机制
最常用的ruby的加载代码技术就是require()方法
require "live_assets"
一些库简单的使用require就能工作的很好,但是随着他们不断增长,他们中的一些逐渐依赖于autoload技术避免一开始就读取全部文件,在rails plugin中autoload是一个很重要的技术,因为他能帮助application的启动时间低于在开发和测试环境,因为我们读取模块仅在我们第一次需要他们的时候。
我们在这章的LIveAssets::SSESubscribe中使用了autoload
module LiveAssets
autoload :SSESubscriber, "live_assets/sse_subscriber"
end
现在,第一次 LiveAssets::SSESubscriber被访问,它会自动被读取,rails plugin和application还有另一个代码加载技术,就是rails的autoload,例如,我们的LiveAssetsController,当我们第一次使用它时被自动加载,但是这个不是由ruby处理的,而是通过rail自带的ActiveSupport::Dependencies
ruby和rails在一开始的问题是加载代码不是原子性的, 它不是一个单独的步骤,例如,如果你有一个请求发生在A线程里,线程A启动读取LiveAssetsController,LiveAssetsController这个类在线程B中 是可见的并且在Thread A完成加载app/controllers/live_assets_controller.rb文件之前,在线程B中已经被一个请求响应,这种情况下,线程B只会看到有一部分控制器的方法,例如,可能紧包含一个hello()方法,没有sse方法,就会导致失败。
尽管一些Ruby实现一直致力于实现Ruby的加载线程安全(前面描述场景并不会发生),rails的autoload并不是线程安全,为了解决这个实际问题,无论何时rails autoload需要加载代码,默认rali只允许一个线程运行,那就意味着在一个时间点,只有一个线程提供服务,那就是为什么我们不能在同一时间使用server-sent events 链接提供资源服务。为了解决这个问题。我们让config.allow_concurrency设置为true。
这对生产意味着什么?我们需要明确允许并发吗,我们部署这个应用程序的选项是什么?
在生产版本中,rails热加载你的代码,所有模型,控制器,帮助方法,在启动时就加载, 因为所有rails代码在启动时被加载,没有代码被重新加载,自动加载是关闭的,当没有autoload时,在rails程序中使用config.allow_concurrency设置为true运行是安全的。这也是rails默认设置的
然而,rails仅仅热加载定义在app目录下的代码。如果我们依赖于ruby autoload,我们需要自己热加载我们自己的代码,否则,我们可能要在一个请求中间加载代码,使用LiveAssets::SSESubscriber可能发生,假设这个场景:
第一个请求访问/live_assets/sse,ruby开始加载liveAssets::SSESubscriber.如果这时候许多请求在这个时间请求这个地址,第一个请求还没完成读取subscriber,导致下面的请求只能看到subscriber定义的部分方法,解决方法就是确保LiveAssets::SSESubscriber在rails程序启动时候被热加载, 因为这是rails 插件和rails自己本身的一个共同的需求,rails为我们提供了一些约定
第一个约定是config.eager_load_namespaces配置选项,在railtie或engine中是有效的,这个配置用来保证一个命名空间列表被热加载,让我们加入LiveAssets到这个列表,在我们的engine定义里
live_assets/3_final/lib/live_assets/engine.rb
config.eager_load_namespaces << LiveAssets
现在rails能够调用LiveAssets.eager_load!来直接热加载我们的代码在生产环境里,然而我们还没有实现这个eager_load!(),让我们在ActiveSuuport::Autoload帮助下定义它
live_assets/3_final/lib/live_assets.rb
module LiveAssets
extend ActiveSupport::Autoload
eager_autoload do
autoload :SSESubscriber
end
end
通过使用ActiveSupport::Autoload扩展我们的模块,我们自动得到一个LiveAssets.eager_load!方法,需要热加载的代码都被定义在eager_autoload()代码块里,我们不再需要传递一个路径给autoload()放啊,Rails根据常量名称来猜测它。
这就是我们需要为rails热加载完成的其余代码,记住,我们每次使用这个技术在我们有代码没有被rails自动加载。通常通过ruby autoload来设置。我们可以在test/dummy目录打开一个控制台,检查rails热加载的所有命名空间
Rails.application.config.eager_load_namespaces # =>
[ ActiveSupport, ActionDispatch, ActiveModel, ActionView,
ActionController, ActiveRecord, ActionMailer, LiveAssets::Engine,
LiveAssets, Dummy::Application ]
记住这个热加载技术不仅仅对线程服务器puma有好处,也适用于unicorn. unicorn在启动后通过一个rails程序快照来操作。 通过热加载我们的代码,我们确保这个快照包含了我们所有提前启动的代码 不需要浪费时间在每次请求时加载代码。
因此,决定哪个服务器用来推送是复杂的,对于长链接请求通常取决于你的WEB服务器处理并发链接的能力 例如,unicorn通过一个线程池,每个web服务器能紧能够在一个时间处理一个请求(单线程多处理模型),如果一个web服务器正在推送流数据,或者接收一个巨大文件上传。就不能处理其他请求你,即使数据仅仅每10秒发送一次。换句话说这章我们使用的puma web服务器能够处理其他请求即使当我们在推送流数据的时候
不幸的是,没有银弹,当涉及到部署时,最好的选择是对可用的不同Web服务器进行基准测试。像puma这种多线程服务器能都处理多请求,thin也可以作为事件服务器,然而,在1.5版本,thin让然不支持推送流, 像Passenger 和 Rainbows 允许你混入不同的并发风格,所以你有一个混合的多线程多任务处理部署选择。 对于混入更多选择,你或许得到最好的结果就是不熟在像jruby和rubinius这样的平台
不同的平台给予开发者不同的安全保证。例如,array操作在jruby中不是线程安全的。这就是一个我们插件中的问题,LiveAssets.subscribers是全局数据结构,可能发生两个请求尝试请求一个订阅事件,在同一时间。 破坏我们的数据结构,那就是说我们需要使用互斥结构包装我们的订阅者这操作,确保仅有一个线程执行一段特定的代码,在某一时刻
live_assets/3_final/lib/live_assets.rb
@@mutex = Mutex.new
def self.subscribe(subscriber)
@@mutex.synchronize do
subscribers << subscriber
end
end
def self.unsubscribe(subscriber)
@@mutex.synchronize do
subscribers.delete(subscriber)
end
end
http://code.macournoyer.com/thin/
https://www.phusionpassenger.com/
http://rainbows.rubyforge.org
http://jruby.org/
http://rubini.us/
通过再次运行测试,我们的测试应该还是绿色,我们的代码现在是线程安全的在jruby上,记住这很重要:每次在请求中全局状态被改变,我们都要检查确保是线程安全的,对应相应的动作.只有写线程安全的代码我们才能有多种部署选择可能性