Thread safety for your Rails
Published over 4 years ago
Rails 2.2 marks the first release of thread safe Rails. But “thread safety” alone, without any context, doesn’t mean shit. When people say Rails is “thread safe” ( or otherwise ), they usually refer to the dispatching process of Rails. Before 2.2, Rails dispatching looked like :
@@guard.synchronize do dispatch_unlocked end
And now it looks somewhat like :
dispatch_unlocked
Long story short, Rails can now serve multiple requests in more than one ruby threads ( or native threads if you’re on JRuby ) parallelly. Charles Nutter has done a good job of explaining the details here.
You totally should if :
You totally should NOT if :
You may have heard a bunch of hype about how threads make everything 100x faster, this is far from the truth. Don’t believe everything the hype merchants want to sell you, test your application first and see if it helps.
Koz’s comments sums it up nicely :
I think the more interesting issue to consider is whether your application will benefit from ‘threaded dispatching’ at all.
The performance of green threads in ruby is kind of disappointing, as are the number of different options which block the interpreter. IO, regexps, calling most native libraries, etc. Odds are with matz’s ruby you’re infinitely better off using passenger + ruby enterprise edition than ruby threads.
JRuby is another matter altogether, and it’s jruby users who should be most excited about this stuff, and the most willing to help us iron out any last bugs.
Currently, you’ll need to manually patch Mongrel’s built in Rails handler for testing multithreaded dispatching. I’ve submitted a patch to mongrel and hopefully there’ll be a new gem version of mongrel soon. In the mean time, monkey patch FTW.
Just put the following lines in your production.rb
config.threadsafe!
However, that’s not enough. There are some consequences if you have never made sure to write thread safe code. They are, however, simple to fix. Usually.
What this means is, if in Thread A you require a file named whatever.rb in which defines a class called Whatever, the class Whatever can be visible from Thread B even before Thread A has finished loading whatever.rb. And because of this ruby behavior, Rails now preloads everything inside app directory.
config.threadsafe! also disables automatic loading by ActiveSupport::Dependencies.
ActiveSupport::Dependencies uses ruby’s const_missing hook to load files automatically for you, whenever possible. For example, if you have following file inside your application’s lib/ directory :
# hello.rb class Hello def world "hello world" end end
Rails has traditionally saved you the trouble of requring that file manually inside your application. Whenever you access Hello ( Hello.new for example ) constant for the first time, ActiveSupport::Dependencies loads hello.rb for you automatically. Note that this is only possible if the file name matches the class name that it defines.
But as this behavior is disabled when you calls config.threadsafe!, you’ll now need to require the file hello.rb manually before Rails starts serving the requests ( typically inside environment.rb or an initializer ).
Alternatively, you can just add lib/ directory to eager load paths. The following inside production.rb will do that :
config.eager_load_paths << "#{RAILS_ROOT}/lib"And that will make Rails preload everything inside lib/ directory.
Imagine your controller having a code that does :
class HomeController < ApplicationController @@visits = 0 def index @@visits += 1 render :text => @@visits end end
This code is not safe if you enable multi threaded dispatching. All your instance methods ( actions in case of controllers ) should only read global values ( $vars, @@vars, class instance variables ) and never modify them.
Here’s a better example which would explains the consequences as well :
class HomeController < ApplicationController before_filter :set_site def index end private def set_site @site = Site.find_by_subdomain(request.subdomains.first) if @site.layout? self.class.layout(@site.layout_name) else self.class.layout('default_lay') end end end
What happens here is :
Imagine your application has two possible subdomains :
When you call self.class.layout(value), Rails will store the value inside a class variable @@layout_, which causes a race condition if called from multiple instance methods in different threads. Wikipedia pagecondition will do a better job of explaining what is a race condition if you have never bothered about it before.
Let us assume that two users are accessing the application : UserA and UserB. UserA’s request is served by Thread1 and UserB’s request is served by Thread2. Here, numbers also represent the order in which these events occur :
The thread safe way to write this code is :
class HomeController < ApplicationController before_filter :set_site layout :site_layout def index end private def set_site @site = Site.find_by_subdomain(request.subdomains.first) end def site_layout if @site.layout? @site.layout_name else 'default_lay' end end end
When you use layout :site_layout, Rails will use the return value of site_layout instance method to determine the layout, which makes it a thread safe way. Please note that this is not the same as calling layout ‘something’. If you pass a string to the class method layout, Rails will use the passed value as the layout.
( Example inspired from Dynamic Layouts Railscast )
if you must, you can always use Thread local variables as the last resort. Ruby provides you with a magical hash Thread.current[] inside any executing thread, where you can store variables accessible anywhere from inside that specific thread. Really, you can check this docs
The following code :
threads = [] threads << Thread.new do Thread.current[:hello] = 1 sleep 2 puts "From T1 : #{Thread.current[:hello]}" end threads << Thread.new do Thread.current[:hello] = 10 puts "From T2 : #{Thread.current[:hello]}" end threads.each {|t| t.join }
will produce :
From T2 : 10 From T1 : 1
You might have seen this in use in with any current_user hacks : Here or here. But it’s still a hack.
If you’re familiar with Rails source ( of interested in being familiar ), you can find Rails using Thread.current[] at several places : Thread.current[:time_zone] or Thread.current[‘query_cache’]. I18 gem uses Thread.current[:locale] to store the value of locale specific to the thread.
But as I said earlier, Thread.current should be used as a last resort only.
There is always the big fat mutex which can be slapped around a piece of code that you want to execute exclusively per thread. You should check the wikipedia page if you’re looking for some explanation :
class HomeController < ApplicationController @@lock = Mutex.new def index @@lock.synchronize do thread_unsafe_code end end private def thread_unsafe_code if @@something == 'hello' do_hello elsif @@something == 'world' do_world else @@something = 'nothing' end end end
This ensures that only one thread can be executing thread_unsafe_code() method at any given point in time. Other threads will block and wait indefinitely for the executing thread to release the lock acquired by @@lock.synchronize.
Adam Hooper raised three valid concerns :
Chances of thread unsafe code being in Rails are close to none. There have never been anything inherently thread unsafe about Rails codebase. If some people had you think otherwise, you listened to the wrong bunch of people/FUD. We’ve had a list of thread unsafe code inside Rails for a long time, and it was a small list.
However, thread safety is like a Random Number Generator – You can never be sure
Jeremy says : No. They are not. Most, in fact, are probably threadsafe. Your claiming this is a major issue is a fairly good indicator that you don’t actually know the core issues with thread safety in Ruby/Rails.
Me : That’s not true. At least all the plugins that I use, are thread safe. Having said that, you should never use a plugin without getting yourself familiar with it’s code base.
Short answer, don’t jump the ship if you can’t be bothered about ensuring your code is thread safe. Always stick to “Simplest thing that works” motto IMO. You could just spend some time researching if running multithreaded Rails is going to benefit your application/business at all or not and evaluate that against the risk/time involved.
But that doesn’t make threadsafe Rails unsuitable for production use. It makes your specific application/team unsuitable for using thread-safe Rails in production mode. Multi threaded programming has never been easy. However, if you write good OO code, thread safety usually comes for free.
UPDATES :