Padrino and my SessionStore

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:

  1. User1: init_with(user1_session)
  2. User1: user=(user1_session[:user_id])
  3. User2: init_with(user2_session)
  4. User2: user=(user2_session[:user_id])
  5. 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.