diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..568b089 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# IDEA specific files +*.iml +.idea + +# Packages +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..78393eb --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright (c) 2016 KTH Royal Institute of Technology + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index f423f11..4792710 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ # kth-node-redis + Redis client module for Node.js. Everything with Promises! + +## Usage + +```javascript +const redis = require('kth-redis'); + +// basics +redis('default', { /* optional redis client config */ }) + .then(function(client) { + return client.getAsync('key'); + }) + .then(function(value) { + // do something with value + }) + .catch(function(err) { + // handle error + }); + +// multi +redis('default', { /* optional redis client config */ }) + .then(function(client) { + return client.multi() + .hmset('foo', { value: 'bar' }) + .expire('foo', 30) + .hgetall('foo') + .execAsync(); + }) + .then(function(results) { + // results[1] => 'OK' + // results[1] => 1 + // results[2] => { value: 'bar' } + + // results will depend on what commands are executed + }) + .catch(function(err) { + // handle error + }); + +// quit if needed +redis.quit('default'); +``` + +## Options + +- `name` optional name, defaults to `default`. Use the same name to get + the same client instance or re-create it. Use a new name to create a + new instance. +- `options` optional config for the Redis client. Has a default retry + strategy. See below for details. For info about the Redis client + options, see https://www.npmjs.com/package/redis. + +## Default retry strategy + +```javascript +function retry_strategy(options) { + if (options.error.code === 'ECONNREFUSED') { + return new Error('Connection refused by server.'); + } + + if (options.total_retry_time > 1000 * 60 * 60) { + return new Error('Retry time exhausted'); + } + + if (options.times_connected > 10) { + return undefined; + } + + return Math.max(options.attempt * 100, 3000); +} +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..c84a4a0 --- /dev/null +++ b/index.js @@ -0,0 +1,135 @@ +'use strict' + +const logger = require('kth-node-log') +const redis = require('redis') +const Promise = require('bluebird') +const _ = require('lodash') + +Promise.promisifyAll(redis.RedisClient.prototype) +Promise.promisifyAll(redis.Multi.prototype) + +const _defaults = { + retry_strategy: function (options) { + if (options.error.code === 'ECONNREFUSED') { + return new Error('Connection refused by server.') + } + + if (options.total_retry_time > 1000 * 60 * 60) { + return new Error('Retry time exhausted') + } + + if (options.times_connected > 10) { + return undefined + } + + return Math.max(options.attempt * 100, 3000) + } +} + +const _defaultName = 'default' + +let _clients = {} + +function _once (fn) { + let called = false + let value = false + + return function () { + if (called) { + return value + } + + value = fn.apply(this, arguments) + called = true + return value + } +} + +function _createClient (name, options, callback) { + const log = logger.child({ redis: name }) + log.debug('Redis creating client') + let isReady = false + + const config = _.defaultsDeep(_defaults, options) + let client = redis.createClient(config) + + callback = _once(callback) + _clients[ name ] = client + _clients[ name ].log = log + + log.debug({ clients: Object.keys(_clients) }, 'Redis clients') + + client.on('error', function (err) { + log.error({ err: err }, 'Redis client error') + callback(err) + }) + + client.on('warning', function (err) { + log.warn({ err: err }, 'Redis client warning') + }) + + client.on('connect', function () { + log.debug('Redis connected') + }) + + client.on('ready', function () { + log.debug('Redis client ready') + log.debug({ config: config }, 'Redis client config') + log.debug(`Redis server version: ${client.server_info.redis_version}`) + isReady = true + callback(null, client) + }) + + client.on('reconnecting', function () { + log.debug('Redis client reconnecting') + }) + + client.on('end', function () { + log.debug('Redis client end') + client = null + delete _clients[ name ] + log.debug({ clients: Object.keys(_clients) }, 'Redis clients') + if (!isReady) { + callback(new Error('Done - Failed to connect to Redis')) + } + }) +} + +/** + * Creates a new client or uses an existing client. + * @param {String} name - Name of the Redis instance. + * @param {Object} options - Redis configuration. + * @returns {Promise} Promise that resolves to a Redis client. + */ +module.exports = function (name, options) { + name = name || _defaultName + let client = _clients[ name ] + + if (client) { + logger.debug(`Redis using client: ${name}`) + return Promise.resolve(client) + } + + return new Promise(function (resolve, reject) { + _createClient(name, options, function (err, client) { + if (err) { + reject(err) + } else { + resolve(client) + } + }) + }) +} + +/** + * Helper to close a Redis client connection by its name. + * @param {String} name - Name of the Redis instance. + */ +module.exports.quit = function (name) { + name = name || _defaultName + let client = _clients[ name ] + if (client) { + // triggers end event + client.quit() + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0f68bc2 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "kth-node-redis", + "version": "1.0.0", + "description": "Redis client module for Node.js. Everything with Promises!", + "private": true, + "main": "index.js", + "scripts": { + "codecheck": "standard", + "preversion": "npm run codecheck", + "postversion": "git push && git push --tags" + }, + "repository": { + "type": "git", + "url": "https://github.com/KTH/kth-node-redis" + }, + "keywords": [ + "node", + "redis" + ], + "peerDependencies": { + "kth-node-log": "https://github.com/KTH/kth-node-log.git#v1.0.0" + }, + "dependencies": { + "bluebird": "^3.3.5", + "lodash": "^4.11.1", + "redis": "^2.5.3" + }, + "devDependencies": { + "standard": "^7.0.0" + } +}