UNPKG

pixl-server-storage

Version:

A key/value/list storage component for the pixl-server framework.

1,086 lines (897 loc) 31.4 kB
// PixlServer Storage System // Copyright (c) 2015 - 2018 Joseph Huckaby // Released under the MIT License var util = require("util"); var async = require('async'); var unidecode = require('unidecode'); var Class = require("pixl-class"); var Tools = require("pixl-tools"); var Perf = require("pixl-perf"); var Component = require("pixl-server/component"); var List = require("./list.js"); var Hash = require("./hash.js"); var Indexer = require("./indexer.js"); var Transaction = require("./transaction.js"); module.exports = Class.create({ __name: 'Storage', __parent: Component, __mixins: [ List, Hash, Indexer, Transaction ], version: require('./package.json').version, defaultConfig: { list_page_size: 50, hash_page_size: 50, concurrency: 1, maintenance: 0, log_event_types: { all:0, get:0, put:0, head:0, delete:0, expire_set:0, perf_sec:0, perf_min:0, commit:0, index:0, unindex:0, search:0, sort:0, maint:1 }, max_recent_events: 0, cache_key_match: '', expiration_updates: false, lower_case_keys: true, queue_timeout: 30000 }, locks: null, cache: null, cacheKeyRegex: null, started: false, customRecordTypes: null, earlyStart: function() { // check for early transaction recovery if (!this.config.get('transactions')) return true; // transactions are enabled return this.transEarlyStart(); }, startup: function(callback) { // setup storage plugin var self = this; this.logDebug(5, "Setting up storage system v" + this.version); // advisory locking system (in RAM, single process only) this.locks = {}; // ram cache for certain keys, configurable this.cache = {}; this.cacheKeyRegEx = null; // cache some config values, and listen for config refresh this.prepConfig(); this.config.on('reload', this.prepConfig.bind(this) ); // dynamically load storage engine based on config var StorageEngine = require( this.config.get('engine_path') || ("./engines/" + this.config.get('engine') + ".js") ); this.engine = new StorageEngine(); this.engine.storage = this; this.engine.init( this.server, this.config.getSub( this.config.get('engine') ) ); // queue for setting expirations and custom engine ops this.queue = async.queue( this.dequeue.bind(this), this.concurrency ); // setup perf tracking system this.perf = new Perf(); this.perf.minMax = true; this.minutePerf = new Perf(); this.minutePerf.minMax = true; this.lastSecondMetrics = {}; this.lastMinuteMetrics = {}; this.recentEvents = {}; // allow others to register custom record types for maint this.customRecordTypes = {}; // bind to server tick, so we can aggregate perf metrics this.server.on('tick', this.tick.bind(this)); this.server.on('minute', this.tickMinute.bind(this)); // setup daily maintenance, if configured if (this.config.get('maintenance')) { // e.g. "day", "04:00", etc. this.server.on(this.config.get('maintenance'), function() { self.runMaintenance(); }); } // allow engine to startup as well this.engine.startup( function(err) { if (err) return callback(err); // set started flag, as transactions may need to recover from a crash self.started = true; // finally, init transaction system self.initTransactions( function(err) { // all done callback(err); } ); // initTransactions } ); // engine.startup }, prepConfig: function() { // save some config values this.listItemsPerPage = this.config.get('list_page_size'); this.hashItemsPerPage = this.config.get('hash_page_size'); this.concurrency = this.config.get('concurrency'); this.logEventTypes = this.config.get('log_event_types'); this.maxRecentEvents = this.config.get('max_recent_events'); this.expHash = this.config.get('expiration_updates'); this.lowerKeys = this.config.get('lower_case_keys'); this.queueTimeout = this.config.get('queue_timeout'); this.cacheKeyRegex = null; if (this.config.get('cache_key_match')) { this.cacheKeyRegex = new RegExp( this.config.get('cache_key_match') ); } }, normalizeKey: function(key) { // downconvert unicode, lower-case, alphanum-dash-dot-slash only, strip leading and trailing slashes key = '' + key; if (this.lowerKeys) key = key.toLowerCase(); return unidecode(key).replace(/[^\w\-\.\/]+/g, '').replace(/\/+/g, '/').replace(/^\//, '').replace(/\/$/, ''); }, isBinaryKey: function(key) { // binary keys have a built-in file extension, JSON keys do not return !!key.match(/\.\w+$/); }, addRecordType: function(type, handlers) { // add custom record type handler (for maint) // handlers: { delete: function } this.customRecordTypes[type] = handlers; }, put: function(key, value, callback) { // store key+value pair var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); // sanity checks if (!value) return callback( new Error("Record value cannot be false.") ); var isBuffer = Buffer.isBuffer(value); if (isBuffer && !this.isBinaryKey(key)) { return callback( new Error("Buffer values are only allowed with keys containing file extensions, e.g. " + key + ".bin") ); } else if (!isBuffer && this.isBinaryKey(key)) { return callback( new Error("You must pass a Buffer object as the value when using keys containing file extensions.") ); } // ram cache if (this.cacheKeyRegex && key.match(this.cacheKeyRegex)) { this.cache[key] = value; } // invoke engine and track perf var pf = this.perf.begin('put'); this.engine.put( key, value, function(err) { // put complete var elapsed = pf.end(); if (!err) self.logTransaction('put', key, { elapsed_ms: elapsed }); callback(err); if (!err) self.emit('put', key, value); } ); }, putStream: function(key, stream, callback) { // store key+stream var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); if (!this.isBinaryKey(key)) { return callback( new Error("Stream values are only allowed with keys containing file extensions, e.g. " + key + ".bin") ); } // sanity checks if (!stream || !stream.pipe) return callback( new Error("Not a valid stream.") ); // invoke engine and track perf var pf = this.perf.begin('put'); this.engine.putStream( key, stream, function(err) { // put complete var elapsed = pf.end(); if (!err) self.logTransaction('put', key, { elapsed_ms: elapsed }); callback(err); if (!err) self.emit('putStream', key); } ); }, putStreamCustom: function(key, stream, opts, callback) { // store key+stream with engine-specific opts var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); if (!this.isBinaryKey(key)) { return callback( new Error("Stream values are only allowed with keys containing file extensions, e.g. " + key + ".bin") ); } // sanity checks if (!stream || !stream.pipe) return callback( new Error("Not a valid stream.") ); // invoke engine and track perf var pf = this.perf.begin('put'); this.engine.putStreamCustom( key, stream, opts, function(err) { // put complete var elapsed = pf.end(); if (!err) self.logTransaction('put', key, { elapsed_ms: elapsed }); callback(err); if (!err) self.emit('putStream', key); } ); }, putMulti: function(records, callback) { // put multiple records at once, given object of keys and values var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); // if engine provides its own putMulti, call that directly if (("putMulti" in this.engine) && !this.currentTransactionPath) { return this.engine.putMulti(records, callback); } async.eachLimit(Object.keys(records), this.concurrency, function(key, callback) { // iterator for each key self.put(key, records[key], function(err) { callback(err); } ); }, function(err) { // all keys stored callback(err); } ); }, head: function(key, callback) { // fetch metadata given key: { mod, len } var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); // invoke engine and track perf var pf = this.perf.begin('head'); this.engine.head( key, function(err, data) { // head complete var elapsed = pf.end(); if (!err) self.logTransaction('head', key, { elapsed_ms: elapsed }); callback(err, data); if (!err) self.emit('head', key, data); } ); }, headMulti: function(keys, callback) { // head multiple records at once, given array of keys // callback is provided an array of values in matching order to keys var self = this; var records = {}; if (!this.started) return callback( new Error("Storage has not completed startup.") ); // if engine provides its own headMulti, call that directly if (("headMulti" in this.engine) && !this.currentTransactionPath) { return this.engine.headMulti(keys, callback); } async.eachLimit(keys, this.concurrency, function(key, callback) { // iterator for each key self.head(key, function(err, data) { if (err) callback(err); records[key] = data; callback(); } ); }, function(err) { if (err) return callback(err); // sort records into array of values ordered by keys var values = []; for (var idx = 0, len = keys.length; idx < len; idx++) { values.push( records[keys[idx]] ); } callback(null, values); } ); }, get: function(key, callback) { // fetch value given key var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); var cacheable = !!(this.cacheKeyRegex && key.match(this.cacheKeyRegex)); // ram cache if (cacheable && (key in this.cache)) { return process.nextTick( callback, null, this.cache[key] ); } // invoke engine and track perf var pf = this.perf.begin('get'); this.engine.get( key, function(err, value, info) { // get complete var elapsed = pf.end(); if (err) return callback(err); // ram cache if (cacheable) { self.cache[key] = value; } self.logTransaction('get', key, { elapsed_ms: elapsed }); callback(null, value, info); if (!err) self.emit('get', key, value, info); } ); }, getBuffer: function(key, callback) { // fetch buffer given key var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); // invoke engine and track perf var pf = this.perf.begin('get'); this.engine.getBuffer( key, function(err, value, info) { // get complete var elapsed = pf.end(); if (err) return callback(err); self.logTransaction('get', key, { elapsed_ms: elapsed }); callback(null, value, info); if (!err) self.emit('get', key, value, info); } ); }, getStream: function(key, callback) { // fetch value via stream pipe var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); if (!this.isBinaryKey(key)) { return callback( new Error("Stream values are only allowed with keys containing file extensions, e.g. " + key + ".bin") ); } this.engine.getStream( key, callback ); }, getStreamRange: function(key, start, end, callback) { // fetch value via stream pipe and range // start and end are both inclusive var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); if (!this.isBinaryKey(key)) { return callback( new Error("Stream values are only allowed with keys containing file extensions, e.g. " + key + ".bin") ); } this.engine.getStreamRange( key, start, end, callback ); }, getMulti: function(keys, callback) { // fetch multiple records at once, given array of keys // callback is provided an array of values in matching order to keys var self = this; var records = {}; if (!this.started) return callback( new Error("Storage has not completed startup.") ); // if engine provides its own getMulti, call that directly if (("getMulti" in this.engine) && !this.currentTransactionPath) { return this.engine.getMulti(keys, callback); } async.eachLimit(keys, this.concurrency, function(key, callback) { // iterator for each key self.get(key, function(err, data) { if (err) return callback(err); records[key] = data; callback(); } ); }, function(err) { if (err) return callback(err); // sort records into array of values ordered by keys var values = []; for (var idx = 0, len = keys.length; idx < len; idx++) { values.push( records[keys[idx]] ); } callback(null, values); } ); }, delete: function(key, callback) { // delete record given key var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); key = this.normalizeKey( key ); // ram cache if (this.cacheKeyRegex && key.match(this.cacheKeyRegex) && (key in this.cache)) { delete this.cache[key]; } // invoke engine and track perf var pf = this.perf.begin('delete'); this.engine.delete( key, function(err) { // delete complete var elapsed = pf.end(); if (!err) self.logTransaction('delete', key, { elapsed_ms: elapsed }); callback(err); if (!err) self.emit('delete', key); } ); }, deleteMulti: function(keys, callback) { // delete multiple records at once, given array of keys var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); // if engine provides its own deleteMulti, call that directly if (("deleteMulti" in this.engine) && !this.currentTransactionPath) { return this.engine.deleteMulti(keys, callback); } async.eachLimit(keys, this.concurrency, function(key, callback) { // iterator for each key self.delete(key, function(err) { callback(err); } ); }, function(err) { // all keys deleted callback(err); } ); }, copy: function(old_key, new_key, callback) { // copy record to new key var self = this; this.logDebug(9, "Copying record: " + old_key + " to " + new_key); // load old key this.get(old_key, function(err, data) { if (err) return callback(err, null); // save new key self.put(new_key, data, callback); } ); }, rename: function(old_key, new_key, callback) { // rename record (copy + delete old) var self = this; this.logDebug(9, "Renaming record: " + old_key + " to " + new_key); this.copy( old_key, new_key, function(err, data) { // copied, now delete old self.delete( old_key, callback ); } ); }, expire: function(key, expiration, force) { // set expiration date on key, normalize to midnight var dargs = Tools.getDateArgs( Tools.normalizeTime( expiration, { hour:0, min:0, sec:0 } ) ); var dnow = Tools.getDateArgs( new Date() ); if (!force && ((dargs.epoch <= dnow.epoch) || (dargs.yyyy_mm_dd == dnow.yyyy_mm_dd))) { // date is in past, move to tomorrow, to avoid race condition with maintenance() // this trick guarantees tomorrow midnight regardless of daylight savings time dargs = Tools.getDateArgs( Tools.normalizeTime( Tools.normalizeTime( dnow.epoch, { hour:12, min:0, sec:0 } ) + 86400, { hour:0, min:0, sec:0 } ) ); } this.logDebug(9, "Setting expiration on: " + key + " to " + dargs.yyyy_mm_dd); this.enqueue({ action: 'expire_set', key: key, expiration: dargs.epoch }); this.emit('expire', key, dargs.epoch); }, enqueue: function(task) { // enqueue task for execution soon if (!this.started) throw new Error("Storage has not completed startup."); if (typeof(task) == 'function') { var func = task; task = { action: 'custom', handler: func }; } task._id = Tools.generateShortID(); this.logDebug(9, "Enqueuing async task: " + task._id + ": " + (task.label || task.action), this.debugLevel(10) ? task : null ); this.queue.push( task ); }, dequeue: function(task, callback) { // run task and fire callback var self = this; this.logDebug(9, "Running async task: " + task._id + ": " + (task.label || task.action), this.debugLevel(10) ? task : null ); // optional timeout for queue item execution var timer = this.queueTimeout ? setTimeout( function() { self.logError('queue', "Async task timed out: " + task._id + ": " + (task.label || task.action), { ms: self.queueTimeout }); if (callback) callback(); callback = null; timer = null; }, this.queueTimeout ) : null; switch (task.action) { case 'expire_set': // set expiration on record var dargs = Tools.getDateArgs( task.expiration ); var cleanup_list_path = '_cleanup/' + dargs.yyyy + '/' + dargs.mm + '/' + dargs.dd; var cleanup_hash_path = '_cleanup/expires'; this.listPush( cleanup_list_path, { key: task.key }, { page_size: 1000 }, function(err, data) { // should never fail, but who knows if (err) self.logError('cleanup', "Failed to push cleanup list: " + cleanup_list_path + ": " + err); if (self.expHash) { self.hashPut( cleanup_hash_path, task.key, { expires: task.expiration }, { page_size: 1000 }, function(err) { // should never fail, but who knows if (err) self.logError('cleanup', "Failed to put cleanup hash: " + cleanup_hash_path + ": " + err); self.logTransaction('expire_set', task.key, { epoch: dargs.epoch, yyyy_mm_dd: dargs.yyyy_mm_dd, list_path: cleanup_list_path }); if (timer) clearTimeout(timer); timer = null; if (callback) callback(); callback = null; } ); // hashPut } // expHash else { self.logTransaction('expire_set', task.key, { epoch: dargs.epoch, yyyy_mm_dd: dargs.yyyy_mm_dd, list_path: cleanup_list_path }); if (timer) clearTimeout(timer); timer = null; if (callback) callback(); callback = null; } } ); // listPush break; // expire_set case 'custom': // custom handler task.handler( task, function(err) { if (err) self.logError('storage', "Failed to dequeue custom task: " + err); if (timer) clearTimeout(timer); timer = null; if (callback) callback(); callback = null; } ); break; // custom } // switch action }, runMaintenance: function(date, callback) { // run daily maintenance (delete expired keys) var self = this; var dargs = Tools.getDateArgs( date || (new Date()) ); var cleanup_list_path = '_cleanup/' + dargs.yyyy + '/' + dargs.mm + '/' + dargs.dd; var cleanup_hash_path = '_cleanup/expires'; var stats = { time_start: Tools.timeNow(), num_deleted: 0, num_skipped: 0, num_errors: 0 }; this.logDebug(3, "Running daily maintenance", cleanup_list_path); var deleteExpiredRecord = function(key, callback) { // delete single expired record of any type var finishDelete = function(err) { // log errors here (some records may already be deleted, which is fine) if (err) stats.num_errors++; // also delete metadata (expires epoch) if (self.expHash) { self.hashDelete( cleanup_hash_path, key, function(herr) { if (!err) stats.num_deleted++; callback(); } ); } else { callback(); } }; if (self.isBinaryKey(key)) { // straight up delete for binary records self.delete( key, finishDelete ); } else { // get JSON record to determine type self.get( key, function(err, data) { if (!data) data = {}; if (data.type && (data.type == 'list')) { self.listDelete( key, true, finishDelete ); } else if (data.type && (data.type == 'hash')) { self.hashDeleteAll( key, true, finishDelete ); } else if (data.type && self.customRecordTypes[data.type] && self.customRecordTypes[data.type].delete) { self.logDebug(6, "Invoking custom record delete handler for type: " + data.type + ": " + key); var func = self.customRecordTypes[data.type].delete; func( key, data, finishDelete ); } else { self.delete( key, finishDelete ); } } ); // get } }; // deleteExpiredRecord var doEngineMaint = function() { // allow engine to run maint as well self.engine.runMaintenance( function() { stats.elapsed_sec = Tools.shortFloat( Tools.timeNow() - stats.time_start ); self.logDebug(3, "Daily maintenance complete"); self.logTransaction('maint', cleanup_list_path, stats); if (callback) callback(); } ); }; // finish this.listEach( cleanup_list_path, function(item, item_idx, callback) { // delete item if still expired var key = item.key; // see if expiration date is still overdue if (self.expHash) { self.hashGet( cleanup_hash_path, key, function(err, data) { if (data && data.expires) { var eargs = Tools.getDateArgs( data.expires ); if ((eargs.epoch <= dargs.epoch) || (eargs.yyyy_mm_dd == dargs.yyyy_mm_dd)) { // still expired, kill it deleteExpiredRecord(key, callback); } else { // oops, expiration changed, skip stats.num_skipped++; self.logDebug(9, "Expiration on record " + key + " has changed to " + eargs.yyyy_mm_dd + ", skipping delete"); callback(); } } else { // no expiration date, just delete it deleteExpiredRecord(key, callback); } } ); // hashGet } // expHash else { deleteExpiredRecord(key, callback); } }, function(err) { // list iteration complete if (err) { self.logDebug(10, "Failed to load list, skipping maintenance (probably harmless)", cleanup_list_path); doEngineMaint(); } else { // no error, delete list self.listDelete( cleanup_list_path, true, function(err) { if (err) { self.logError('maint', "Failed to delete cleanup list: " + cleanup_list_path + ": " + err); } doEngineMaint(); } ); // listDelete } // succes } // list complete ); // listEach }, lock: function(key, wait, callback) { // lock key in exclusive mode, possibly wait until acquired if (!this.started) return callback( new Error("Storage has not completed startup.") ); if (key.match(/^(\w*\|+)(.+)$/)) key = RegExp.$1 + this.normalizeKey(RegExp.$2); else key = this.normalizeKey(key); this.logDebug(9, "Requesting lock: " + key); if (this.locks[key]) { var lock = this.locks[key]; if (wait) { lock.clients.push(callback); this.logDebug(9, "Key is already locked: " + key + ", waiting for unlock (" + lock.clients.length + " clients waiting)"); } else { this.logDebug(9, "Key is already locked: " + key); callback( new Error("Key is locked"), lock ); } } else { this.logDebug(9, "Locked key: " + key); var lock = { type: 'ex', clients: [] }; this.locks[key] = lock; callback(null, lock); } }, unlock: function(key) { // release lock on key if (!this.started) throw new Error("Storage has not completed startup."); if (key.match(/^(\w*\|+)(.+)$/)) key = RegExp.$1 + this.normalizeKey(RegExp.$2); else key = this.normalizeKey(key); if (this.locks[key]) { var lock = this.locks[key]; if (lock.type != 'ex') { this.logError('lock', "Lock is incorrect type (expected exclusive): " + key); return; } this.logDebug(9, "Unlocking key: " + key + " (" + lock.clients.length + " clients waiting)"); var callback = lock.clients.shift(); if (callback) { this.logDebug(9, "Locking key: " + key); callback(null, lock); } else delete this.locks[key]; } }, shareLock: function(key, wait, callback) { // lock key in shared (read-only) mode, possibly wait until acquired var self = this; if (!this.started) return callback( new Error("Storage has not completed startup.") ); if (key.match(/^(\w*\|+)(.+)$/)) key = RegExp.$1 + this.normalizeKey(RegExp.$2); else key = this.normalizeKey(key); this.logDebug(9, "Requesting shared lock: " + key); if (this.locks[key]) { var lock = this.locks[key]; if ((lock.type == 'sh') && !lock.clients.length) { // lock is already shared and no exclusive clients are waiting, so join the party lock.readers++; this.logDebug(9, "Joined shared lock: " + key, { readers: lock.readers }); callback(null, lock); } else { // exclusive lock (or shared lock with exclusive clients waiting), so we must wait if (!wait) return callback( new Error("Key is locked"), lock ); var func = function(err, lock) { if (err) return callback(err); // acquired lock, convert to shared if (lock.type == 'ex') { self.logDebug(9, "Locked key in shared mode: " + key); lock.type = 'sh'; lock.readers = 1; callback(null, lock); // look for more pending shared readers while (lock.clients[0] && lock.clients[0].__pixl_share_client) { var client = lock.clients.shift(); lock.readers++; self.logDebug(9, "Joined shared lock: " + key, { readers: lock.readers }); client(null, lock); } } else { // lock already shared, and we've been joined to it callback(null, lock); } }; // got lock // add special flag so we know client wants to be shared func.__pixl_share_client = 1; // wait for exclusive lock (which we will convert to shared) this.lock(key, true, func); } } else { this.logDebug(9, "Locked key in shared mode: " + key); var lock = { type: 'sh', clients: [], readers: 1 }; this.locks[key] = lock; callback(null, lock); } }, shareUnlock: function(key) { // release lock on shared key if (!this.started) throw new Error("Storage has not completed startup."); if (key.match(/^(\w*\|+)(.+)$/)) key = RegExp.$1 + this.normalizeKey(RegExp.$2); else key = this.normalizeKey(key); if (this.locks[key]) { var lock = this.locks[key]; if (lock.type != 'sh') { this.logError('lock', "Lock is incorrect type (expected shared): " + key); return; } if (lock.readers > 0) { lock.readers--; this.logDebug(9, "Removing reader from shared lock: " + key, { readers: lock.readers }); if (lock.readers > 0) return; } // all readers gone, so treat as exclusive and fully unlock key lock.type = 'ex'; this.unlock(key); } }, waitForQueueDrain: function(callback) { // wait for queue to finish all pending tasks if (this.queue.idle()) callback(); else { this.logDebug(3, "Waiting for queue to finish " + this.queue.running() + " active and " + this.queue.length() + " pending tasks"); this.queue.drain = callback; } }, waitForAllLocks: function(callback) { // wait for all locks to release before proceeding var self = this; var num_locks = Tools.numKeys(this.locks); if (num_locks) { this.logDebug(3, "Waiting for " + num_locks + " locks to be released", Object.keys(this.locks)); async.whilst( function () { return (Tools.numKeys(self.locks) > 0); }, function (callback) { setTimeout( function() { callback(); }, 250 ); }, function() { // all locks released self.logDebug(9, "All locks released."); callback(); } ); // whilst } else callback(); }, logTransaction: function(type, key, data) { // proxy request to system logger with correct component if (this.maxRecentEvents) { if (!this.recentEvents[type]) this.recentEvents[type] = []; this.recentEvents[type].push({ date: Tools.timeNow(), type: type, key: key, data: data }); if (this.recentEvents[type].length > this.maxRecentEvents) { this.recentEvents[type].shift(); } } if (this.logEventTypes[type] || this.logEventTypes['all']) { this.logger.set( 'component', this.__name ); this.logger.transaction( type, key, data ); } }, tick: function() { // called every second by pixl-server // rotate and log second perf metrics var metrics = this.lastSecondMetrics = this.perf.getMinMaxMetrics(); if (Tools.numKeys(metrics) && (this.logEventTypes.perf_sec || this.logEventTypes.all)) { this.logger.print({ component: this.__name, category: 'perf', code: 'second', msg: "Last Second Performance Metrics", data: metrics }); if (this.engine.cache) { this.logger.print({ component: this.__name, category: 'cache', code: 'second', msg: "Last Second Cache Stats", data: this.engine.cache.getStats() }); } } // import perf into minutePerf this.minutePerf.import( this.perf ); // and reset second perf this.perf.reset(); }, tickMinute: function() { // called every minute by pixl-server // rotate and log minute perf metrics var metrics = this.lastMinuteMetrics = this.minutePerf.getMinMaxMetrics(); if (Tools.numKeys(metrics) && (this.logEventTypes.perf_min || this.logEventTypes.all)) { this.logger.print({ component: this.__name, category: 'perf', code: 'minute', msg: "Last Minute Performance Metrics", data: metrics }); if (this.engine.cache) { this.logger.print({ component: this.__name, category: 'cache', code: 'minute', msg: "Last Minute Cache Stats", data: this.engine.cache.getStats() }); } } // and reset minute perf this.minutePerf.reset(); }, getStats: function() { // get perf and other misc stats var stats = { version: this.version, engine: this.engine.__name, concurrency: this.concurrency, transactions: !!this.transactions, last_second: this.lastSecondMetrics, last_minute: this.lastMinuteMetrics, recent_events: this.recentEvents, queue: { active: this.queue.running(), pending: this.queue.length() }, locks: {} }; // locks have actual callback functions, so convert to JSON-friendly for (var key in this.locks) { var lock = this.locks[key]; if (lock.type == 'ex') { stats.locks[key] = { type: 'exclusive', clients: lock.clients.length + 1 }; } else if (lock.type == 'sh') { stats.locks[key] = { type: 'shared', readers: lock.readers }; } } return stats; }, shutdown: function(callback) { // shutdown storage var self = this; this.logDebug(2, "Shutting down storage system"); this.waitForQueueDrain( function() { // queue drained, now wait for locks self.waitForAllLocks( function() { // all locks released, now shutdown engine if (self.engine) self.engine.shutdown(callback); else callback(); } ); // waitForLocks } ); // waitForQueueDrain } });