├── .gitignore ├── LICENSE ├── README.md ├── cluster-node-cache.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.sublime-project 3 | *.sublime-workspace 4 | *.log 5 | coverage/ 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Lee Turner 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cluster-node-cache 2 | =========== 3 | [![NPM version](https://badge.fury.io/js/cluster-node-cache.png)](http://badge.fury.io/js/cluster-node-cache) 4 | 5 | [![NPM](https://nodei.co/npm/cluster-node-cache.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/cluster-node-cache/) 6 | 7 | # Please note: I am no longer actively maintaining or working on this 8 | 9 | # Simple and fast NodeJS internal caching that works in a clustered environment. 10 | 11 | This module is a wrapper for [node-cache](https://github.com/tcs-de/nodecache) that allows it to work in a 12 | clustered environment - without it, you would have an instance of node-cache per core. It's API and functionality 13 | is mostly similar. 14 | 15 | # Note 16 | This does not yet support the the 2.0 release of node-cache. 17 | 18 | # Install 19 | 20 | ``bash 21 | npm install cluster-node-cache 22 | `` 23 | # Examples: 24 | 25 | ## Initialize (INIT): 26 | 27 | ```js 28 | var cluster = require('cluster'); 29 | var myCache = require('cluster-node-cache')(cluster); 30 | ``` 31 | 32 | ### Options 33 | 34 | - `stdTTL`: *(default: `0`)* the standard ttl as number in seconds for every generated cache element. 35 | `0` = unlimited 36 | - `checkperiod`: *(default: `600`)* The period in seconds, as a number, used for the automatic delete check interval. 37 | `0` = no periodic check. 38 | 39 | ```js 40 | var cluster = require('cluster'); 41 | var myCache = require('cluster-node-cache')(cluster, {stdTTL: 100, checkperiod: 900}); 42 | ``` 43 | 44 | ### Optional CLS support 45 | 46 | ```js 47 | var cluster = require('cluster'); 48 | var cls = require('continuation-local-storage'); 49 | var ns = cls.createNamespace('myNamespace'); 50 | var myCache = require('cluster-node-cache')(cluster,{},ns); 51 | ``` 52 | 53 | ## Store a key (SET): 54 | 55 | `myCache.set(key, val, [ttl])` 56 | 57 | Sets a `key` `value` pair. It is possible to define a `ttl` (in seconds). 58 | Returns `true` on success. 59 | 60 | ```js 61 | obj = { my: "Special", variable: 42 }; 62 | myCache.set("myKey", obj).then(function(result) { 63 | console.log(result.err); 64 | console.log(result.success); 65 | }); 66 | ``` 67 | 68 | ## Retrieve a key (GET): 69 | 70 | `myCache.get(key)` 71 | 72 | Gets a saved value from the cache. 73 | Returns a `undefined` if not found or expired. 74 | 75 | ```js 76 | eyCache.get("myKey").then(function(results) { 77 | if(results.err) { 78 | console.log(results.err); 79 | } else { 80 | console.log(results.value.myKey); 81 | } 82 | }); 83 | ``` 84 | 85 | ## Get multiple keys (MGET): 86 | 87 | `myCache.get([ key1, key2, ...])` 88 | 89 | Gets multiple saved values from the cache. 90 | 91 | ```js 92 | myCache.get(["keyA", "keyB"]).then(function(results) { 93 | if(results.err) { 94 | console.log(results.err); 95 | } else { 96 | console.log(results.value); 97 | } 98 | }); 99 | ``` 100 | 101 | 102 | ## Delete a key (DEL): 103 | 104 | `myCache.del(key)` 105 | 106 | Delete a key. Returns the number of deleted entries. A delete will never fail. 107 | 108 | ``` 109 | myCache.del("myKey").then(function(results) { 110 | if(!results.err) { 111 | console.log(results.count); 112 | } 113 | }); 114 | ``` 115 | 116 | ## Change TTL (TTL): 117 | 118 | `myCache.ttl(key, ttl)` 119 | 120 | Redefine the ttl of a key. Returns true if the key has been found and changed. Otherwise returns false. 121 | If the ttl-argument isn't passed the default-TTL will be used. 122 | 123 | ```js 124 | myCache = new NodeCache({ stdTTL: 100 }) 125 | myCache.ttl( "myKey", 100).then(function(results) { 126 | if(!results.err) { 127 | console.log(results.changed); // true 128 | } 129 | }); 130 | ``` 131 | 132 | ## List keys (KEYS) 133 | 134 | `myCache.keys()` 135 | 136 | Returns an array of all existing keys. 137 | 138 | ```js 139 | myCache.keys().then(function(results) { 140 | if(!results.err) { 141 | console.log(results.keys); 142 | } 143 | }); 144 | ``` 145 | 146 | ## Statistics (STATS): 147 | 148 | `myCache.getStats()` 149 | 150 | Returns the statistics. 151 | 152 | ```js 153 | myCache.getStats().then(function(results) { 154 | console.log(results); 155 | /* 156 | { 157 | keys: 0, // global key count 158 | hits: 0, // global hit count 159 | misses: 0, // global miss count 160 | ksize: 0, // global key size count 161 | vsize: 0 // global value size count 162 | } 163 | */ 164 | }); 165 | ``` 166 | 167 | ## Flush all data (FLUSH): 168 | 169 | `myCache.flushAll()` 170 | 171 | Flush all data. 172 | 173 | ```js 174 | myCache.flushAll().then(function(results) { 175 | console.log(results); 176 | /* 177 | { 178 | keys: 0, // global key count 179 | hits: 0, // global hit count 180 | misses: 0, // global miss count 181 | ksize: 0, // global key size count 182 | vsize: 0 // global value size count 183 | } 184 | */ 185 | }); 186 | ``` 187 | 188 | # Pull requests welcomed ! 189 | * Especially if they contain tests better than the current terrible ones :) 190 | 191 | 192 | # The ISC License 193 | 194 | Copyright (c) 2015, Lee Turner 195 | 196 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 197 | 198 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 199 | -------------------------------------------------------------------------------- /cluster-node-cache.js: -------------------------------------------------------------------------------- 1 | module.exports = function(cluster, options, namespace) { 2 | var UNDEFINED_KEY_ERROR = "Undefined or null key"; 3 | 4 | var NodeCache = require("node-cache"); 5 | var Promise = require('bluebird'); 6 | var clsBluebird = require('cls-bluebird'); 7 | 8 | var crypto = require('crypto'); 9 | 10 | function prefixData(data) { 11 | data.clusternodecache = true; 12 | 13 | if (namespace && namespace.name) { 14 | data.namespace = namespace.name; 15 | } 16 | 17 | return data; 18 | } 19 | 20 | function incoming_message(worker, msg) { 21 | // validate this message is for us 22 | if (!msg.clusternodecache) { 23 | return; 24 | } 25 | 26 | if (msg.namespace && namespace && msg.namespace != namespace.name) { 27 | // for another cache (namespace) 28 | return; 29 | } 30 | 31 | switch(msg.method) { 32 | case "set": 33 | if(msg.key === undefined || msg.key === null) { 34 | worker.send(prefixData({ 35 | sig: msg.method + msg.key + msg.timestamp, 36 | body: { 37 | err: UNDEFINED_KEY_ERROR, 38 | success: {} 39 | } 40 | })); 41 | } else { 42 | cache.set(msg.key, msg.val, msg.ttl, function(err, success) { 43 | worker.send(prefixData({ 44 | sig: msg.method + msg.key + msg.timestamp, 45 | body: { 46 | err: err, 47 | success: success 48 | } 49 | })); 50 | }); 51 | } 52 | break; 53 | case "get": 54 | if(msg.key === undefined || msg.key === null) { 55 | worker.send(prefixData({ 56 | sig: msg.method + msg.key + msg.timestamp, 57 | body: { 58 | err: UNDEFINED_KEY_ERROR, 59 | success: {} 60 | } 61 | })); 62 | } else { 63 | cache.get(msg.key, function(err, value) { 64 | worker.send(prefixData({ 65 | sig: msg.method + msg.key + msg.timestamp, 66 | body: { 67 | err: err, 68 | value: value 69 | } 70 | })); 71 | }); 72 | } 73 | break; 74 | case "del": 75 | cache.del(msg.key, function(err, count) { 76 | worker.send(prefixData({ 77 | sig: msg.method + msg.key + msg.timestamp, 78 | body: { 79 | err: err, 80 | count: count 81 | } 82 | })); 83 | }); 84 | break; 85 | case "ttl": 86 | cache.ttl(msg.key, msg.ttl, function(err, changed) { 87 | worker.send(prefixData({ 88 | sig: msg.method + msg.key + msg.timestamp, 89 | body: { 90 | err: err, 91 | changed: changed 92 | } 93 | })); 94 | }); 95 | break; 96 | case "keys": 97 | cache.keys(function(err, keys) { 98 | worker.send(prefixData({ 99 | sig: msg.method + msg.timestamp, 100 | body: { 101 | err: err, 102 | keys: keys 103 | } 104 | })); 105 | }); 106 | break; 107 | case "getStats": 108 | worker.send(prefixData({ 109 | sig: msg.method + msg.timestamp, 110 | body: cache.getStats() 111 | })); 112 | break; 113 | case "flushAll": 114 | cache.flushAll(); 115 | worker.send(prefixData({ 116 | sig: msg.method + msg.timestamp, 117 | body: cache.getStats() 118 | })); 119 | break; 120 | } 121 | } 122 | 123 | if(cluster.isMaster && !process.env.DEBUG) { 124 | var cache = new NodeCache(options); 125 | 126 | cluster.on('online', function(worker) { 127 | worker.on('message', incoming_message.bind(null, worker)); 128 | }); 129 | 130 | return cache; 131 | } else { 132 | var ClusterCache = {}; 133 | var resolve_dict = {}; 134 | var debugCache = {}; 135 | 136 | var debugMode = Boolean(process.env.DEBUG); 137 | 138 | if(debugMode) { 139 | debugCache = new NodeCache(options); 140 | } 141 | 142 | if (namespace){ 143 | clsBluebird(namespace,Promise); 144 | } 145 | 146 | process.on("message", function(msg) { 147 | // validate this message is for us 148 | if (!msg.clusternodecache) { 149 | return; 150 | } 151 | 152 | if (msg.namespace && namespace && msg.namespace != namespace.name) { 153 | // for another cache (namespace) 154 | return; 155 | } 156 | 157 | if(resolve_dict[msg.sig]) { 158 | resolve_dict[msg.sig](msg.body); 159 | delete resolve_dict[msg.sig]; 160 | } 161 | }); 162 | 163 | ClusterCache.set = function(key, val, ttl) { 164 | return new Promise(function(resolve, reject) { 165 | if(debugMode) { 166 | if(key === undefined || key === null) { 167 | resolve({ err: "Undefined or null key", success: {} }); 168 | } else { 169 | debugCache.set(key, val, ttl, function(err, success) { 170 | resolve({ err: err, success: success }); 171 | }); 172 | } 173 | } else { 174 | var timestamp = getId(); 175 | resolve_dict["set" + key + timestamp] = resolve; 176 | process.send(prefixData({ 177 | method: "set", 178 | timestamp: timestamp, 179 | key: key, 180 | val: val, 181 | ttl: ttl 182 | })); 183 | } 184 | }); 185 | }; 186 | 187 | ClusterCache.get = function(key) { 188 | return new Promise(function(resolve, reject) { 189 | if(debugMode) { 190 | if(key === undefined || key === null) { 191 | resolve({ err: UNDEFINED_KEY_ERROR, success: {} }); 192 | } else { 193 | debugCache.get(key, function(err, value) { 194 | resolve({ err: err, value: value }); 195 | }); 196 | } 197 | } else { 198 | var timestamp = getId(); 199 | resolve_dict["get" + key + timestamp] = resolve; 200 | process.send(prefixData({ 201 | method: "get", 202 | timestamp: timestamp, 203 | key: key, 204 | })); 205 | } 206 | }); 207 | }; 208 | 209 | ClusterCache.del = function(key) { 210 | return new Promise(function(resolve, reject) { 211 | if(debugMode) { 212 | debugCache.del(key, function(err, count) { 213 | resolve({ err: err, count: count }); 214 | }); 215 | } else { 216 | var timestamp = getId(); 217 | resolve_dict["del" + key + timestamp] = resolve; 218 | process.send(prefixData({ 219 | method: "del", 220 | timestamp: timestamp, 221 | key: key, 222 | })); 223 | } 224 | }); 225 | }; 226 | 227 | ClusterCache.ttl = function(key, ttl) { 228 | return new Promise(function(resolve, reject) { 229 | if(debugMode) { 230 | debugCache.ttl(key, ttl, function(err, changed) { 231 | resolve({ err: err, changed: changed }); 232 | }); 233 | } else { 234 | var timestamp = getId(); 235 | resolve_dict["ttl" + key + timestamp] = resolve; 236 | process.send(prefixData({ 237 | method: "ttl", 238 | timestamp: timestamp, 239 | key: key, 240 | ttl: ttl 241 | })); 242 | } 243 | }); 244 | }; 245 | 246 | ClusterCache.keys = function() { 247 | return new Promise(function(resolve, reject) { 248 | if(debugMode) { 249 | debugCache.keys(function(err, keys) { 250 | resolve({ err: err, value: keys }); 251 | }); 252 | } else { 253 | var timestamp = getId(); 254 | resolve_dict['keys' + timestamp] = resolve; 255 | process.send(prefixData({ 256 | method: "keys", 257 | timestamp: timestamp, 258 | })); 259 | } 260 | }); 261 | }; 262 | 263 | ClusterCache.getStats = function() { 264 | return new Promise(function(resolve, reject) { 265 | if(debugMode) { 266 | resolve(debugCache.getStats()); 267 | } else { 268 | var timestamp = getId(); 269 | resolve_dict['getStats' + timestamp] = resolve; 270 | process.send(prefixData({ 271 | method: "getStats", 272 | timestamp: timestamp, 273 | })); 274 | } 275 | }); 276 | }; 277 | 278 | ClusterCache.flushAll = function() { 279 | return new Promise(function(resolve, reject) { 280 | if(debugMode) { 281 | resolve(debugCache.flushAll()); 282 | } else { 283 | var timestamp = getId(); 284 | resolve_dict['flushAll' + timestamp] = resolve; 285 | process.send(prefixData({ 286 | method: "flushAll", 287 | timestamp: timestamp, 288 | })); 289 | } 290 | }); 291 | }; 292 | 293 | return ClusterCache; 294 | } 295 | 296 | function getId() { 297 | return crypto.randomBytes(16).toString("hex"); 298 | } 299 | }; 300 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cluster-node-cache", 3 | "version": "0.1.5", 4 | "description": "A cluster aware wrapper for node-cache", 5 | "main": "cluster-node-cache.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "keywords": [ 10 | "cluster", 11 | "node-cache", 12 | "cache" 13 | ], 14 | "author": "Lee Turner", 15 | "license": "ISC", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/lvx3/cluster-cache.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/lvx3/cluster-cache/issues" 22 | }, 23 | "homepage": "https://github.com/lvx3/cluster-cache", 24 | "keywords": [ "cluster", "node-cache", "cache", "caching", "local", "variable", "multi", "memory", "internal", "node", "memcached", "object" ], 25 | "tags": [ "cluster", "node-cache", "cache", "caching", "local", "variable", "multi", "memory", "internal", "node", "memcached", "object" ], 26 | "dependencies": { 27 | "bluebird": "^2.9.14", 28 | "node-cache": "^1.1.0", 29 | "cls-bluebird": "^1.1.3" 30 | }, 31 | "devDependencies": { 32 | "async": "^0.9.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var cluster = require('cluster'); 2 | var async = require('async'); 3 | var cluster_cache; 4 | try { 5 | var cls = require('continuation-local-storage'); 6 | var ns = cls.createNamespace('myNamespace'); 7 | cluster_cache = require('./cluster-node-cache')(cluster,{},ns); 8 | } catch (e) { 9 | console.log("*** CLS support not found, continuing without"); 10 | cluster_cache = require('./cluster-node-cache')(cluster); 11 | } 12 | 13 | if(cluster.isMaster && !process.env.DEBUG) { 14 | worker = cluster.fork(); 15 | cluster.on('exit', function() { 16 | cluster.fork(); 17 | }); 18 | } else { 19 | cluster_cache.get("key that can't exist"); 20 | cluster_cache.get(undefined); 21 | cluster_cache.get(null); 22 | 23 | cluster_cache.set("myKey", "myVal", 1).then(function(result) { 24 | console.log("got result err: " + result.err); 25 | console.log("got result success: " + result.success); 26 | }); 27 | 28 | cluster_cache.get("myKey").then(function(result) { 29 | console.log("Get cache result... "); 30 | console.log(result.value); 31 | }); 32 | 33 | cluster_cache.get("myKey2").then(function(result) { 34 | console.log("Get cache result... "); 35 | console.log(result.value); 36 | }); 37 | 38 | cluster_cache.set("myKey3", "myVal").then(function(result) { 39 | return cluster_cache.set("myKey4", "myVal"); 40 | }).then(function(result) { 41 | console.log("looking for multiple keys"); 42 | cluster_cache.get(["myKey3", "myKey4"]).then(function(result) { 43 | console.log("Results from looking for multiple keys"); 44 | console.log(result); 45 | }); 46 | }); 47 | 48 | 49 | cluster_cache.set(undefined, "some value").then(function(result) { 50 | console.log("Results of trying to set undefined key"); 51 | console.log(result.value); 52 | }); 53 | 54 | cluster_cache.set("undefined", undefined).then(function(result) { 55 | console.log("Results of setting undefined value"); 56 | console.log(result.value); 57 | }); 58 | 59 | cluster_cache.get("undefined").then(function(result) { 60 | console.log("Results of getting a value that's undefined"); 61 | console.log(result.value); 62 | }); 63 | 64 | cluster_cache.del("myKey"); 65 | 66 | cluster_cache.getStats().then(function(result) { 67 | console.log(result); 68 | }); 69 | 70 | cluster_cache.getStats().then(function(result) { 71 | console.log(result); 72 | }); 73 | 74 | async.parallel([ 75 | function(callback) { 76 | cluster_cache.set("val", "val").then(function(result) { 77 | console.log("-----"); 78 | console.log("ASYNC Set val"); 79 | console.log(result); 80 | console.log("-----"); 81 | callback(null, result); 82 | }); 83 | }, 84 | function(callback) { 85 | cluster_cache.set("val2", "val2").then(function(result) { 86 | console.log("-----"); 87 | console.log("Set val2"); 88 | console.log(result); 89 | console.log("-----"); 90 | callback(null, result); 91 | }); 92 | }, 93 | function(callback) { 94 | cluster_cache.get("val").then(function(result) { 95 | console.log("-----"); 96 | console.log("Looking for val, found "); 97 | console.log(result); 98 | console.log("-----"); 99 | callback(null, result); 100 | }); 101 | } 102 | ], 103 | // optional callback 104 | function(err, results){ 105 | // the results array will equal ['one','two'] even though 106 | // the second function had a shorter timeout. 107 | console.log("ERRORS: " + err); 108 | console.log("RESULTS: " + results); 109 | }); 110 | 111 | async.concat([ 'key1', 'key2', 'key3'], function(key_name, callback) { 112 | cluster_cache.set(key_name, key_name).then(function(results) { 113 | console.log("Set " + key_name); 114 | cluster_cache.set(key_name + "alt", key_name + "alt"); 115 | }); 116 | cluster_cache.get(key_name).then(function(results) { 117 | console.log("-----"); 118 | console.log("Get " + key_name); 119 | console.log(results); 120 | console.log("-----"); 121 | cluster_cache.get(key_name+"alt").then(function(altresults) { 122 | console.log("-----"); 123 | console.log("Alt results"); 124 | console.log(altresults); 125 | callback(null, altresults); 126 | console.log("-----"); 127 | }); 128 | }); 129 | }, 130 | function(err, results) { console.log("End of concat"); console.log(results); }); 131 | } 132 | --------------------------------------------------------------------------------