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?
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.
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
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.
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.