Sessions in Snap
Side Note: You can try the example application online at http://zelda.designacourse.com:8000/ if I happen to have it online. It’s a frivolous little game in which the web app tries to learn about animals. It’s all in a session with no persistent storage, though, so don’t spend too many hours teaching it about different animals.
I deliberately used a silly name, because this is an early attempt at adding sessions, and I don’t want to steal any important naming real estate that could be used later once some consensus builds about the right way to do this.
The motivation for releasing this package was the new extensions system in Snap 0.3. In a nutshell, extensions give Snap a nice common way to handle various added pieces of functionality that need their own internal state. Although the API is still experimental, it’s a very useful organizational tool.
The source code in the example package (linked above) is a good way to see what an application using mysnapsession might look like, but here’s the high-level overview.
Part 1: The MonadSession type class
Snap extensions all have a basic interface specified with a type class. For example, the built-in Heist extension comes with a type class called MonadHeist, and the example timer extension comes with MonadTimer. The fundamental type class for session support is called MonadSession. It’s a fairly simple type class, supporting two operations:
- getSessionObject: Retrieves an object representing this session
- putSessionObject: Replaces the object representing this session
You get to choose your own session object type, but not that this is a little different from the session API for every other programming language out there: there is one object representing the session. If you want name/value pairs, then your session object should probably be a Map from the Data.Map module.
Why didn’t I offer an interface that stores name/value pairs, a la every other web framework in existence? It would have been possible; in Haskell, that would just be a Map String Dynamic, using the Data.Dynamic package. The reason I didn’t do so is that I’m not convinced this is always the right way to go. I’ve done troubleshooting with a lot of other web applications in the past where strange behavior comes from leftover values in the session map, and I think it’s very possible to be more disciplined about the possible states of a session if you’re given a choice.
That’s not to say you can’t use a Map String Dynamic yourself: go ahead and do so. Write convenience functions for it. It’ll work fine. I just didn’t want to unnecessarily hard-wire the choice into the infrastructure.
Part 2: Memory-backed session implementation
The implementation I provide for sessions is memory-backed. This has both advantages and disadvantages.
- This is the simplest kind of sessions to implement: you don’t need a persistence layer.
- This is the only kind of sessions that can store values of arbitrary types. You can store first-class functions and such in the session, no problem.
- Because the session object exists in RAM on only one computer, it is fragile. If you restart the web application, it goes away. Unfortunately, it looks like this makes the development loader unusable; your sessions are wiped out on each request.
- If you need to load balance the application across multiple systems to scale it up, then you need some way to ensure that all requests from a session end up at the same node. This is called “sticky sessions”, and it’s supported, for example, in Apache’s mod_proxy. At some point, I should look more closely into how to set that up.
Aside from that, the memory-backed implementation works pretty much as you’d expect. It spawns a reaper thread that runs through the session list and discards old sessions after a user-configurable session timeout. Session keys are kept in cookies. (URL encoded session keys could be added as a feature pretty easily, but I haven’t done so.)
The MonadSession type class should be suitable for building sessions backed by databases or client-side cookies or other mechanisms as well. There would certainly be a good reason to do so. The memory-backed option was just a simple thing to do, which has some good use cases — including ones I’m hoping to use Snap for soon.
Part 3: Continuation-based programming
This is the bit of the API that’s demonstrated by the example application. It’s built on the previous two parts.
This part of the library has a long history. It originated as a post by Chris Eidhof to the HappStack mailing list. I cleaned up, simplified, and reorganized that code, and packed it up as the happstack-dlg package. (I regret, in hindsight, using so bold a name; this is certainly not an official part of the HappStack project.) Among other changes, I removed Chris’s error handling stuff… a decision I stick by, and have continued through to the present day. Error handling can, after all, be built on top of the simpler core.
Now that I’m working with Snap, and with this new opportunity, I’ve ported the code and included it as the Snap.Extension.Dialogues module in this package. I did drop the scaffolding and formlets support that existed in the happstack-dlg module. The formlet stuff was dropped mainly because formlets seems to be getting deprecated, and digestive-functors-blaze is still on my future reading list. The “scaffolding”, on the other hand, was just too ugly for me. I’d like to redo it properly using Heist for a customizable look and feel in the future, but only after some thought.
That’s it. Enjoy!