Rails Global Variables in Rails

larrylv · 2014年12月26日 · 最后由 zgm 回复于 2014年12月27日 · 2912 次阅读

Blog: http://blog.larrylv.com/global-variables-in-rails/

In a Rails application, sometimes you may wanna use global variables for every request, with which you don't have to send the object as a parameter everywhere, like current_user for model layers (which may not be a good idea).

So how could we do that?

Ruby Global Variables

If you know Ruby well, you may know variable with a beginning with $ is global.

But global variables with $ prefix are supposed to be accessible from every single palce of your code, so they are shared among all threads, and that's definitely not what we want, right?

So basically, don't use Ruby native global variables, ever.

Thread.current

So we want our global variables to be thread-safe, a.k.a thread-local.

This is where Thread comes in.

Threads are the Ruby implementation for a concurrent programming model.

Programs that require multiple threads of execution are a perfect candidate for Ruby's Thread class.

With Thread.current method, you could get the currently executing thread. Then you could use Thread.[] and Thread.[]= to get and set thread-local variables, respectively.

Thread.current[:current_user] = user

If you read the links of ruby-doc carefully, you may notice that actually the variable is fiber-local instead of thread-local. Since we rarely use fibers these days, especially for new 2.x Ruby versions, we could assume this is equal to thread-local. But if you do use fibers, and want your variables to be thread-local, please use Thread.thread_variable_set and Thread.thread_variable_get.

But there is one problem.

If you use Thread.current with fancy evented/threaded web servers like Thin or Puma, please watch out! Values can stick around longer that you'd expect, and this can cause bugs. For example, if we had this in our controller:

def index
  Thread.current[:counter] ||= 0
  Thread.current[:counter] += 1

  render :text => Thread.current[:counter]
end

If we ran this on MRI with Webrick, you'd get 1 as output, every time. But if you run it with Thin or Puma, you get 1, then 2, then 3...

So what's the solution?

Steve Klabnik releases a gem called request_store to do that for you. Everywhere you used Thread.current, just change it to RequestStore.store. And no matter what server you use, you'll get 1 every time: the storage is local to that request.

def index
  RequestStore.store[:counter] ||= 0
  RequestStore.store[:counter] += 1

  render :text => RequestStore.store[:counter]
end

Codes of the gem are pretty simple, just insert a middleware to Rails and use Thread.current[:request_store] to store variables, and clear Thread.current[:request_store] after every request.

But with Thread.current(or RequestStore.store), there are two pains may bother you later:

1. Someone could accidentally overwrite your data.

If the other developer picks the same key with yours, and overwrite it somewhere, you app will just break. Or if you are a gem author, that is really something you need to consider.

2. It's not well-structured.

Or we could say that it's not very OO. You don't know what's in your Thread.current[], and you're gonna have to read every line of codes with Thread.current[].

And with these two pains, we may ask: What's the better solution?

ActiveSupport::PerThreadRegistry

ActiveSupport::PerThreadRegistry module is used to encapsulate access to thread local variables.

Instead of polluting the thread locals namespace:

Thread.current[:connection_handler]

We could define a class that extends this module:

module ActiveRecord
  class RuntimeRegistry
    extend ActiveSupport::PerThreadRegistry
    attr_accessor :connection_handler
  end
end

and invoke the declared instancec accessors as class methods. So

ActiveRecord::RuntimeRegistry.connection_handler = connection_handler

sets a connection handler local to the current thread, and

ActiveRecord::RuntimeRegistry.connection_handler

returns a connection handler local to the current thread.

This feature is accomplished by instantiating the class and storing the instance as a thread local keyed by the class name. In the example above a key "ActiveRecord::RuntimeRegistry" is stored in Thread.current.

The implementation is pretty simple, too. See the codes here: module PerThreadRegistry.

With PerThreadRegistry module, the previous pains concerning you are gone.

As to the previous RequestStore problem, someone provides a solution, or you could just hack it yourself. It's quite simple.

Wrap It Up

  • Don't ever use Ruby native global variables.

  • Thread.current could do it, but it's not good for complicated apps or codes of a gem.

  • Clear thread local variables after every request, or it may stick around longer than you'd expect.

  • Use ActiveSupport::PerThreadRegistry for better global variables management and document.

References

共收到 15 条回复

别用 request_store, 直接丢 request.env 就好了

还有一个错误: thin 不是基于 thread 的

#2楼 @luikore :-) thin 那个是直接 copy 的 request_store 的 README. 我改一下吧。

是说放在 request.env 里?这样不太好吧...

#3楼 @larrylv 看下实现就知道丢 request.env 是多么好的选择... 那个东西加了个 rack middleware 完了还 clear 一遍 store, 何必啊...

#4楼 @luikore model 层不能取到 request.env 吧。

#5楼 @larrylv model 层大部分情况是不需要它的, 真需要加个方法参数或者实例变量传进去就好了...

#6楼 @luikore 恩。我在最开始时说了,为了不用为每个方法都传个参数,这才是 Global Variable 的意义嘛。 不过 model 层大部分情况不需要这点我同意。

另外,个人觉得,request.env 这个东西还是不要污染的好。如果因为 request_store 的实现不好就去选择另外一种不好的实现,似乎不太准确。毕竟 request.env 不是为了让用户去放 global variables 的。

#7楼 @larrylv 不不, 它的作用就是让用户去放 request 范围内的 "global" variable 的...

真的 global 有全局变量和常量, 范围限定的 "global" 还有 thread 范围放 Thread.current fiber 范围放 Fiber.current session 范围放 session browser 范围放 cookie

#8楼 @luikore 所以就应该把所有的东西都扔到一个巨大的 hash 里吗?我不太能认同。 即使是 request 范围内的,request.env 也只是放了真正的请求信息,而不是这个请求的逻辑结果从而用来传递数据,不然为什么叫 request.env 而不是 request.data ...

这也是 ActiveSupport::PerThreadRegistry 试图解决的问题,更好的管理逻辑相关的 global 信息。

#9楼 @larrylv warden 就是把 current_user 放那里的啦, rack 的 spec 都说应用程序可以随便改了, 而且 rack 很小心的把 rack 相关的 key 都写成 "rack.xxx" 了. global 当然是 environment 的一部分, 进程 global 不就是用 ENV[] 访问吗?

#10楼 @luikore 如果放在 request.env 里,那跟把这些写到 ApplicationController 里,搞个 memoize 的方法存下来,没什么区别啊?而且把业务逻辑结果放在了它该存在的地方,而不是 request.env ...

warden 放在 request.env 里是为了 rack app 间的数据通信吧,warden 为了让下一层的 rack app 使用到,当然需要这么做没错,但这不代表我最后一层的 rack app 还要这么做啊。我同一层的数据共享为什么非要用到 request.env 呢...

其实说起 Global Variables 以及写这篇 blog, 主要是因为在复杂的应用中现在的 Thread.current[] 实现方式对于管理来说很困难,所以才会想说起 PerThreadRegistry 这个 module. 而实现在 request.env 里还是没有解决到这个痛点(对于我来讲是痛点,:) ),所以我还是认为使用 request.env 是个 bad idea.

#11楼 @larrylv 好吧, 对于我来说屁大的事加个 gem 加一层 middleware 牵连无辜 action 才是个痛点...

感觉加在 request.env 还不如写在 ApplictaionController 里面.

也没觉得把 current_user 当参数传有什么不好的地方, 我就非常不喜欢 User.current 这种写法.

#13楼 @zgm User.current 就像毒品,最初用的时候很嗨!后面副作用很大! 对大项目而言是有害的。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册