When the Rails Magic Stops. Cookies, Storage, and Sessions.
tldr: Rails session store can run into a race conditions when heavily using ajax. A possible solution is using server side session store instead. Further session store issues occur when you have multiple thread / processes.
Recently I worked on a small Ruby quiz application and wanted to use rails session store to keep track of people’s progress even if they refreshed or closed their browser. I thought no problem, sessions are pretty straight forward, all I have to do is check if the there is an existing session and if not create a new one.
The code manifested into 2 controller actions. One that gets an existing user or creates one if one does not exist already. Another to update a user’s quiz result.
def get_current_result
@user =User.where(id: session[:user]).first
unless @user
@user = User.new
@user.save!
session[:user] = @user.id
end
render @user.result.to_json
end
def update_result
@user = User.find(session[:user])
@user.update_result!(params[:result])
render @user.result.to_json
end
The program would first call get_current_result
to set up the session, then use the session data on all successive calls to update_result
. update_result
would use the session data to find the user and update their result. This seemed like a straightforward solution and I thought it would work fine without thinking too much about how the session was stored. However it turned out that was not the case and it totally did matter how my session data was stored.
First, let’s examine exactly what is happening to the cookie session store when my little quiz app executed.
Exhibit #1:
- A new user comes to the quiz app, and we make a ajax call to
get_current_result
from the client - The server creates a new users and saves the user id to the session.
- The session data data is then returned to the client via a cookie
- The cookie is saved on the client side
- Next the inquisitive student answers a question on the quiz app, and we make an ajax call to update_result
-
update_result
sends the client cookie with the session data back to the server - The server uses the saved session data to look up the user and update their quiz result for that user
- In the response, the cookie is again returned to the client
- The cookie is again saved on the client side
As you can see every time a call is made, the cookie (if it exist) is sent with the request and is returned whether it is updated or not, via the response. The above seems to work fine but what happens if you do more than one ajax call at once? That is basically what I was doing. At the same time I was getting the current result for the user, I was getting the quiz data via a different call. In this call all I was returning was the quiz itself and I wasn’t modifying the session at all. Unfortunately, with this extra call, we start to see the rails session magic break down.
Exhibit #2:
- A new user comes to the quiz app, and we make a ajax call to
get_current_result
ANDget_quiz
at the same time from the client (cuz who would make that call synchronously) - Its a race to see which request can get through the internet and arrive at the server first. In this example lets just say
get_current_result
arrives first. - The request for
get_current_result
arrives and the server creates a new user and saves the user id to the session. - The session data is then returned to the client via a cookie
- The request for
get_quiz
arrives. The session data is NOT modified - The blank session data is then returned to the client via a cookie
- The response from
get_current_result
arrives back at the client and the cookie is saved - The response from
get_quiz
arrives back at the client and overwrites the cookie that was just saved. - The cookie/session is now nil again!
- Next the inquisitive student answers a question on the quiz app, and we make an ajax call to
update_result
. -
update_result
send the client cookie with the session data back to the server - The server tries to look up the user using the session data, but its blank :(. Panic ensues.
As you can see from the example, the session cookie is updated on every request, regardless of if the session was modified or not. Depending on when the response gets back to the client last, thats the cookie that will be used in the next call. For example, if in our previous example, if get_current_result
’s response was slower than get_quiz
, then our cookie would have the correct data and the next call to update_response
would of work fine! So sometimes it will work and sometimes not all depending on the internet gods. This type of race condition is no fun to deal with.
The implications of this is that using cookie storage for sessions when you are doing multiple ajax call is just not safe. All information saved in the session might be wrong or nonexistent next time you check. So whats the solution?
Synchoronous AJAX
If you made all ajax calls synchronous waiting for the response of a previous ajax call before making another, you could solve this problem. However, that would greatly hinder the performance of your application, and overall a practice that isn’t commonly used. Also if you had a lot of ajax code you’d have to write code like this:
$.getJSON(‘/quiz’, function(data) {
$.getJSON(‘/answers’, function(data) {
$.getJSON(‘/results’, function(data) {
$.getJSON(‘/internet’, function(data) {
// do stuff.
});
});
});
});
** Obviously, you could clean this up a bit but you get my point.
Server side session store
A better solution would be to use a server side session store like active record or memcache. Doing so prevents the session data from being reliant on client side cookies. Session data no longer has to be passed between the client and the server which means no more potential race conditions when two ajax are simultaneously made!
Heres is my previous example using active record session store.
Exhibit #3:
- A new user comes to the quiz app, and we make a ajax call to
get_current_result
ANDget_quiz
at the same time from the client - Both
get_current_result
andget_quiz
are requested. Again, lets just say theget_current_result
request arrives first. - The request for
get_current_result
arrives and the server creates a new users and saves the user id to the session. - The session data is then saved to the database.
- The request for
get_quiz arrives
. The session data is NOT modified. - Both
get_current_result
andget_quiz
respond back to the client - Next the inquisitive student answers a question on the quiz app, and we make an ajax call to
update_result
. - The server pulls the session data from the database, and uses it to save the user’s results.
Now that was pretty straight forward! No passing the session back and forth between the client and server and no race conditions when you have multiple ajax calls. All my issues were solved by changing the type of session storage I used without having to change a line of code!
Finally, no solution is without its trade offs. Since all sessions ever created are stored in the database, the database can get pretty large. Because of this, we’ll need to run a cron job every so often to clean up old sessions. On top of this, you can STILL run into race conditions if using a multi process or multi threaded web server which honestly I haven’t tried, and is a topic for another time.
Anyways, that’s it! Rails is a great framework but sometimes you have to graduate from the “defaults” like cookie storage and try some of the other options out there!