Rails thread safety… ain’t a thing

So because of reasons I’ve started writing Ruby again. That mostly makes me happy. Developers prostrate themselves at the foot of the gods of UX and customers’ acceptance criteria but often neglect the whole developer experience and how palatable their own tooling is. Despite some frankly pretty shaky underpinnings, there’s a lot to like in the Ruby ecosystem if you’re just trying to get shit done.

Anyway, just pinning a note mainly for my own benefit* about the liberal use of the ||= conditional assignment operator in Rails (or Rack-based apps in general). Go ahead and run this:

50.times do |i|
  Thread.new { $x ||= i }

sleep 1

puts $x

If you had neither visibility nor atomicity concerns, you’d always expect 0, right? Go ahead and run it a few more times. If you ran it with JRuby then you probably got a nonzero result straight away, anyway. Ok, so there’s a bit of bad news:

  1. ||= does not magically compile down to a CMPXCHG instruction or somesuch magic**. It’s just sugar for ‘if operand is null assign to i else return current value’. Therefore multiple concurrently executing instructions can be interleaved and potentially reference the same address before we’re done
  2.  There’s no well-defined model describing atomicity of variable reassignment or value visibility in Ruby; it’s all architecture, interpreter and interpreter-version dependent
  3. The Rack spec gives no indication whether app.call() could be invoked concurrently or not

Hrmph. So are we screwed? In the case of something like a Rails app where you’re knowingly concurrently mutating something, it’s good to wrap it in a Mutex; for the above example, that’d boil down to something like this:

lock = Mutex.new

50.times do |i|
  Thread.new { lock.synchronize { $x ||= i } }

sleep 1

puts $x

Of course, this sort of programming is horrendously deadlock/livelock prone just as in any other imperative language, regardless of type system, not to mention the fact that you just killed all the parallelism in this case; Ruby’s out-of-the-box concurrency tools are pretty coarse-grained, unfortunately.

You haven’t answered the question

Yeah, I was trying to waffle and do some veiled trolling at the same time. No, you’re not screwed. Follow a few rules of thumb and you’ll be fine:

  • Rails controllers & models seem to be reused on a per-thread basis, so if you’re sharing, e.g. class variables, you’ll at worst end up with one instance per thread (the number of which is normally capped by the server which preemptively creates them)
  • Anything at the Actionpack level or lower can be expected to be concurrently called and regenerated per-request

Rails was declared ‘thread-safe’ around 2.2.x IIRC, but that was achieved by sticking a giant mutex around the app.call() entry point, which is kind of intellectually running away from the problem. Since then the global lock was peeled back but documentation on changes to the threading model changes in the interim seems a bit thin on the ground.

I don’t want to overtly troll too much, because the challenge of concurrent programming is definitely nontrivial; the original mutex-based cop-out is not an admonishment to the Rails framework- it’s one of those things where solid foundations matter (i.e. not using C-Ruby), and one reason JRuby continues to be relevant. It took Sun, subsequently Oracle plus many other concurrency luminaries on their payroll billions of dollars and man-hours to come up with the JMM, a formidable stab at guaranteeing memory consistency across platforms where not only instruction ordering is not guaranteed (x86, ARM, I’m looking at you), but Sun, the main vendor readily agreed to mandate this in a portable, runtime-agnostic spec rather than opaquely baking the behaviour into their reference implementation. And even with all that good-will, political clout and money it took them until Java 5 to get it right. Kinda.

So yeah, congrats- you’ve beaten the odds and the threat of irrelevance and now your app has to scale; it’s time to go tackle some hard problems. Concurrency is one of them 😉


*because c’mon, nobody else fuckin’ reads this
**on MRI? What are you smoking?

Leave a Reply

Your email address will not be published. Required fields are marked *