API Docs for: 0.8.0
Show:

File: include/service/memory_entity_service.js

/*
    Copyright (C) 2016  PencilBlue, LLC

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';

//dependencies
var util = require('../util.js');

module.exports = function MemoryEntityServiceModule(pb) {

    /**
     * Memory storage service
     *
     * @module Services
     * @submodule Storage
     * @class MemoryEntityService
     * @constructor
     * @param {Object} options
     * @param {String} options.objType
     * @param {String} options.keyField
     * @param {String} [options.valueField=null]
     * @param {String} [options.site=GLOBAL_SITE]
     * @param {String} [options.onlyThisSite=false]
     * @param {Integer} [options.timeout=0] The number of seconds that a value will remain in cache
     * before expiry.
     */
    function MemoryEntityService(options){
        this.type       = TYPE;
        this.objType    = options.objType;
        this.keyField   = options.keyField;
        this.valueField = options.valueField ? options.valueField : null;
        this.timeout    = options.timeout || 0;
        this.site       = options.site || GLOBAL_SITE;
        this.onlyThisSite = options.onlyThisSite ? true : false;

        //ensure we are cleaning up after ourselves
        if (REAPER_HANDLE === null) {
            MemoryEntityService.startReaper();
        }

        //ensure we can get change events.  If we're already registered the function will return false
        pb.CommandService.getInstance()
            .registerForType(MemoryEntityService.getOnChangeType(), MemoryEntityService.changeHandler);
    }

    /**
     * @private
     * @static
     * @property GLOBAL_SITE
     * @type {String}
     */
    var GLOBAL_SITE = pb.SiteService.GLOBAL_SITE;

    /**
     * @private
     * @static
     * @property TIMERS
     * @type {Object}
     */
    var TIMERS = {};

    /**
     * @private
     * @static
     * @property
     * @type {Object}
     */
    var STORAGE = {};

    /**
     * @private
     * @static
     * @property
     * @type {number}
     */
    var REAPER_HANDLE = null;

    /**
     * @private
     * @static
     * @readonly
     * @property DEFAULT_REAPER_INTERVAL
     * @type {number}
     */
    var DEFAULT_REAPER_INTERVAL = 30000;

    /**
     * The type string that describes the storage medium for the service
     * @private
     * @static
     * @readonly
     * @property TYPE
     * @type {String}
     */
    var TYPE = 'Memory';

    /**
     * Retrieve a value from memory
     *
     * @method get
     * @param  {String}   key
     * @param  {Function} cb  Callback function
     */
    MemoryEntityService.prototype.get = function(key, cb){
        var internalKey = MemoryEntityService.getKey(key, this.site, this.objType);
        var value = getSiteValue(this, internalKey);

        process.nextTick(function() { cb(null, value); });
    };

    /**
     * @private
     * @static
     * @method getSiteValue
     * @param self
     * @param internalKey
     * @return {*}
     */
    function getSiteValue(self, internalKey) {
        var rawVal = null;
        if (typeof STORAGE[internalKey] !== 'undefined') {
            rawVal = STORAGE[internalKey];
        }

        //value not found
        if (rawVal === null) {
            return null;
        }

        return getCorrectValueField(rawVal, self.valueField);
    }

    /**
     * @private
     * @static
     * @method getCorrectValueField
     * @param rawVal
     * @param valueField
     * @return {*}
     */
    function getCorrectValueField(rawVal, valueField) {
        var value = null;
        if (valueField === null) {
            value = rawVal;
        }
        else {
            value = rawVal[valueField];
        }
        return value;
    }

    /**
     * Set a value in memory.  Triggers a command to be sent to the cluster to
     * update the value
     * @method set
     * @param {String} key
     * @param {*} value
     * @param {Function} cb Callback function
     */
    MemoryEntityService.prototype.set = function(key, value, cb) {
        var self = this;
        this._set(key, value, function(err, result) {
            if (result) {
                self.onSet(key, value);
            }
            cb(err, result);
        });
    };

    /**
     * Sets a value
     * @private
     * @method _set
     * @param {String} key
     * @param {Object|String|Integer|Float|Boolean} value
     * @param {Function} cb
     */
    MemoryEntityService.prototype._set = function(key, value, cb) {
        var rawValue = null;
        var internalKey = MemoryEntityService.getKey(key, this.site, this.objType);
        if (STORAGE[internalKey]) {
            rawValue = STORAGE[internalKey];
            if (this.valueField === null) {
                rawValue = value;
            }
            else {
                rawValue[this.valueField] = value;
            }
        }
        else if (this.valueField === null){
            rawValue = value;
        }
        else{
            rawValue = {
                object_type: this.objType
            };
            rawValue[this.keyField]   = key;
            rawValue[this.valueField] = value;
        }
        STORAGE[internalKey] = rawValue;

        //check for existing timeout
        this.setKeyExpiration(key);
        cb(null, true);
    };

    /**
     * Called when when a value changes
     * @method onSet
     * @param {String} key,
     * @param {Object|String|Integer|Float|Boolean} value
     */
    MemoryEntityService.prototype.onSet = function(key, value) {
        var command = {
            key: key,
            value: value,
            site: this.site,
            objType: this.objType,
            onlyThisSite: this.onlyThisSite,
            keyField: this.keyField,
            valueField: this.valueField,
            timeout: this.timeout,
            ignoreme: true
        };
        pb.CommandService.getInstance()
            .sendCommandToAllGetResponses(MemoryEntityService.getOnChangeType(), command, util.cb);
    };

    /**
     * Sets a timeout to purge a key after the configured timeout has occurred.  If
     * a timeout has already been set it will be cleared and a new one will be
     * created.
     * @method setKeyExpiration
     * @param {String} key The key for the value to be cleared
     */
    MemoryEntityService.prototype.setKeyExpiration = function(key) {
        if (this.timeout <= 0) {
            return;
        }

        //check for existing timeout
        var internalKey = MemoryEntityService.getKey(key, this.site, this.objType);

        //now set the timeout if configured to do so
        TIMERS[internalKey] = Date.now() + this.timeout;
    };

    /**
     * Purge the key from memory
     *
     * @method purge
     * @param  {String}   key
     * @param  {Function} cb  Callback function
     */
    MemoryEntityService.prototype.purge = function(key, cb) {
        var internalKey = MemoryEntityService.getKey(key, this.site, this.objType);
        var exists = !!STORAGE[internalKey];
        if(exists) {
            delete STORAGE[internalKey];
            delete TIMERS[internalKey];
        }
        process.nextTick(function() { cb(null, exists); });
    };

    /**
     * Retrieves the internal key format for a given key
     * @static
     * @method getKey
     * @param {string} key
     * @param {string} site
     * @param {string} objType
     * @return {string}
     */
    MemoryEntityService.getKey = function(key, site, objType) {
        return key + '-' + site + '-' + objType;
    };

    /**
     * Retrieves the command type that is to be used to listen for changes to
     * key/value pairs within the registered instance
     * @static
     * @method
     * @return {String} The command type to be registered for
     */
    MemoryEntityService.getOnChangeType = function() {
        return TYPE + '-change';
    };

    /**
     * Creates a change handler that will update the value of a property when an
     * incomming command requests it.
     * @static
     * @method createChangeHandler
     */
    MemoryEntityService.changeHandler = function(command) {

        var memoryEntityService = new MemoryEntityService(command);
        memoryEntityService._set(command.key, command.value, util.cb);
    };

    /**
     * Searches for expired keys and removes them from memory
     * @static
     * @method reap
     * @return {number} The number of keys that were reaped
     */
    MemoryEntityService.reap = function() {
        var reaped = 0;
        var now = Date.now();
        var keys = Object.keys(TIMERS);
        keys.forEach(function(key) {
            if (TIMERS[key] <= now) {
                delete TIMERS[key];
                delete STORAGE[key];
                reaped++;
            }
        });
        pb.log.silly('MemoryEntityService: Scanned %s keys and reaped %s in %sms', keys.length, reaped, Date.now() - now);
        return reaped;
    };

    /**
     * Attempts to start the reaper that will remove expired keys from the in-memory cache
     * @static
     * @method startReaper
     * @return {Boolean} TRUE if reaper is started, FALSE if it is already started
     */
    MemoryEntityService.startReaper = function() {
        if (REAPER_HANDLE === null) {
            REAPER_HANDLE = setInterval(MemoryEntityService.reap, DEFAULT_REAPER_INTERVAL);

            pb.system.registerShutdownHook('MemoryEntityService', MemoryEntityService.dispose);

            pb.log.silly('MemoryEntityService: Reaper started');
            return true;
        }
        return false;
    };

    /**
     * Disposes of the storage and timers.  It also terminates the reaping of expired keys.
     * @static
     * @method dispose
     * @param {Function} cb
     */
    MemoryEntityService.dispose = function(cb) {

        //release data and timeout hashes
        STORAGE = {};
        TIMERS = {};

        //stop reaping, nothing to reap
        if (REAPER_HANDLE !== null) {
            clearInterval(REAPER_HANDLE);
        }

        //clean up by un registering the change handler to prevent memory leaks
        pb.CommandService.getInstance().unregisterForType(MemoryEntityService.getOnChangeType(), MemoryEntityService.changeHandler);

        cb(null, true);
    };

    return MemoryEntityService;
};