Production Ready Node: Caching

c

aching is often times the first line of defense against poor performing applications and very quickly becomes a critical and complex part of various parts of the stack. One of the more frustrating aspects of caching, is that there are so many very capable solutions available - Redis, memcached, memory etc. Even some databases or plain old `files` can be used to cache data in certain circumstances. Each of them having different strengths, weaknesses, uses cases and different APIs.

Cache ['kash] -n, noun; -v, verb

a component that stores data so future requests for that data can be served faster; the data stored in a cache might be the results of an earlier computation, or the duplicates of data stored elsewhere.

It is not uncommon to see multiple caches used in multiple places in complex applications. To minimize complexities of different implementations, we want to create a packages that exposes a singular abstract proxy API to for multiple caching backends to serve as the low level building block for more complex implementations. What we want to be able to do is something like this:

var cache = require('project-cache')

// default configured cache
cache.set('key1', 1, console.log);
cache.get('key1', 1, console.log);
cache('default').get('key1',console.log);

// specify configured named redis cache
cache('redis').set('rediskey', 2);

// use a cache designated for images
var img = fs.readFileSync('some/image.png').toString('base64')
cache('images').set('some/image.png', img )

// allow cache look up as property access
cache.images.set('some/image.png,img);

Cache Configuration

Of course, we are going to need a way specify and configure one or more cache backends. We want to allow for a general default cache, as well as as many named caches. So our config might look like this:

{
    "caches":{
        "default":{
            "backend":"redis",
            "location":{
                "host":"localhost",
                "port":3393
            }
        },
        "file":{
            "backend":"/full/path/to/file/cache/module"
            "location":"/var/cache/project.cache"
            "timeout":300000
        }
    }
}

Now that we have hashed out a general configuration structure, we can start defining a simple function that can look up a named cache backend module

// project-cache/index.js

var path = require('path')
var conf = require('project-conf')
var logger = require('project-log')
var caches = clone( conf.get('caches') || {} );
var Cache;

function getBackend( name ){
    var err; 
    if( caches.hasOwnProperty( name ) ){
        return caches[name];
    }
    err = new Error();
    err.message = 'No cache named ' + name + ' found';
    err.name = 'ImproperlyConfigured'
    throw err;
};

Cache = function Cache( cache ){
    return getBackend( cache );
};

Yep! Pretty simple. Now we want to populate our internal caches object by replacing each of the configuration objects with an instance of the matching backend using the configuration specified.

Cache Module

The Cache module api isn't overly complex. It really just needs to forward arguments to the methods of a backend. In addition to the typical get and set method you usually see in cache modules, I like to add methods for dealing with array values, like pop and push. I Tend to lean on redis in my personal projects for caching, and have found the ability to use arrays or sets to be really handy. While most other cache solutions don't support these operations out of the box, it is pretty easy to implement in the cache backend. So the module might look something like this:

// project-cache/index.js

Object
    .keys( caches )
    .forEach(function(key){
        // look for a default name, or default property
        if( key == 'default' || !!caches[key].default ){
            hasDefault = true;
            logger.info("setting %s cache backend as default", caches[key].backend );
        }
        var bkend = attempt(
            // try to require a locally defined backend 
            function(){
                return require( path.join( __dirname, 'backends', caches[ key ].backend ) );
            }

             // backend may be a full path
             // if local require failed, require the backend directly 
            ,function(){
                return require( caches[ key ].backend );
            }
        )
        if( !bkend ){
            logger.error('unable to locate cache backend %s', caches[ key ].backend );
        } else {
            logger.info('loading %s cache backend', caches[ key ].backend, caches[key] );

            // store an instance of the cache
            // passing the original config to the constructor
            caches[ key ] = new bkend( caches[ key ] );

            // define a dynamic property on the cache 
            Object.defineProperty(Cache, key, {
                get: function get( ){
                    return getBackend( key );
                }
            });
        }
    }
)

A simple loop over each of the keys in our caches object replacing each of the values with cache instances. Now all that is left is to define the public API on our cache. This part is really simple, all we need to do is look up the backend by name, or default if not specified and call the a method of the same name passing the arguments untouched.

// project-cache/index.js ( continued )

Object.defineProperties(Cache,{
    /**
     * Short cut to the get method of the default cache backend
     * @static
     * @param {...String} key The key(s) to fetch from the
     * @param {Function} callback callback function to be executed when the operation is complete
     **/
    get:{
        writable: false
        ,value:function get(){
            var bkend = getBackend('default');
            return bkend.get.apply( bkend, arguments );
        }
    }
    /**
     * Sets a value at the specified key
     * @static
     * @param {String} key Key to identify a value
     * @param {String|Number} value The value to set at the key
     * @param {Number} [timeout=Cache.timeout] a timeout override for a specific operation
     * @param {Function} callback A callback function to be executed when the operation is finished
     **/
    , set:{
        writable: false
        ,value:function set(){
            var bkend = getBackend('default');
            return bkend.set.apply( bkend, arguments );
        }
    }

    /**
     * Sets a value if, and only if the key doesn't not already exists
     * @static
     * @param {String} key Key to identify a value
     * @param {String|Number} value The value to set at the key
     * @param {Number} [timeout=Cache.timeout] a timeout override for a specific operation
     * @param {Function} callback A callback function to be executed when the operation is finished
     **/
    , add:{
        writable: false
        ,value:function add(){
            var bkend = getBackend('default');
            return bkend.add.apply( bkend, arguments );
        }
    }

    /**
     * Increment the value by one, setting 1 if not previously set
     * @static
     * @param {String} key The key to increment if it exists
     * @param {Function} callback A callback function to be executed when the operation is finished
     **/
    , incr:{
        writable: false
        ,value:function incr(){
            var bkend = getBackend('default');
            return bkend.incr.apply( bkend, arguments );
        }
    }

    /**
     * Decrements the value by 1, setting 0 if not previously set
     * @static
     * @param {String} key The key to decrement if it exists
     * @param {Function} callback A callback function to be executed when the operation is finished
     **/
    , decr:{
        writable: false
        ,value:function decr(){
            var bkend = getBackend('default');
            return bkend.decr.apply( bkend, arguments );
        }
    }

    /**
     * Push value on to array, creating an array if not set
     * @static
     * @param {String} key Key to identify a value
     * @param {String|Number} value The value to set at the key
     * @param {Number} [timeout=Cache.timeout] a timeout override for a specific operation
     * @param {Function} callback A callback function to be executed when the operation is finished
     **/
    ,push:{
        writable: false
        ,value:function push(){
            var bkend = getBackend('default');
            return bkend.push.apply( bkend, arguments );
        }
    }

    /**
     * pop value from an array
     * @static
     * @param {String} key Key to identify a value
     * @param {String|Number} value The value to set at the key
     * @param {Number} [timeout=Cache.timeout] a timeout override for a specific operation
     * @param {Function} callback A callback function to be executed when the operation is finished
     **/
    , pop:{
        writable: false
        ,value:function pop(){
            var bkend = getBackend('default');
            return bkend.pop.apply( bkend, arguments );
        }
    }

    /**
     * Clears all values from the cache
     **/
    , flush:{
        writable: false
        ,value:function flush(){
            var bkend = getBackend('default');
            return bkend.flush.apply( bkend, arguments );
        }
    }

    /**
     * Disconnects the cache backend
     **/
    , close:{
        writable: false
        ,value:function close(){
            var bkend = getBackend('default');
            return bkend.close.apply( bkend, arguments );
        }
    }

});

module.exports = Cache;

Dummy Backend

All that is really left to do, is to implement the set of caching backends for your project. In general, I like to implement 2 or 3 simple ones to smooth over development and testing. I will generally make a no-op, memory and one for what ever the primary caching solution might be ( redis, memcached, etc. ). The dummy implementation is pretty simple to do.

// project-cache/lib/backends/dummy.js

// empty function
function noop(){};

// Constructor
function Dummy( ){
    // it doesn't really do anything...
};

Dummy.prototype.add = function add( key, value, timeout, callback ){
    return callback && callback( null, value );
};

Dummy.prototype.get = function get(){
    var callback = typeof arguments[ arguments.length -1 ] == 'function' ?
    arguments[arguments.length -1 ] : noop;
    return callback && callback( null, null );
};

Dummy.prototype.set = function set( key, value, timeout, callback ){
    return callback && callback( null, value );
};

Dummy.prototype.incr = function incr( key, callback ){
    return callback && callback( null, 1 );
};

Dummy.prototype.dec = function decr( key, callback  ){
    return callback && callback( null, 0 );  	
};

Dummy.prototype.push = function push(key, value, timeout, callback ){
    return callback && callback( null, [ value ] );
};

Dummy.prototype.pop = function pop(key, callback ){
    return callback && callback( null, null ); 
};

Dummy.prototype.flush = function flush(callback){
    return callback && callback( null, null );
};

Dummy.prototype.close = function close( callback ){
    return callback && callback( null, true );
};

Now we have modular, highly configurable, multi-tenant caching module that we can re-use on multiple projects which can adopt any number of caching solutions to get the job done. With this simple module, we can tackle anything from caching api response, session data, to template fragments. Or anything else that comes our way.

modules oop production ready caching proxy node