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.