├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── README.md ├── config └── env.js ├── index.js ├── lib ├── RedisEventNotifier.js └── adapters │ └── logger.js ├── log4js.json ├── package.json └── spec ├── mocks └── redis.js ├── redisEventNotifier.spec.js ├── test.harness.js └── test.integration.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly" : false, 3 | "eqeqeq" : true, 4 | "immed" : true, 5 | "latedef" : true, 6 | "newcap" : true, 7 | "noarg" : true, 8 | "sub" : true, 9 | "undef" : true, 10 | "boss" : true, 11 | "eqnull" : true, 12 | "maxerr" : 100000, 13 | "browser" : true, 14 | "globals" : { 15 | "ENV" : true, 16 | "console" : false, 17 | "define" : false, 18 | "require" : false, 19 | "_" : false, 20 | "rcl" : false, 21 | "module" : true, 22 | "it" : false, 23 | "xit" : false, 24 | "describe" : false, 25 | "xdescribe" : false, 26 | "beforeEach" : false, 27 | "afterEach" : false, 28 | "expect" : false, 29 | "spyOn" : false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g npm 6 | services: 7 | - redis-server -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | "use strict"; 3 | 4 | grunt.initConfig({ 5 | jasmine_node : { 6 | 7 | unit : { 8 | projectRoot : ".", 9 | requirejs : false, 10 | forceExit : true, 11 | specNameMatcher : "spec" 12 | }, 13 | 14 | integration : { 15 | projectRoot : ".", 16 | requirejs : false, 17 | forceExit : true, 18 | specNameMatcher : "integration" 19 | } 20 | }, 21 | jshint : { 22 | options : { 23 | jshintrc : '.jshintrc' 24 | }, 25 | task : ['lib/**/*.js'] 26 | } 27 | }); 28 | 29 | grunt.loadNpmTasks('grunt-contrib-jshint'); 30 | grunt.loadNpmTasks('grunt-jasmine-node'); 31 | 32 | grunt.registerTask('default', ['jshint', 'test-unit']); 33 | grunt.registerTask('dev', ['jshint']); 34 | grunt.registerTask('test', ['jasmine_node']); 35 | grunt.registerTask('test-unit', ['jasmine_node:unit']); 36 | grunt.registerTask('test-integration', ['jasmine_node:integration']); 37 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Redis KeySpace Event Notifier [![Build Status](https://travis-ci.org/iamchrismiller/redis-notifier.png)](https://travis-ci.org/iamchrismiller/redis-notifier) 2 | 3 | Subscribe To Redis Keyspaced Events (v2.8.x) 4 | Using Redis' Newly Released Keyspaced Events Feature You can now subscribe to events that the server emits 5 | Depending on the subscription mode you subscribe with when starting the Redis Server. 6 | 7 | `--notify-keyspace-events ` 8 | 9 | - K Keyspace events, published with __keyspace@__ prefix. 10 | - E Keyevent events, published with __keyevent@__ prefix. 11 | - g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ... 12 | - $ String commands 13 | - l List commands 14 | - s Set commands 15 | - h Hash commands 16 | - z Sorted set commands 17 | - x Expired events (events generated every time a key expires) 18 | - e Evicted events (events generated when a key is evicted for maxmemory) 19 | - A Alias for g$lshzxe, so that the "AKE" string means all the events. 20 | 21 | ## Getting Started 22 | 23 | Using NPM + Package.json, simply just run `npm install` 24 | 25 | If you are using `node_redis` pre `v0.11.0` checkout the tag `v0.1.2` 26 | 27 | ## Usage / Configuration 28 | 29 | Start Redis Server : `redis-server CONF --notify-keyspace-events KExe` 30 | 31 | ```javascript 32 | var redis = require('redis'); 33 | var RedisNotifier = require('redis-notifier'); 34 | 35 | var eventNotifier = new RedisNotifier(redis, { 36 | redis : { host : '127.0.0.1', port : 6379 }, 37 | expired : true, 38 | evicted : true, 39 | logLevel : 'DEBUG' //Defaults To INFO 40 | }); 41 | 42 | //Listen for event emission 43 | eventNotifier.on('message', function(pattern, channelPattern, emittedKey) { 44 | var channel = this.parseMessageChannel(channelPattern); 45 | switch(channel.key) { 46 | case 'expired': 47 | console.info(`Expired Key ${key}`); 48 | break; 49 | case "evicted": 50 | console.info(`Evicted Key ${key}`); 51 | break; 52 | default: 53 | logger.debug("Unrecognized Channel Type:" + channel.type); 54 | } 55 | }); 56 | ``` 57 | 58 | ## Contributing 59 | In lieu of a formal style-guide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using grunt. 60 | 61 | ## Release History 62 | 63 | - 1.0.0 updated required dependencies 64 | - 0.2.0 updated node_redis connection args, added deinit method 65 | - 0.1.2 updated logger interface 66 | - 0.1.1 changed expire attribute to expired 67 | - 0.1.0 Initial release 68 | 69 | ## License 70 | 71 | Licensed under the Apache 2.0 license. 72 | 73 | ## Author 74 | 75 | Chris Miller 76 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | /*global process, module*/ 2 | 3 | module.exports = { 4 | redis : { 5 | host : '127.0.0.1', 6 | port : 6379 7 | } 8 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/RedisEventNotifier'); -------------------------------------------------------------------------------- /lib/RedisEventNotifier.js: -------------------------------------------------------------------------------- 1 | /*global require, module*/ 2 | 3 | //node 4 | var EventEmitter = require('events').EventEmitter; 5 | //npm 6 | var extend = require('extend'); 7 | //local 8 | var logAdapter = require('./adapters/logger'), 9 | logger = logAdapter.getInstance('redis-event-notifier'); 10 | 11 | 12 | /** 13 | * Redis Event Notifier 14 | * Subscribe to Redis Keyspace Notifications(v2.8.x) 15 | * @param redis 16 | * @param options 17 | * @constructor 18 | */ 19 | function RedisNotifier(redis, options) { 20 | 21 | this.settings = extend(true, { 22 | redis : { 23 | host : 'localhost', 24 | port : 6379, 25 | db : 0, 26 | options : { 27 | family : null //IPv4/IPv6 28 | } 29 | }, 30 | expired : true, 31 | evicted : true, 32 | logLevel : 'INFO' 33 | }, options || {}); 34 | 35 | //Set Global Log Level 36 | logAdapter.setLogLevel(this.settings.logLevel); 37 | 38 | 39 | //Require Redis if its not injected 40 | if (!redis || typeof redis !== 'object') { 41 | throw new Error("You must provide a Redis module"); 42 | } 43 | 44 | //The Redis Subscriber Instance 45 | logger.info("Initializing" + JSON.stringify(this.settings)); 46 | 47 | // Call the super EventEmitter constructor. 48 | EventEmitter.call(this); 49 | 50 | //Create Redis Subscriber Client 51 | this.subscriber = redis.createClient(this.settings.redis.port, this.settings.redis.host, this.settings.redis.options); 52 | //Select Appropriate Database 53 | this.subscriber.select(this.settings.redis.db); 54 | 55 | //Redis Ready To Subscribe 56 | this.subscriber.on('ready', function () { 57 | logger.info("Redis Subscriber Ready"); 58 | //Subscribe To Expired/Evicted Events 59 | this._subscribeToEvents.call(this); 60 | }.bind(this)); 61 | 62 | //Bind To Redis Store Message Handler 63 | this.subscriber.on("pmessage", function (pattern, channel, key) { 64 | logger.debug("Received Message" + JSON.stringify(arguments)); 65 | this.emit('message', pattern, channel, key); 66 | }.bind(this)); 67 | } 68 | 69 | //Inherit From The EventEmitter Prototype 70 | RedisNotifier.prototype = Object.create(EventEmitter.prototype); 71 | 72 | /** 73 | * Subscribe to Expired/Evicted Events 74 | * Emitted From Redis 75 | * @private 76 | */ 77 | RedisNotifier.prototype._subscribeToEvents = function () { 78 | logger.info("Subscribing To Events"); 79 | //events generated every time a key expires 80 | if (this.settings.expired) { 81 | this._subscribeKeyevent('expired'); 82 | } 83 | //events generated when a key is evicted for max-memory 84 | if (this.settings.evicted) { 85 | this._subscribeKeyevent('evicted'); 86 | } 87 | 88 | //Let user know its ready to handle subscriptions 89 | this.emit('ready'); 90 | }; 91 | 92 | 93 | /** 94 | * De-init the subscriptions 95 | */ 96 | RedisNotifier.prototype.deinit = function() { 97 | if (this.settings.expired) { 98 | this._unsubscribeKeyevent('expired'); 99 | } 100 | if (this.settings.evicted) { 101 | this._unsubscribeKeyevent('evicted'); 102 | } 103 | }; 104 | 105 | /** 106 | * Parse The Type/Key From ChannelKey 107 | * @param channel 108 | * @returns {{type: *, key: *}} 109 | */ 110 | RedisNotifier.prototype.parseMessageChannel = function (channel) { 111 | //__keyevent@0__:expired 112 | var re = /__([a-z]*)+@([0-9])+__:([a-z]*)/i; 113 | var parts = channel.match(re); 114 | 115 | return { 116 | type : parts[1], 117 | key : parts[3] 118 | }; 119 | }; 120 | 121 | /** 122 | * Subscribe To Specific Redis Keyspace Event 123 | * @param key 124 | * @private 125 | */ 126 | RedisNotifier.prototype._subscribeKeyspace = function (key) { 127 | var subscriptionKey = "__keyspace@" + this.settings.redis.db + "__:" + key; 128 | logger.debug("Subscribing To Event " + subscriptionKey); 129 | this.subscriber.psubscribe(subscriptionKey); 130 | }; 131 | 132 | /** 133 | * UnSubscribe To Specific Redis Keyspace Event 134 | * @param key 135 | * @private 136 | */ 137 | RedisNotifier.prototype._unsubscribeKeyspace = function (key) { 138 | var subscriptionKey = "__keyspace@" + this.settings.redis.db + "__:" + key; 139 | logger.debug("UnSubscribing From Event " + subscriptionKey); 140 | this.subscriber.punsubscribe(subscriptionKey); 141 | }; 142 | 143 | /** 144 | * Subscribe To KeyEvent (Expired/Evicted) 145 | * @param key 146 | * @private 147 | */ 148 | RedisNotifier.prototype._subscribeKeyevent = function (key) { 149 | var subscriptionKey = "__keyevent@" + this.settings.redis.db + "__:" + key; 150 | logger.debug("Subscribing To Event :" + subscriptionKey); 151 | this.subscriber.psubscribe(subscriptionKey); 152 | }; 153 | 154 | 155 | /** 156 | * UnSubscribe To KeyEvent (Expired/Evicted) 157 | * @param key 158 | * @private 159 | */ 160 | RedisNotifier.prototype._unsubscribeKeyevent = function (key) { 161 | var subscriptionKey = "__keyevent@" + this.settings.redis.db + "__:" + key; 162 | logger.debug("UnSubscribing From Event :" + subscriptionKey); 163 | this.subscriber.punsubscribe(subscriptionKey); 164 | }; 165 | 166 | 167 | module.exports = RedisNotifier; -------------------------------------------------------------------------------- /lib/adapters/logger.js: -------------------------------------------------------------------------------- 1 | /*global module, require, __dirname*/ 2 | 3 | 4 | //npm 5 | var log4js = require('log4js'); 6 | 7 | //Load log4 Config 8 | log4js.configure(__dirname + '/../../log4js.json'); 9 | 10 | //Global LogLevel Informational By Default 11 | var globalLogLevel = log4js.levels.INFO; 12 | 13 | var logger = { 14 | 15 | _instances : [], 16 | 17 | getInstance : function (name) { 18 | if (!this._instances[name]) { 19 | this._instances[name] = log4js.getLogger(name); 20 | } 21 | 22 | this._instances[name].setLevel(globalLogLevel); 23 | return this._instances[name]; 24 | }, 25 | 26 | setLogLevel : function (level) { 27 | level = level.toUpperCase(); 28 | 29 | if (Object.keys(log4js.levels).indexOf(level) !== -1) { 30 | //Set Global For All New Loggers 31 | globalLogLevel = level; 32 | //Set All Active Logger Levels 33 | for (var instName in this._instances) { 34 | this._instances[instName].setLevel(globalLogLevel); 35 | } 36 | } else { 37 | throw new Error("Log Level %s not recognized", level); 38 | } 39 | } 40 | }; 41 | 42 | /** 43 | * Log4js Wrapper 44 | * @param loggerName 45 | * @returns {Logger} 46 | */ 47 | module.exports = { 48 | 49 | getInstance : function (loggerName) { 50 | return logger.getInstance(loggerName); 51 | }, 52 | 53 | setLogLevel : function (level) { 54 | logger.setLogLevel(level); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /log4js.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders" : [ 3 | { 4 | "type" : "console" 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-notifier", 3 | "version": "1.0.0", 4 | "description": "Redis Keyspace Event Notifier", 5 | "author": "Chris Miller", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "grunt test-unit" 9 | }, 10 | "devDependencies": { 11 | "grunt": "~0.4.2", 12 | "grunt-cli": "~0.1.11", 13 | "grunt-jasmine-node": "~0.3.1" 14 | }, 15 | "dependencies": { 16 | "extend": "~3.0.0", 17 | "grunt-contrib-jshint": "~0.11.3", 18 | "hiredis": "^0.4.1", 19 | "log4js": "~0.6.9", 20 | "redis": "^2.4.2" 21 | }, 22 | "keywords": [ 23 | "redis", 24 | "keyspace", 25 | "events", 26 | "notifier", 27 | "redis-keyspace" 28 | ], 29 | "license": "Apache" 30 | } 31 | -------------------------------------------------------------------------------- /spec/mocks/redis.js: -------------------------------------------------------------------------------- 1 | /*global process*/ 2 | 3 | "use strict"; 4 | 5 | //node 6 | var EventEmitter = require('events').EventEmitter; 7 | var util = require('util'); 8 | 9 | function RedisClient(options) { 10 | this.options = options; 11 | 12 | // Call the super EventEmitter constructor. 13 | EventEmitter.call(this); 14 | 15 | var self = this; 16 | process.nextTick(function() { 17 | self.emit('ready'); 18 | }); 19 | } 20 | 21 | //Inherit EventEmitter Prototype Methods 22 | RedisClient.prototype = Object.create( EventEmitter.prototype ); 23 | 24 | RedisClient.prototype.psubscribe = function(key) {}; 25 | RedisClient.prototype.punsubscribe = function(key) {}; 26 | RedisClient.prototype.select = function(key) {}; 27 | 28 | 29 | //Test Helper 30 | RedisClient.prototype._triggerMessage = function(pattern, channel, expiredKey) { 31 | this.emit("pmessage", pattern, channel, expiredKey); 32 | }; 33 | 34 | module.exports = { 35 | 36 | createClient : function(options) { 37 | return new RedisClient(options); 38 | } 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /spec/redisEventNotifier.spec.js: -------------------------------------------------------------------------------- 1 | /*global process, describe */ 2 | 'use strict'; 3 | 4 | //Test Harness Containing Pointer To Lib 5 | var harness = require('./test.harness'), 6 | RedisEventNotifier = require(harness.lib + 'RedisEventNotifier'), 7 | redis = require('./mocks/redis'), 8 | notifierOptions = {logLevel : 'DEBUG'}; 9 | 10 | //Connection Test Suite 11 | describe('RedisEventNotifier Suite', function () { 12 | 13 | it('Should have a construct and throw an error if redis instance is not supplied', function () { 14 | expect(function () { 15 | new RedisEventNotifier(null, notifierOptions); 16 | }).toThrow(new Error("You must provide a Redis module")); 17 | }); 18 | 19 | it('Should evaulate the channel response correctly for parseMessageChannel', function () { 20 | var eventNotifier = new RedisEventNotifier(redis, notifierOptions); 21 | 22 | var expiredKeyTest = eventNotifier.parseMessageChannel('__keyevent@0__:expired'); 23 | expect(expiredKeyTest.key).toBe('expired'); 24 | expect(expiredKeyTest.type).toBe('keyevent'); 25 | 26 | var evictedKeyTest = eventNotifier.parseMessageChannel('__keyevent@0__:evicted'); 27 | expect(evictedKeyTest.key).toBe('evicted'); 28 | expect(evictedKeyTest.type).toBe('keyevent'); 29 | }); 30 | 31 | it('Should emit a "message" event when a key expires', function (done) { 32 | var eventNotifier = new RedisEventNotifier(redis, notifierOptions); 33 | 34 | process.nextTick(function () { 35 | //trigger expire message (test helper) 36 | eventNotifier.subscriber._triggerMessage('__keyevent@0__:expired', '__keyevent@0__:expired', 'test.key'); 37 | }); 38 | 39 | eventNotifier.on('message', function (pattern, channel, key) { 40 | expect(pattern).toBe('__keyevent@0__:expired'); 41 | expect(channel).toBe('__keyevent@0__:expired'); 42 | expect(key).toBe('test.key'); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('Should emit a "message" event when a key is evicted', function (done) { 48 | var eventNotifier = new RedisEventNotifier(redis, notifierOptions); 49 | 50 | process.nextTick(function () { 51 | //trigger expire message (test helper) 52 | eventNotifier.subscriber._triggerMessage('__keyevent@0__:evicted', '__keyevent@0__:evicted', 'test.key'); 53 | }); 54 | 55 | eventNotifier.on('message', function (pattern, channel, key) { 56 | expect(pattern).toBe('__keyevent@0__:evicted'); 57 | expect(channel).toBe('__keyevent@0__:evicted'); 58 | expect(key).toBe('test.key'); 59 | done(); 60 | }); 61 | }); 62 | 63 | }); 64 | 65 | -------------------------------------------------------------------------------- /spec/test.harness.js: -------------------------------------------------------------------------------- 1 | /*global module*/ 2 | 3 | 'use strict'; 4 | 5 | //Create Test Harness and necessary Bootstrapping 6 | 7 | var harness = { 8 | 9 | lib : __dirname + '/../lib/', 10 | 11 | //Automatically Set Warn LogLevel 12 | setLogLevel : function(level) { 13 | var logger = require(harness.lib + "adapters/logger"); 14 | logger.setLogLevel(level); 15 | } 16 | }; 17 | 18 | module.exports = harness; -------------------------------------------------------------------------------- /spec/test.integration.js: -------------------------------------------------------------------------------- 1 | /*global require, describe, it, process*/ 2 | 'use strict'; 3 | 4 | var harness = require('./test.harness'), 5 | RedisEventNotifier = require(harness.lib + 'RedisEventNotifier'), 6 | redis = require('redis'), 7 | notifierOptions = {logLevel : 'DEBUG'}; 8 | 9 | 10 | //Integration Test Suite 11 | describe('RedisEventNotifier Integration Suite', function () { 12 | 13 | it('Should create a successful redis connection', function (done) { 14 | var eventNotifier = new RedisEventNotifier(redis, notifierOptions); 15 | 16 | //Wait For Event Notifier To Be Ready 17 | eventNotifier.on('ready', function () { 18 | expect(this.subscriber.connected).toBeTruthy(); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('Should Receive a __keyevent@0__:expired event upon key expire', function (done) { 24 | var eventNotifier = new RedisEventNotifier(redis, notifierOptions); 25 | 26 | //Wait for Message Event 27 | eventNotifier.on('message', function (pattern, channel, key) { 28 | expect(pattern).toBe('__keyevent@0__:expired'); 29 | expect(channel).toBe('__keyevent@0__:expired'); 30 | expect(key).toBe('test.key'); 31 | done(); 32 | }); 33 | 34 | //Wait For Event Notifier To Be Ready 35 | eventNotifier.on('ready', function () { 36 | //Test Client To Expire Key 37 | var client = redis.createClient(); 38 | //select correct db 39 | client.select(0); 40 | //Wait For Client TO Be Ready So I can Issue TTL Command 41 | client.on('ready', function () { 42 | //Set Expire event for the Notifier to pick up 43 | client.SETEX(['test.key', 2, 'value'], function() {}); 44 | }); 45 | }); 46 | }); 47 | }); --------------------------------------------------------------------------------