Better Globals With a Tiny ActiveSupport Module

Generally, global variables are bad. But sometimes, a global in the right place can make your code a lot simpler.

In Rails, you might set data once during a request, but use it in every layer of your app. Which user is making the request? What permissions do they have? Which database connection should be used?

This information should be available throughout your code, so passing it everywhere would just be a bunch of noise. But using a Ruby global variable, or setting a class variable, will cause its own problems. Multiple threads could overwrite it, and you’d end up with a huge mess, and maybe unrecoverably bad data.

You need something that’s global, but for just your request.

The plain Ruby way

Usually, you’ll see this handled with Thread.current[]. It looks like this:

Thread.current[:current_user] = user

This is simple enough. It’s a decent solution. But it has two big drawbacks:

  1. Someone could accidentally overwrite your data.

    What if two people picked the same key? You’d stomp on each other’s data. And if you’re storing something like users, that would be seriously bad. In your own apps, this probably won’t be a problem. But if you’re writing gems, or your Rails app is big and messy, it’s something you need to think about.

  2. It’s not well-structured.

    Thread.current[] is just a big bag of data. You can’t easily document it. If you want to know what you can take out of it, you have to search for what you put into it.

    Of course, if you’re putting away enough global data that this is a problem, you have more to worry about than Thread.current[]’s lack of structure. But it’s still a point to keep in mind.

So, is there a better solution than Thread.current[]? As you might imagine, Rails itself manages a lot of request-local data. And the box of goodies that is ActiveSupport has a different pattern to follow.

The Rails way

If you dig through ActiveSupport, you might run into ActiveSupport::PerThreadRegistry. You can probably tell from the name that this is exactly what we’re looking for.

With ActiveSupport::PerThreadRegistry, the earlier example would look like this:

class RequestRegistry
  extend ActiveSupport::PerThreadRegistry

  attr_accessor :current_user, :current_permissions
end

RequestRegistry.current_user = user
RequestRegistry.current_user # => user

It’s a tiny bit more work. But with ActiveSupport::PerThreadRegistry:

  • You have a place to put documentation. Anyone looking at your class will know exactly what global data you’re expecting, and what global data they can use.

  • You get an API that looks good. In fact, it looks like you’re just setting class variables. That’s probably what you’d be doing if you didn’t have to worry about threads.

  • Any data you set on the class will be namespaced. You don’t have to worry about RequestRegistry.current_user colliding with PhoneNumberApiRegistry.current_user, they’re treated totally separately.

Rails uses ActiveSupport::PerThreadRegistry internally, for things like ActiveRecord connection handling, storing EXPLAIN queries, and ActiveSupport::Notifications. So if you’re looking for more good examples of the power of PerThreadRegistry, Rails itself is a great place to start.

When threads and requests don’t match

For some Rails servers, a single thread handles multiple requests. This means anything you put into a PerThreadRegistry will stick around for the next request. This is probably not what you want.

You could do what Rails does, and clean up the things you put into the PerThreadRegistry after you’re done with them. Depending on what you put in there, you might be doing this anyway.

In the comments, MattB points to an easier solution: request_store. It acts like Thread.current[], but clears itself after each request.

This leaves the door wide open for someone to create a PerRequestRegistry, which combines the safety of request_store with the API of PerThreadRegistry. If someone’s done this already, I’d love to hear about it!

Try it out

Global data can be bad. Too many globals can quickly lead to unmaintainable code.

But if it makes sense to keep data in a central place for an entire request, go beyond Thread.current[]. Give ActiveSupport::PerThreadRegistry or request_store a shot. If nothing else, it’ll make dealing with global data a little less risky.

Pushing through tutorials, and still not learning anything?

Have you slogged through the same guide three times and still don't know how to build a real app?

In this free 7-day Rails course, you'll learn specific steps to start your own Rails apps — without giving up, and without being overwhelmed.

You'll also discover the fastest way to learn new Rails features with your 32-page sample of Practicing Rails: Learn Rails Without Being Overwhelmed.

Sign up below to get started:

Powered by ConvertKit

Did you like this article? You should read these:

Comments