One of my favourite pastimes is to go through GitHub issues for libraries I like. One of those is the Rack gem, where I found an issue titled "Middleware should be frozen by default". A couple of questions I had were: What exactly is a frozen middleware? and why should that be done?
Example: Web request count
As a simple first example, let's consider a Rack middleware which counts the number of requests received by the server. A very simple (and broken) implementation might look like this:
The @counter
instance variable gets incremented each time call
method gets called, which happens for every request. If you're not
familiar with what a middleware is, or how they get used, these
resources might be useful:
Rack Middlewares on Railscasts Understanding Rack apps and Middleware Introduction to Rack Middleware Middleware recipes on Sinatra Recipes
Running this application in a single threaded environment results in the following output:
You can run this is in single threaded mode as -
Running this in a multi-threaded environment, however, results in the following output:
In the multi-threaded environment, the counter doesn't increment. This is called a race condition, and occurs when two or more threads can access shared data and they try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any time, you don't know the order in which the threads will attempt to access the shared data. Hence, the result of the change in data is dependant on the thread scheduling algorithm i.e. both threads are racing
to access/change the data.
Achieving thread-safety
When we want to avoid thread safety issues in multi-threaded environments we have some options:
- Not mutate the state in the middleware
- Freeze middleware instances to catch the thread-safety issues in the middleware that you didn't write yourself.
- Use data structures from the concurrent-ruby gem.
How do we do this?
Example: Web request thread-safe count
The word atomic
it means that the contents of the block are executed to completion without other threads being able to read/modify the value (note that this is not same as a mutex). Multiple threads attempting to change the same AtomicReference
object will not make it end up in an inconsistent state.
Freezing middleware instances
Rack middleware is initialized only on the first request of the process. So any instance variables acts like class variables, and modifying them in call()
isn't thread-safe. It's necessary to dup
the middleware to be thread-safe. A middleware should be frozen to avoid potential issues with handling concurrent requests. Rack recently introduced a freeze_app
method to freeze middleware instances. An example usage of that would be:
In this example, we are initializing an instance variable to 1
when we hit the /a
url. We call freeze_app
method in the middleware. When we run this program, and hit /a
multiple times the freeze_app
method will notify us that there is a problem by raising an exception, which you wouldn’t otherwise know:
The server will respond with 200 for all other URLs because we are not
modifying the instance variable in those. Internally freeze_app
method calls a .freeze
on the middleware instances.
Can I unfreeze a frozen object?
No, it's not possible in MRI and JRuby.
Hope you found this useful.