Today I was playing with Padrino session handling. Padrino provides a session
variable to store user specific information. I don’t like to use this session variable directly all the time, I believe in data encapsulation to hide its logic and give only an interface to reach session info. Since the recommended size of session object is lower than 4Kb, I usually store only primary keys in the session store and build up the object according to this key. So if I use an own-defined SessionStore
to hide the session handling logic, the SessionStore.user
would return my User object, and it’s nicer than using User.find(session[:user_id])
every time when I want to use the actual user. By the way I can implement caching logic to SessionStore to reduce the database hits.
The first very simple logic of the SessionStore is the following:
1 2 3 4 5 6 | # app.rb class MyApp < Padrino::Application before do SessionStore.init_with(session) end end |
# app.rb class MyApp < Padrino::Application before do SessionStore.init_with(session) end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # lib/session_store.rb class SessionStore def self.init_with(session) @@session = session end def self.user=(user) @@session[:user] = user.id end def self.user User.find(@@session[:user]) end end |
# lib/session_store.rb class SessionStore def self.init_with(session) @@session = session end def self.user=(user) @@session[:user] = user.id end def self.user User.find(@@session[:user]) end end
When the application starts the before filter hook initializes this SessionStore with the Padrino session, and later I just call SessionStore.user
when I want to work with the current user. I used static variables that can be very dangerous. Why? Because it’s not thread-safe!
What happens in a Padrino/Sinatra/Rack application?
When we start the server, it will initialize the application and load the code of SessionStore into memory. When the user hits the server, the before
hook initializes the store with the user’s session. The @@session
value is reachable from any thread in my application, so what happens if somebody in the same time hits the application too? Let’s see the process:
- User1: init_with(user1_session)
- User1: user=(user1_session[:user_id])
- User2: init_with(user2_session)
- User2: user=(user2_session[:user_id])
- User1: current_user_id => user2_session[:user_id]
I hope you feel the security hole, since user1 will see user2′s profile! I know the likelihood of this happening is low, but if your server is overloaded or you have a bottleneck in the system (slow database connection), it can be a problem.
When you start the application with padrino start
command, it will start one process and one thread. It’s good for developing, but the production environment can be different, more processes with more threads! So if I place a sleep 10
in the SessionStore.init_with
method and load the page in two different browser, the second request must wait until the first request is processed. I guess now Padrino runs only one thread, later it can change, be careful, change the code!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class SessionStore def self.init_with(session) self.session = session end def self.user=(user) self.session[:user] = user.id end def self.user User.find(self.session[:user]) end private def self.session @@session[Thread.current.object_id] end def self.session=(value) @@session[Thread.current.object_id] = value end def self.init_session @@session = {} end init_session end |
class SessionStore def self.init_with(session) self.session = session end def self.user=(user) self.session[:user] = user.id end def self.user User.find(self.session[:user]) end private def self.session @@session[Thread.current.object_id] end def self.session=(value) @@session[Thread.current.object_id] = value end def self.init_session @@session = {} end init_session end
The other very important thing is the @@session
variable will be stored in the memory while your application is running. Servers run more days, weeks, months, I guess you don’t want to consume all your memory. Use the after hook for cleaning!
1 2 3 4 5 6 | # app.rb class MyApp < Padrino::Application after do SessionStore.clean end end |
# app.rb class MyApp < Padrino::Application after do SessionStore.clean end end
1 2 3 4 5 6 | # lib/session_store.rb class SessionStore def self.clean @@session.remove(Thread.current.object_id) end end |
# lib/session_store.rb class SessionStore def self.clean @@session.remove(Thread.current.object_id) end end
So in this way you can have an own SessionStore that hides all the application logic without any multi-thread issues.
Leave Your Response