Tuesday, December 1, 2009

Twisted Web in 60 seconds: session endings


Welcome back to "Twisted Web in 60 seconds". Over the previous two entries, I introduced Twisted Web's session APIs. This included accessing the session object, storing state on it, and retrieving it later. I described how the Session object has a lifetime which is tied to the notional session it represents. In this installment, I'll describe how you can exert some control over that lifetime and react when it expires.




The lifetime of a session is controlled by the sessionTimeout attribute of the Session class. This attribute gives the number of seconds a session may go without being accessed before it expires. The default is 15 minutes. In this example, I'll show you change that to a different value.




One way to override the value is with a subclass:



  from twisted.web.server import Session

 class ShortSession(Session):
     sessionTimeout = 60



To have Twisted Web actually make use of this session class, rather than the default, it is also necessary to override the sessionFactory attribute of Site. I could do this with another subclass, but I can also do it to just one instance of Site:



  from twisted.web.server import Site

 factory = Site(rootResource)
 factory.sessionFactory = ShortSession



Sessions given out for requests served by this Site will use ShortSession and only last one minute without activity.




You can have arbitrary functions run when sessions expire, too. This can be useful for cleaning up external resources associated with the session, tracking usage statistics, and more. This functionality is provided via Session.notifyOnExpire. It accepts a single argument: a function to call when the session expires. Here's a trivial example which prints a message whenever a session expires:



  from twisted.web.resource import Resource

 class ExpirationLogger(Resource):
     sessions = set()

     def render_GET(self, request):
         session = request.getSession()
         if session.uid not in self.sessions:
             self.sessions.add(session.uid)
             session.notifyOnExpire(lambda: self._expired(session.uid))
         return ""

     def _expired(self, uid):
         print "Session", uid, "has expired."
         self.sessions.remove(uid)



Keep in mind that using a method as the callback will keep the instance (in this case, the ExpirationLogger resource) in memory until the session expires.




With those pieces in hand, here's an example that prints a message whenever a session expires, and uses sessions which last for 5 seconds:



from twisted.web.server import Site, Session
from twisted.web.resource import Resource
from twisted.internet import reactor

class ShortSession(Session):
   sessionTimeout = 5

class ExpirationLogger(Resource):
   sessions = set()

   def render_GET(self, request):
       session = request.getSession()
       if session.uid not in self.sessions:
           self.sessions.add(session.uid)
           session.notifyOnExpire(lambda: self._expired(session.uid))
       return ""

   def _expired(self, uid):
       print "Session", uid, "has expired."
       self.sessions.remove(uid)

rootResource = Resource()
rootResource.putChild("logme", ExpirationLogger())
factory = Site(rootResource)
factory.sessionFactory = ShortSession

reactor.listenTCP(8080, factory)
reactor.run()



Since Site customization is required, this example can't be rpy-based, so it brings back the manual reactor.listenTCP and reactor.run calls. Run it and visit /logme to see it in action. Keep visiting it to keep your session active. Stop visiting it for five seconds to see your session expiration message.




That pretty much wraps things up for Twisted Web's built in session support. Next time I'll cover some of Twisted Web's proxying features.

5 comments:

  1. Is it a lot of effort to tie a long-running session to a storage backend like file or a RDBMS? Consider a shopping cart which needs to be persisted for a month. And while it can be stored in-memory and that's generally good, there are two caveats: even a rare visitor will occupy memory for nothing, and the second one is that the contents is lost during a server restart (I mean normal restart when there's a chance to save the state; abnormal restarts are more or less destructive anyway).

    Of course it can be done by extending, but I'm curious whether there are built-in, "kosher" means to do that in the Session itself.

    ReplyDelete
  2. You will have to do some extending to achieve this sort of thing. The hooks are there - Site.sessionFactory, Session.notifyOnExpire, etc. Twisted Web itself (at least for now) stays away from trying to implement any sort of persistent session storage.

    ReplyDelete
  3. thank you very much

    ReplyDelete
  4. I notice that you've got a second set of sessions. The one that comes with Site and a second set that is a member of ExpirationLogger. Is the fact that Site manages a set of sessions (actually, a dict) an implementation detail?

    In other words, if I want users to be able to log in, log out and have their logouts happen automatically after N seconds, do I need to manage a collection of sessions myself?

    ReplyDelete
  5. I created the extra set to track sessions because I wanted to only call notifyOnExpire once for each session. The session tracking done by the Site doesn't make it easy to do this. I could have overridden some other part of the session instantiation process (eg makeSession, which it seems I haven't discussed), but this approach seemed simplest.

    Site.sessions is ostensibly public - you can tell because none of the names begin with an underscore - so you should probably feel okay using it directly if it's helpful. Of course, it has no documentation, so you might want to contribute some. The more well defined it is, the less likely someone will change it incompatibly in the future. :)

    As far as having logouts actually happen, you don't need to do anything special. Sessions will expire. And in a real application, I think it's more usual that login would happen at a URL solely for that purpose (eg /login) and would be protected by certain checks (is the user logged in already, for example) so the issue of making sure `notifyOnExpire` only gets set up once should be less important.

    ReplyDelete