Last Updated: March 08, 2016
·
2.45K
· ju

Basic Caching Strategy Javascript Class

I recently was asked to cache some data using memcached in node.js. Everything was easy to setup and the caching was working great with the cluster of memcached servers I was developing on. And as part of the application's testing we force shutdown a single memcached instance in the cluster to see how the application adapts. Unfortunately, we were seeing unacceptably negative forward facing response times. I later found out that there was a bug in the memcached library we were using. That has since been fixed, but even after that bug had been resolved we were seeing some of the cached requests that had been forced closed hanging and waiting for a response. We determined that it was most likely due to the fact the TCP socket never receives the FIN message from the memcached server, so the socket hangs until the timeout is met (around 60 seconds).

Either way, we needed a strategy that would protect the client from experiencing high response times. In comes my BasicCachingStrategy class. It allows us to define a standard API for simple get/set caching with the enforcement of a timeout.

Code is documented and will probably do a better job at explaining what it does than I will. ;)

  • When overriding this class, you should always override the get and set prototype methods.
  • When using this class, call timeout_get and timeout_set. Doing so enforces a timeout on the get and set methods.
  • This class is AMD/RequireJS/NodeJS/Browser ready

https://gist.github.com/mrlannigan/6314235

'use strict';

/**
 * Basic Caching Strategy Class file
 * @author Julian Lannigan <julian@jlconsulting.co>
 * @since 22AUG2013
 */

(function () {
  /**
   * Timeout Error Class
   * @param {String} msg Timeout error message
   * @extends {Error}
   */
  function TimeoutError(msg) {
    Error.captureStackTrace && Error.captureStackTrace(this, this);
    this.message = msg || 'Error';
  }
  TimeoutError.super_ = Error;
  TimeoutError.prototype = Object.create(Error.prototype, {
    constructor: {value: TimeoutError, enumerable: false},
    name: {value: 'Timeout Error'}
  });

  /**
   * Basic Caching Strategy
   */
  function BasicCachingStrategy() {}

  /**
   * Default timeout length (ms)
   * @type {Number}
   */
  BasicCachingStrategy.prototype.timeout = 5000;

  /**
   * Extension of timeout property as a class property
   * @type {Number}
   */
  Object.defineProperty(BasicCachingStrategy, 'timeout', {
    configurable: false,
    enumerable: true,
    writable: true,
    value: BasicCachingStrategy.prototype.timeout
  });

  /**
   * Extension of parent class for error class to use if a timeout occurs
   * @type {Error}
   */
  Object.defineProperty(BasicCachingStrategy, 'TimeoutError', {
    configurable: false,
    enumerable: true,
    writable: true,
    value: TimeoutError
  });

  /**
   * Default `always miss` caching function (should always be overridden)
   * @param  {String}   key        Key for cache
   * @param  {Function} callback   Callback (err, cached?, cachedValue)
   * @return {BasicCachingStrategy}
   */
  BasicCachingStrategy.prototype.get = function (key, callback) {
    callback(null, false, null);
    return this;
  };

  /**
   * Default `always not cached` caching function (should always be overridden)
   * @param {String}   key         Key for cache
   * @param {Mixed}    value       Value to store
   * @param {Function} callback    Callback (err, cached?)
   * @return {BasicCachingStrategy}
   */
  BasicCachingStrategy.prototype.set = function (key, value, callback) {
    callback(null, false);
    return this;
  };

  /**
   * Wrapper method for `get` with the addition of a timeout
   * 
   * If you are writing a library to use this object, you should always
   * call the timeout version of the applicable function.  Override at
   * your own risk.
   * 
   * @param  {String}   key        Key for cache
   * @param  {Function} callback   Callback (err, cached?, cachedValue)
   * @return {BasicCachingStrategy}
   */
  BasicCachingStrategy.prototype.timeout_get = function (key, callback) {
    var self = this,
      timeout,
      called = false;

    timeout = setTimeout(function () {
      called = true;
      callback(new BasicCachingStrategy.TimeoutError('reached during get'), false, null);
    }, this.timeout);

    this.get(key, function () {
      clearTimeout(timeout);
      if (called) { return; }
      callback.apply(self, arguments);
    });

    return this;
  };

  /**
   * Wrapper method for `set` with the addition of a timeout
   *
   * If you are writing a library to use this object, you should always
   * call the timeout version of the applicable function.  Override at
   * your own risk.
   * 
   * @param {String}   key         Key for cache
   * @param {Mixed}    value       Value to store
   * @param {Function} callback    Callback (err, cached?)
   * @return {BasicCachingStrategy}
   */
  BasicCachingStrategy.prototype.timeout_set = function (key, value, callback) {
    var self = this,
      timeout,
      called = false;

    timeout = setTimeout(function () {
      called = true;
      callback(new BasicCachingStrategy.TimeoutError('reached during set'), false);
    }, this.timeout);

    this.set(key, value, function () {
      clearTimeout(timeout);
      if (called) { return; }
      callback.apply(self, arguments);
    });

    return this;
  };

  // AMD / RequireJS
  if (typeof define !== 'undefined' && define.amd) {
    define([], function () {
      return BasicCachingStrategy;
    });
  }
  // Node.js
  else if (typeof module !== 'undefined' && module.exports) {
    module.exports = BasicCachingStrategy;
  }
  // included directly via <script> tag
  else {
    this.BasicCachingStrategy = BasicCachingStrategy;
  }

})();