13

I have to write a threaded Rails app because I am running it atop of Neo4j.rb, which embeds a Neo4j graph database inside the Rails process, and thus I have to serve multiple requests from the same process. Yeah, it'd be cool if connecting to a Neo4j database worked like SQL databases, but it doesn't, so I'll quit complaining and just use it.

I'm quite worried about the implications of writing concurrent code (as I should be), and just need some advice on how to handle common a common scenario - a controller sets an instance variable or a variable in the session hash, then some stuff happens. Consider the following crude code to demonstrate what I mean:

# THIS IS NOT REAL PRODUCTION CODE
# I don't do this in real life, it is just to help me ask my question, I
# know about one-way hashing, etc.!

class SessionsController
  def create
    user = User.find_by_email_and_password(params[:email], params[:password])
    raise 'auth error' unless user
    session[:current_user_id] = user.id
    redirect_to :controller => 'current_user', :action => 'show'
  end
end

class CurrentUserController
  def show
    @current_user = User.find(session[:current_user_id])
    render :action => :show # .html.erb file that uses @current_user
  end
end

The question: Are there any race conditions in this code?

In SessionsController, are the session hash and the params hash thread-local? Say the same browser session makes multiple requests to /sessions#create (to borrow Rails route syntax) with different credentials, the user that is logged in should be the request that hit the line session[:current_user_id] = user.id last? Or should I wrap a mutex lock around the controller action?

In the CurrentUserController, if the show action is hit simultaneously by two requests with different sessions, will the same @current_user variable be set by both? I.e. will the first request, as it is processing the .html.erb file, find that it's @current_user instance variable has suddenly been changed by the second thread?

Thanks

Asfand Qazi
  • 6,586
  • 4
  • 32
  • 34

2 Answers2

14

Each request gets a new instance of your controller. As a consequence controller instance variables are thread safe. params and session are also backed by controller instance variables (or the request object itself) and so are also safe.

pd40
  • 3,187
  • 3
  • 20
  • 29
Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • This is an old thread, sorry . But I've a follow up. That makes a lot of sense (and why people don't worry about @variables in controller methods. However if that's true, I'm confused at how this guy every had problems with his counter http://tenderlovemaking.com/2012/06/18/removing-config-threadsafe.html – Oliver Shaw Feb 12 '17 at 13:19
  • 1
    @OliverShaw in that post counter is a class instance variable (because it is in the `class << self` block), so all the requests would be modifying the same counter – Frederick Cheung Feb 12 '17 at 13:41
  • Coming back to Ruby. I'd remembered class variables, but not class instance variables. Thanks! – Oliver Shaw Feb 12 '17 at 14:20
3

It's important to know what is shared between threads and what isn't.

Now back to your specific example. Two requests hit CurrentUserController#show simultaneously, hence they are handled by two concurrent threads. The key here is that each thread has its own instance of CurrentUserController, so there are two @current_user variables which don't interfere. So there's no race condition around @current_user.

An example of race condition would be this:

class ApplicationController < ActionController::Base
  before_each :set_current_user
  cattr_accessor :current_user

  def set_current_user
    self.class.current_user = User.find_by_id(session[:current_user_id])
  end
end

# model
class LogMessage < ActiveRecord::Base
  belongs_to :user

  def self.log_action(attrs)
    log_message = new(attrs)
    log_message.user = ApplicationController.current_user
    log_message.save
  end
end

On more general note, because of GIL (Global Interpreter Lock) benefits from using threads in MRI ruby are rather limited. There are implementation which are free from GIL (jruby).

Serge Balyuk
  • 3,442
  • 1
  • 23
  • 23
  • Thanks for the example, however I already know that class variables would cause a race. On the GIL issue, I think you're wrong - Ruby 1.9 threads are native, and I'm using JRuby anyway. – Asfand Qazi Aug 17 '12 at 14:02
  • 1
    I'm glad you're on the right path. As for MRI ruby 1.9: it has native threads and GIL, which is global mutex used by those native threads. – Serge Balyuk Aug 17 '12 at 14:26