Dancer::Session::Redis - Writing a session engine for Dancer 2
In order to continue presenting all the good things about Dancer 2, I wanted a good reason to show how the session handling is done. On the other hand, I didn't want to explain how the code is designed without something concrete to explain. So I came with the idea of writing a new session engine for Dancer 2, explaining all the steps here.
Moreover, after having searched a bit, it appears that there is no Redis backend for storing sessions so that's a perfect opportunity to write it! Let's go!
A word about Redis
I suppose you already know Redis, if you don't, well, you should! It's a blazing fast NoSQL key/value store. It can reach amazing performances like a rate of 100,000 requests per second. Using it as a session storage to make sure multiple interface servers can share the same information is very common.
Anything stored in Redis is serialized as a JSON string.
Session engines in Dancer 2
We've already seen that Dancer 2 is entirely written with Moo. So you won't
be surprised if I tell you that in Dancer 2, the engines are actually roles. So
if we want to write a session backend for Dancer 2, we need to consume the
Dancer::Core::Role::SessionFactory
role. Why not Session
instead of
SessionFactory
you ask? Well, it's the major design change (which I'm very
happy with) if you compare to how things are done in Dancer 1.
In Dancer 1, we have one object, a Dancer::Session::Something. That object will
contain the session itself (expiration time, ID, content) and the session
storage itself (for instance for YAML, you'll find here a session_dir
attribute).
It's probably not shocking at first sight, I presume because Dancer 1 still works like that and I never heard someone complaning about it, after almost 3 years.
But recently, I was facing an issue with sessions in Dancer 2 and I started thinking about how things are done, and I realized the design could be better by far. Indeed, it came to my mind that we have actually two very different things here: the session itself, and its storage backend.
There are two concepts: a Session
class, that contains generic information
like an expiration time, a content, and ID and that is agnostic of
how it is stored; and a SessionFactory class, which describes how to create,
update and delete a Session
object.
So in Dancer 2, a session backend consumes the role
Dancer::Core::Role::SessionFactory
.
Let's do that!
Dancer::Session::Redis
First thing to do is to create the class for our session engine, we'll name it,
abviously Dancer::Session::Redis
:
package Dancer::Session::Redis; use Moo; with 'Dancer::Core::Role::SessionFactory';
First, we want our backend to have a handle over a Redis connection. To do that,
we'll create an attribute redis
that will do all the boring bits for us,
lazily.
use JSON; use Redis; use Dancer::Core::Types; # brings helper for types has redis => ( is => 'rw', isa => InstanceOf['Redis'], lazy => 1, builder => '_build_redis', );
The lazy attribute is very interesting, it says to Moo that this attribute will be built (initialized) only when called the first time. It means that the connection to Redis won't be opened until necessary.
sub _build_redis { my ($self) = @_; Redis->new( server => $self->server, password => $self->password, encoding => undef, ); }
As you can see, we want to create two more attributes: server
and
password
. Dancer 2 will pass anything defined in the config to the engine
creation. So for instance, if we have:
# config.yml ... engines: session: Redis: server: foo.mydomain.com password: S3Cr3t
The server and password entries will be passed to the constructor of the Redis session engine, as expected. That's another sign of the proper design of Dancer 2, no more settings shared among all the packages, the Redis engine only knows about itself, it has absolutely no idea of what a Dancer config is.
has server => (is => 'ro', required => 1); has password => (is => 'ro');
The role requires that we implement _retrieve
, _flush
, _destroy
and
_sessions
.
The first one, _retrieve
is supposed to return a session object for a session
ID it's passed. In our case, it's pretty easy, our sessions are going to be
stored in Redis, the session ID will be the key, the session the value. So
retrieving is as easy as doing a get and decoding the JSON string returned:
sub _retrieve { my ($self, $session_id) = @_; my $json = $self->redis->get($session_id); my $hash = from_json( $json ); return bless $hash, 'Dancer::Core::Session'; }
Now we should implement the _flush
method, which is called by Dancer when the
session needs to be stored in the backend. That's actually a write to Redis. The
method receives a Dancer::Core::Session
object and is supposed to store it.
sub _flush { my ($self, $session) = @_; my $json = to_json( { %{ $session } } ); $self->redis->set($session->id, $json); }
Now, the _destroy
method that is supposed to remove a session from the
backend, the session ID is passed.
Easy one, we should delete the key from Redis.
sub _destroy { my ($self, $session_id) = @_; $self->redis->del($session_id); }
And finally, the last one, the _sessions
method which is supposed to list all
the session IDs currently stored in the backend. It's in our case as stupid as
listing all the keys that Redis has.
sub _sessions { my ($self) = @_; my @keys = $self->redis->keys('*'); return \@keys; }
Voilà. The session engine is ready. Let's play with it now.
Behind the session
keyword
It should be clear now that a session storage system is not the same thing as a
session itself. Let's look at how things work when you use the session
keyword in Dancer.
First of all, keep in mind that when Dancer 2 executes a route handler to
process a request, it creates a Dancer::Core::Context
object. That object is
designed to handle all the volatile information of the current request: the
environment, the request object, the newborn response object, the cookies...
This context is passed to all the components of Dancer that can play with it, to build the response. For instance, a before filter will receive that context object.
That where we'll find the session handle for the current client, in the context.
The beauty of it is that it's actually a lazy attribute, and its builder has
one thing to do: look if the client has a dancer.session
cookie, and if so,
try to retrieve
the session from the storage engine, with the value of the
cookie (the session ID).
has session => ( is => 'rw', isa => Session, lazy => 1, builder => '_build_session', ); sub _build_session { my ($self) = @_; my $session; # Find the session engine my $engine = $self->app->setting('session'); croak "No session engine defined, cannot use session." if ! defined $engine; # find the session cookie if any my $session_id; my $session_cookie = $self->cookie('dancer.session'); if (defined $session_cookie) { $session_id = $session_cookie->value; } # if we have a session cookie, try to retrieve the session if (defined $session_id) { eval { $session = $engine->retrieve(id => $session_id) }; croak "Fail to retreive session: $@" if $@ && $@ !~ /Unable to retrieve session/; } # create the session if none retrieved return $session ||= $engine->create(); }
So the very first time session
is called, the object is either retrieved
from the backend, or a new Dancer::Core::Session
is created, and stored in
the context.
Then, a before
filter makes sure a cookie dancer.session
is added to the
headers.
# Hook to add the session cookie in the headers, if a session is defined $self->add_hook(Dancer::Core::Hook->new( name => 'core.app.before_request', code => sub { my $context = shift; # make sure an engine is defined, if not, nothing to do my $engine = $self->setting('session'); return if ! defined $engine; # push the session in the headers $context->response->push_header('Set-Cookie', $context->session->cookie->to_header); } ));
At this time, the user's code comes into play, using the session
keyword,
which is actually that simple:
sub session { my ($self, $key, $value) = @_; my $session = $self->context->session; croak "No session available, a session engine needs to be set" if ! defined $session; # return the session object if no key return $session if @_ == 1; # read if a key is provided return $session->read($key) if @_ == 2; # write to the session $session->write($key => $value); }
And to conclude, an after
filter is set to call the flush
method of the
storage backend. Indeed, the write
calls you see above do not change the
sesssion in the storage system, they only alter the object itself. That's a
major improvement if you compare to Dancer 1, because as everything is mixed and
less well decoupled, in Dancer 1, a write to the session is also a flush to the
backend system.
Here, we know that we flush exactly once per request, when the response is ready.
# Hook to flush the session at the end of the request, this way, we're sure we # flush only once per request $self->add_hook( Dancer::Core::Hook->new( name => 'core.app.after_request', code => sub { # make sure an engine is defined, if not, nothing to do my $engine = $self->setting('session'); return if ! defined $engine; return if ! defined $self->context; $engine->flush(session => $self->context->session); }, ) );
Code for this article
The code of the session backend described here has been released on GitHub.
It's not released on CPAN yet because we'll wait for Dancer 2 to be there first, for obvious reasons.
Author
This article has been written by Alexis Sukrieh for the Perl Dancer Advent Calendar 2012.