如果这段代码不工作,Rails怎么可能工作?Ractor似乎从根本上与许多大量使用的Rails API不兼容。
require 'logger'
class Rails
def self.logger
@logger ||= Logger.new(STDOUT)
end
end
Ractor.new do
Rails.logger.info "Hello"
end.take
周末,我在Diffend.io中增加了对Ractors的支持,这是一个为Ruby和Rails提供的OSS供应链安全和管理的免费平台,所以我对这个话题比较新鲜。Mike的代码说明了开发者在使其代码与Ractors兼容时将面临的一个问题。
当你试图运行它时,你将会遇到一个异常。
terminated with exception (report_on_exception is true):
`take': thrown by remote Ractor. (Ractor::RemoteError)
`logger': can not access instance variables of classes/modules
from non-main Ractors (RuntimeError)
有没有什么方法可以保留Rails#logger API,并允许它从我们想要的任何Ractor中使用?
有!有!有
所以,让我们先解释一下为什么这段代码不能工作。
def self.logger
@logger ||= Logger.new(STDOUT)
end
这段代码实际上有两个问题,尽管只有一个是立即可见的。
- 你不能从主处理器以外的Ractors访问可共享对象的实例变量。
- 你不能从非主Ractor访问STDOUT(至少不是这样)。
好消息是在字里行间说的:虽然我们不能使用可共享对象,也不能引用实例变量,但我们可以保留Rails.logger API!
class Rails
def self.logger
rand
end
end
Ractor.new do
Rails.logger
end.take.then { p _1 }
#=> 0.06450369439220172
但是我们想共享一个记录器,对吗?嗯,不完全是。我们想要的是能够使用相同的API来记录一些信息。而这正是这里的关键点。
我们可以迅速绕过所有的问题。我们只需要一个单独的Ractor,它将为我们的应用程序运行所有的日志记录,并与标准的记录器兼容的API。
我们需要什么来实现这一点呢?不多。我们需要。
- 创建一个Ractor,该Ractor将拥有唯一的应用范围内的记录器。
- 创建用于记录的API。
- 将Ractor连接到
Rails#logger接口。
这一切都可以通过几行代码实现。
class Rogger < Ractor
def self.new
super do
# STDOUT cannot be referenced but $stdout can
logger = ::Logger.new($stdout)
# Run the requested operations on our logger instance
while data = recv
logger.public_send(data[0], *data[1])
end
end
end
# Really cheap logger API :)
def method_missing(m, *args, &_block)
self << [m, *args]
end
end
class Rails
LOGGER = Rogger.new
def self.logger
LOGGER
end
end
Ractor.new do
Rails.logger.info "Hello"
end
而当我们运行它时,我们最终会遇到不同的挑战。
terminated with exception (report_on_exception is true):
ruby/3.0.0/logger/formatter.rb:15:in `call': can not access global variables $$ from non-main Ractors (RuntimeError)
from ruby/3.0.0/logger.rb:586:in `format_message'
from ruby/3.0.0/logger.rb:476:in `add'
from ruby/3.0.0/logger.rb:529:in `info'
from test.rb:23:in `public_send'
from test.rb:23:in `block in new'
更新:我下面说的那个拉动请求已经被合并了,所以这个猴子补丁不再需要了。
事实证明,Ruby的defaulf日志格式并不适合Ractor。我已经开启了修复这个问题的拉动请求,所以一旦合并,基本的Ruby日志格式就可以正常工作了。目前,我们将给它打上猴子补丁。
class Logger::Formatter
def call(severity, time, progname, msg)
Format % [
severity[0..0],
format_datetime(time),
Process.pid,
severity,
progname,
msg2str(msg)
]
end
end
有了这个,我们可以从任何我们想要的ractor中运行我们的日志。
require 'logger'
class Logger::Formatter
def call(severity, time, progname, msg)
Format % [
severity[0..0],
format_datetime(time),
Process.pid,
severity,
progname,
msg2str(msg)
]
end
end
class Rogger < Ractor
def self.new
super do
logger = ::Logger.new($stdout)
while data = recv
logger.public_send(data[0], *data[1])
end
end
end
def method_missing(m, *args, &_block)
self << [m, *args]
end
end
class Rails
LOGGER = Rogger.new
def self.logger
LOGGER
end
end
Ractor.new do
Rails.logger.info "Hello"
end
sleep(1)
ruby test.rb
I, [2020-09-28T18:23:56.181512 #11519] INFO -- : Hello
总结
在Rails等事物中提供Ractor支持并不容易。有许多挑战需要解决,但同时,我认为这是一个利用Ruby新功能的绝佳机会。这也是一个摆脱Ruby和Rails中的反模式的好机会,从我有记忆以来,这些反模式就一直存在。由于Ractors的存在,有一个全新的工程世界将更容易实现。
今年,我还想探索用Ruby VM运行同质化的Docker容器的可能性,在其中我可以对运行在特定公会中的服务进行负载平衡。理论上说,这可以允许在亚秒级缓解突然的流量高峰,而不需要有许多过度配置的实例。