├── VERSION ├── config ├── error │ ├── param.json │ └── general.json ├── coreConstant.js └── cache.js ├── test ├── env │ ├── redis.sh │ ├── in-memory.sh │ ├── memcached.sh │ ├── in-memory.json │ ├── in-memory2.json │ ├── memcached.json │ ├── memcached2.json │ ├── redis.json │ └── redis2.json ├── cache_del.js ├── cache_getobject.js ├── cache_multiget.js ├── cache_release_lock.js ├── cache_acquire_lock.js ├── cache_touch.js ├── cache_increment.js ├── cache_decrement.js ├── cache_set.js ├── cache_setobject.js └── cache_get.js ├── lib ├── cache │ ├── existingInstance.js │ ├── helper.js │ └── implementer │ │ ├── InMemory.js │ │ ├── Memcached.js │ │ └── Redis.js ├── formatter │ └── response.js └── logger │ └── customConsoleLogger.js ├── instance_composer.js ├── .prettierrc.json ├── config.json.example ├── .travis.yml ├── package.json ├── .gitignore ├── CHANGELOG.md ├── .eslintrc.js ├── index.js ├── README.md ├── services └── CacheInstance.js └── LICENSE /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.8 2 | -------------------------------------------------------------------------------- /config/error/param.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/env/redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export TEST_CACHING_ENGINE='redis' -------------------------------------------------------------------------------- /test/env/in-memory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export TEST_CACHING_ENGINE='none' -------------------------------------------------------------------------------- /test/env/memcached.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export TEST_CACHING_ENGINE='memcached' -------------------------------------------------------------------------------- /lib/cache/existingInstance.js: -------------------------------------------------------------------------------- 1 | // This is to share the instance map between multiple @ostdotcom/cache objects. 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/env/in-memory.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "engine": "none", 4 | "namespace": "A", 5 | "defaultTtl": "60", 6 | "consistentBehavior": "1" 7 | } 8 | } -------------------------------------------------------------------------------- /test/env/in-memory2.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "engine": "none", 4 | "namespace": "B", 5 | "defaultTtl": "60", 6 | "consistentBehavior": "1" 7 | } 8 | } -------------------------------------------------------------------------------- /lib/formatter/response.js: -------------------------------------------------------------------------------- 1 | const OSTBase = require('@ostdotcom/base'), 2 | responseHelper = new OSTBase.responseHelper({ moduleName: 'OSTCache' }); 3 | 4 | module.exports = responseHelper; 5 | -------------------------------------------------------------------------------- /test/env/memcached.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "engine": "memcached", 4 | "servers": ["localhost:11212", "localhost:11213"], 5 | "defaultTtl": 60, 6 | "consistentBehavior": "1" 7 | } 8 | } -------------------------------------------------------------------------------- /test/env/memcached2.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "engine": "memcached", 4 | "servers": ["localhost:11214", "localhost:11215"], 5 | "defaultTtl": 60, 6 | "consistentBehavior": "1" 7 | } 8 | } -------------------------------------------------------------------------------- /instance_composer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Instance Composer from @ostdotcom/base 5 | * 6 | * @module instance_composer 7 | */ 8 | 9 | const OSTBase = require('@ostdotcom/base'); 10 | module.exports = OSTBase.InstanceComposer; 11 | -------------------------------------------------------------------------------- /test/env/redis.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "engine": "redis", 4 | "host": "localhost", 5 | "port": "6380", 6 | "password": "my-secret", 7 | "enableTsl": "0", 8 | "defaultTtl": 60, 9 | "consistentBehavior": "1" 10 | } 11 | } -------------------------------------------------------------------------------- /test/env/redis2.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "engine": "redis", 4 | "host": "localhost", 5 | "port": "6381", 6 | "password": "my-secret", 7 | "enableTsl": "0", 8 | "defaultTtl": 60, 9 | "consistentBehavior": "1" 10 | } 11 | } -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "arrowParens": "always", 10 | "parser": "flow", 11 | "proseWrap": "preserve" 12 | } 13 | -------------------------------------------------------------------------------- /lib/logger/customConsoleLogger.js: -------------------------------------------------------------------------------- 1 | const OSTBase = require('@ostdotcom/base'), 2 | Logger = OSTBase.Logger; 3 | 4 | const rootPrefix = '../..', 5 | coreConstants = require(rootPrefix + '/config/coreConstant'); 6 | 7 | // Following is to ensure that INFO logs are printed when debug is off. 8 | const loggerLevel = Number(coreConstants.DEBUG_ENABLED) === 1 ? Logger.LOG_LEVELS.DEBUG : Logger.LOG_LEVELS.INFO; 9 | 10 | module.exports = new Logger('ost-cache', loggerLevel); 11 | -------------------------------------------------------------------------------- /config/coreConstant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class for core constants. 3 | * 4 | * @class CoreConstant 5 | */ 6 | class CoreConstant { 7 | /** 8 | * Returns IC namespace. 9 | * 10 | * @returns {string} 11 | */ 12 | get icNameSpace() { 13 | return 'OSTCache'; 14 | } 15 | 16 | /** 17 | * Returns debug enabled or not. 18 | * 19 | * @returns {boolean} 20 | */ 21 | get DEBUG_ENABLED() { 22 | // eslint-disable-next-line no-process-env 23 | return process.env.OST_DEBUG_ENABLED; 24 | } 25 | } 26 | 27 | module.exports = new CoreConstant(); 28 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | //{ 2 | // "engine": "memcached", 3 | // "servers": [ 4 | // "127.0.0.1:11211" 5 | // ], 6 | // "defaultTtl": 36000, 7 | // "consistentBehavior": "1" 8 | //} 9 | 10 | 11 | //{ 12 | // "engine": "redis", 13 | // "host": "localhost", 14 | // "port": "6830", 15 | // "password": "dsdsdsd", 16 | // "enableTsl": "0", 17 | // "defaultTtl": 36000, 18 | // "consistentBehavior": "1" 19 | //} 20 | 21 | 22 | //{ 23 | // "engine": "none", 24 | // "namespace": "pk_sdsd", 25 | // "defaultTtl": 36000, 26 | // "consistentBehavior": "1" 27 | //} 28 | 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | sudo: required 4 | branches: 5 | only: 6 | - master 7 | - develop 8 | - /^release-.*/ 9 | notifications: 10 | email: 11 | recipients: 12 | - ci.report@ost.com 13 | on_success: always 14 | on_failure: always 15 | node_js: 16 | - "12" 17 | - "10" 18 | # Create a Travis memcached enabled environment. 19 | services: 20 | - "memcached" 21 | before_install: 22 | - sudo apt-get update 23 | - sudo apt-get install nodejs 24 | - sudo apt-get install npm 25 | install: 26 | - npm install 27 | - npm install -g mocha 28 | # Start memcached instances on localhost. 29 | before_script: 30 | - memcached -p 11212 -d 31 | - memcached -p 11213 -d 32 | - memcached -p 11214 -d 33 | - memcached -p 11215 -d 34 | - sudo redis-server /etc/redis/redis.conf --port 6380 --requirepass 'my-secret' 35 | - sudo redis-server /etc/redis/redis.conf --port 6381 --requirepass 'my-secret' 36 | - ps aux | grep 'memcached' 37 | - ps aux | grep 'redis-server' 38 | script: 39 | - source test/env/in-memory.sh 40 | - mocha test --exit 41 | - source test/env/memcached.sh 42 | - mocha test --exit 43 | - source test/env/redis.sh 44 | - mocha test --exit 45 | after_script: "skip" 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ostdotcom/cache", 3 | "version": "1.0.8-beta.6", 4 | "description": "OST Cache is the central cache implementation for OST products.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "index.js", 8 | "pre-commit": "lint-staged" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ostdotcom/cache.git" 13 | }, 14 | "keywords": [ 15 | "OpenST", 16 | "OST", 17 | "Simple Token", 18 | "memcached", 19 | "redis", 20 | "cache" 21 | ], 22 | "author": "OST.com Inc.", 23 | "license": "LGPL-3.0", 24 | "bugs": { 25 | "url": "https://github.com/ostdotcom/cache/issues" 26 | }, 27 | "homepage": "https://github.com/ostdotcom/cache#readme", 28 | "dependencies": { 29 | "memcached": "2.2.2", 30 | "redis": "2.8.0", 31 | "@ostdotcom/base": "^2.0.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "6.5.1", 35 | "chai": "4.2.0", 36 | "ink-docstrap": "1.3.2", 37 | "lint-staged": "8.0.3", 38 | "mocha": "5.2.0", 39 | "pre-commit": "1.2.2", 40 | "prettier": "1.14.3" 41 | }, 42 | "pre-commit": [ 43 | "pre-commit" 44 | ], 45 | "lint-staged": { 46 | "*.js": [ 47 | "prettier --write --config .prettierrc.json", 48 | "git add" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | package-lock.json 14 | 15 | # SDK 16 | /.idea 17 | /.idea/* 18 | .DS_Store 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Dev Folder 46 | .dev 47 | 48 | # Debug log from npm 49 | npm-debug.log 50 | .vscode 51 | 52 | # Typescript v1 declaration files 53 | typings/ 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Cache v1.0.8-beta.2 2 | - Improved logging for memcached errors. 3 | - Change in retry timestamps and counts. 4 | 5 | ## Cache v1.0.7 6 | - Upgraded node version to 10.x 7 | - Version bump for dependencies. 8 | 9 | ## Cache v1.0.6 10 | - Integrated with new Instance Composer. 11 | - Migrated to ES6. 12 | - Migrated repository from OpenST Foundation to OST organization and renamed it. 13 | - Follow common JS style guide followed across all OST repos([Cache#30](https://github.com/ostdotcom/cache/issues/30)). 14 | - We take configuration as OST cache constructor params and then use the config in place of environment variables, where-ever needed ([Cache#29](https://github.com/ostdotcom/cache/issues/29)). 15 | - Application can create different configurations, instantiate cache for each configuration and then communicate with respective (appropriate) cache instance. 16 | - Version bump for dependencies. 17 | 18 | ## Cache v1.0.5 19 | Minor changes. 20 | 21 | ## Cache v1.0.3 22 | - Logger, response helper, promise context, promise queue manager and web3 from OST Base is now used in OST Cache. OST Base repository was created and all the common functionality which different openst modules need were moved to it. 23 | 24 | - Log level support was introduced and non-important logs were moved to debug log level. 25 | 26 | - Standardized error codes are now being used in OST Cache. 27 | -------------------------------------------------------------------------------- /config/cache.js: -------------------------------------------------------------------------------- 1 | const OSTBase = require('@ostdotcom/base'); 2 | 3 | const rootPrefix = '..', 4 | coreConstants = require(rootPrefix + '/config/coreConstant'); 5 | 6 | const InstanceComposer = OSTBase.InstanceComposer; 7 | 8 | /** 9 | * Class for cache config helper. 10 | * 11 | * @class CacheConfigHelper 12 | */ 13 | class CacheConfigHelper { 14 | /** 15 | * Constructor for cache config helper. 16 | * 17 | * @param {object} configStrategy 18 | * @param {object} configStrategy.cache 19 | * @param {number} configStrategy.cache.defaultTtl 20 | * @param {string} configStrategy.cache.host 21 | * @param {string/number} configStrategy.cache.port 22 | * @param {string} configStrategy.cache.password 23 | * @param {string/number} configStrategy.cache.enableTsl 24 | * @param {array} [configStrategy.cache.servers] 25 | * @param {string/number} configStrategy.DEBUG_ENABLED 26 | * @param {object} instanceComposer 27 | * 28 | * @constructor 29 | */ 30 | // eslint-disable-next-line no-unused-vars 31 | constructor(configStrategy, instanceComposer) { 32 | const oThis = this; 33 | 34 | oThis.DEFAULT_TTL = configStrategy.cache.defaultTtl; 35 | oThis.REDIS_HOST = configStrategy.cache.host; 36 | oThis.REDIS_PORT = configStrategy.cache.port; 37 | oThis.REDIS_PASS = configStrategy.cache.password; 38 | oThis.REDIS_TLS_ENABLED = configStrategy.cache.enableTsl == '1'; 39 | oThis.MEMCACHE_SERVERS = (configStrategy.cache.servers || []).map(Function.prototype.call, String.prototype.trim); 40 | oThis.DEBUG_ENABLED = configStrategy.DEBUG_ENABLED; 41 | 42 | oThis.INMEMORY_CACHE_NAMESPACE = configStrategy.cache.namespace || ''; 43 | } 44 | } 45 | 46 | InstanceComposer.registerAsObject(CacheConfigHelper, coreConstants.icNameSpace, 'CacheConfigHelper', true); 47 | 48 | module.exports = CacheConfigHelper; 49 | -------------------------------------------------------------------------------- /config/error/general.json: -------------------------------------------------------------------------------- 1 | { 2 | "something_went_wrong": { 3 | "http_code": "422", 4 | "code": "UNPROCESSABLE_ENTITY", 5 | "message": "Something went wrong." 6 | }, 7 | 8 | "missing_cache_key": { 9 | "http_code": "422", 10 | "code": "UNPROCESSABLE_ENTITY", 11 | "message": "Cache key does not exist." 12 | }, 13 | 14 | "invalid_cache_key": { 15 | "http_code": "422", 16 | "code": "UNPROCESSABLE_ENTITY", 17 | "message": "Cache key validation failed." 18 | }, 19 | 20 | "invalid_cache_value": { 21 | "http_code": "422", 22 | "code": "UNPROCESSABLE_ENTITY", 23 | "message": "Cache value validation failed." 24 | }, 25 | 26 | "array_is_invalid_cache_value": { 27 | "http_code": "422", 28 | "code": "UNPROCESSABLE_ENTITY", 29 | "message": "Cache value cannot be array." 30 | }, 31 | 32 | "cache_keys_non_array": { 33 | "http_code": "422", 34 | "code": "UNPROCESSABLE_ENTITY", 35 | "message": "Cache keys should be an array." 36 | }, 37 | 38 | "non_numeric_cache_value": { 39 | "http_code": "422", 40 | "code": "UNPROCESSABLE_ENTITY", 41 | "message": "Cache value validation failed." 42 | }, 43 | 44 | "non_int_cache_value": { 45 | "http_code": "422", 46 | "code": "UNPROCESSABLE_ENTITY", 47 | "message": "Cache value is not an integer." 48 | }, 49 | 50 | "cache_expiry_nan": { 51 | "http_code": "422", 52 | "code": "UNPROCESSABLE_ENTITY", 53 | "message": "Cache expiry is not a valid number." 54 | }, 55 | 56 | "flush_all_not_supported": { 57 | "http_code": "422", 58 | "code": "UNPROCESSABLE_ENTITY", 59 | "message": "Flush all Cache Keys can not be supported by In-Memory Cache. Please Restart your service to flush cache." 60 | }, 61 | 62 | "flush_all_keys_failed": { 63 | "http_code": "422", 64 | "code": "UNPROCESSABLE_ENTITY", 65 | "message": "Flush all Cache Keys failed." 66 | }, 67 | 68 | "acquire_lock_failed": { 69 | "http_code": "422", 70 | "code": "UNPROCESSABLE_ENTITY", 71 | "message": "Acquire Lock failed. Please try again after sometime." 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /lib/cache/helper.js: -------------------------------------------------------------------------------- 1 | const rootPrefix = '../..', 2 | logger = require(rootPrefix + '/lib/logger/customConsoleLogger'), 3 | generalErrorConfig = require(rootPrefix + '/config/error/general'); 4 | 5 | /** 6 | * Class for cache helper. 7 | * 8 | * @class CacheHelper 9 | */ 10 | class CacheHelper { 11 | // Check if cache key is valid or not 12 | validateCacheKey(key) { 13 | const oThis = this; 14 | 15 | if (typeof key !== 'string') { 16 | logger.error(`Cache key not a string: ${key}`); 17 | 18 | return false; 19 | } 20 | 21 | if (key === '') { 22 | logger.error(`Cache key should not be blank: ${key}`); 23 | 24 | return false; 25 | } 26 | 27 | if (oThis._validateCacheValueSize(key, 250) !== true) { 28 | logger.error( 29 | `Cache key byte size should not be > 250. Key: ${key}. Size: ${oThis._validateCacheValueSize(key, 250)}` 30 | ); 31 | 32 | return false; 33 | } 34 | 35 | if (oThis._validateCacheKeyChars(key) !== true) { 36 | logger.error(`Cache key has unsupported chars: ${key}`); 37 | 38 | return false; 39 | } 40 | 41 | return true; 42 | } 43 | 44 | /** 45 | * Check if cache value is valid or not. 46 | * 47 | * @param {*} value 48 | * 49 | * @returns {boolean} 50 | */ 51 | validateCacheValue(value) { 52 | const oThis = this; 53 | 54 | return value !== undefined && oThis._validateCacheValueSize(value, 1024 * 1024) === true; 55 | } 56 | 57 | /** 58 | * Check if cache expiry is valid or not. 59 | * 60 | * @param {number} value 61 | * 62 | * @returns {boolean} 63 | */ 64 | validateCacheExpiry(value) { 65 | return !!(value && typeof value === 'number'); 66 | } 67 | 68 | /** 69 | * Fetch error config. 70 | * 71 | * @returns {{api_error_config: *, param_error_config: {}}} 72 | */ 73 | fetchErrorConfig() { 74 | return { 75 | param_error_config: {}, 76 | api_error_config: generalErrorConfig 77 | }; 78 | } 79 | 80 | /** 81 | * Check if cache value size is < size. 82 | * 83 | * @param {string} value 84 | * @param {number} size 85 | * 86 | * @returns {boolean} 87 | * @private 88 | */ 89 | _validateCacheValueSize(value, size) { 90 | return Buffer.byteLength(JSON.stringify(value), 'utf8') <= size; 91 | } 92 | 93 | // Check key has valid chars. 94 | /** 95 | * Check key has valid chars. 96 | * 97 | * @param {string} key 98 | * 99 | * @returns {boolean} 100 | * @private 101 | */ 102 | _validateCacheKeyChars(key) { 103 | return !/\s/.test(key); 104 | } 105 | } 106 | 107 | module.exports = new CacheHelper(); 108 | -------------------------------------------------------------------------------- /test/cache_del.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy; 11 | if (testCachingEngine === 'redis') { 12 | configStrategy = require(rootPrefix + '/test/env/redis.json'); 13 | } else if (testCachingEngine === 'memcached') { 14 | configStrategy = require(rootPrefix + '/test/env/memcached.json'); 15 | } else if (testCachingEngine === 'none') { 16 | configStrategy = require(rootPrefix + '/test/env/in-memory.json'); 17 | } 18 | 19 | const engineType = configStrategy.cache.engine; 20 | 21 | function performTest(cacheObj, keySuffix) { 22 | describe('Cache Del ' + keySuffix, function() { 23 | keySuffix = keySuffix + '_' + new Date().getTime(); 24 | 25 | it('should return promise', async function() { 26 | let cKey = 'cache-key' + keySuffix, 27 | response = cacheObj.del(cKey); 28 | assert.typeOf(response, 'Promise'); 29 | }); 30 | 31 | it('should fail when key/value is not passed', async function() { 32 | let response = await cacheObj.del(); 33 | assert.equal(response.isSuccess(), false); 34 | }); 35 | 36 | it('should fail when key is undefined', async function() { 37 | let response = await cacheObj.del(undefined); 38 | assert.equal(response.isSuccess(), false); 39 | }); 40 | 41 | it('should fail when key is blank', async function() { 42 | let cKey = '', 43 | response = await cacheObj.del(cKey); 44 | assert.equal(response.isSuccess(), false); 45 | }); 46 | 47 | it('should fail when key is number', async function() { 48 | let cKey = 10, 49 | response = await cacheObj.del(cKey); 50 | assert.equal(response.isSuccess(), false); 51 | }); 52 | 53 | it('should fail when key has space', async function() { 54 | let cKey = 'a b' + keySuffix, 55 | response = await cacheObj.del(cKey); 56 | assert.equal(response.isSuccess(), false); 57 | }); 58 | 59 | it('should fail when key length is > 250 bytes', async function() { 60 | let cKey = Array(252).join('x'), 61 | response = await cacheObj.del(cKey); 62 | assert.equal(response.isSuccess(), false); 63 | }); 64 | 65 | it('should pass when key is not set', async function() { 66 | let cKey = 'cache-key-not-key-del' + keySuffix, 67 | response = await cacheObj.del(cKey); 68 | assert.equal(response.isSuccess(), true); 69 | assert.equal(response.data.response, true); 70 | }); 71 | 72 | it('should pass when key is set', async function() { 73 | let cKey = 'cache-key' + keySuffix, 74 | cValue = 'String Value', 75 | responseSet = await cacheObj.set(cKey, cValue), 76 | response = await cacheObj.del(cKey); 77 | assert.equal(response.isSuccess(), true); 78 | assert.equal(response.data.response, true); 79 | }); 80 | }); 81 | } 82 | 83 | ostCache = OSTCache.getInstance(configStrategy); 84 | cacheImplementer = ostCache.cacheInstance; 85 | 86 | performTest(cacheImplementer, 'ConsistentBehaviour'); 87 | performTest(cacheImplementer, 'InconsistentBehaviour'); 88 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true 6 | }, 7 | extends: 'eslint:recommended', 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly' 11 | }, 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | sourceType: 'module' 15 | }, 16 | rules: { 17 | 'linebreak-style': ['error', 'unix'], 18 | quotes: ['error', 'single'], 19 | semi: ['error', 'always'], 20 | 'no-var': 'error', 21 | 'block-scoped-var': 'error', 22 | curly: 'error', 23 | 'default-case': 'error', 24 | 'dot-notation': 'error', 25 | 'no-empty-function': 'error', 26 | 'no-else-return': 'error', 27 | 'no-eval': 'error', 28 | 'no-extra-bind': 'error', 29 | 'no-extra-label': 'error', 30 | 'no-implied-eval': 'error', 31 | 'no-invalid-this': 'error', 32 | 'no-multi-spaces': 'error', 33 | 'no-new': 'error', 34 | 'no-new-func': 'error', 35 | 'no-new-wrappers': 'error', 36 | 'no-return-assign': 'error', 37 | 'no-return-await': 'error', 38 | 'no-self-compare': 'error', 39 | 'no-sequences': 'error', 40 | 'no-throw-literal': 'error', 41 | 'no-useless-call': 'error', 42 | 'no-useless-catch': 'error', 43 | 'no-useless-concat': 'error', 44 | 'no-with': 'error', 45 | 'no-void': 'error', 46 | 'prefer-promise-reject-errors': 'error', 47 | yoda: 'error', 48 | 'prefer-const': 'error', 49 | //"prefer-arrow-callback": "error", 50 | 'handle-callback-err': 'error', 51 | 'no-async-promise-executor': 'error', 52 | 'no-case-declarations': 'error', 53 | 'no-shadow': 'error', 54 | 'no-undef-init': 'error', 55 | 'no-undefined': 'error', 56 | 'no-use-before-define': 'error', 57 | 'no-new-require': 'error', 58 | 'no-process-env': 'error', 59 | // 'no-sync': 'error', 60 | strict: 'error', 61 | 'arrow-spacing': 'error', 62 | 'no-floating-decimal': 'error', 63 | 'no-buffer-constructor': 'error', 64 | // "no-mixed-requires": ["error", { "grouping": true }], 65 | 'max-classes-per-file': ['error', 1], 66 | 'arrow-body-style': ['error', 'as-needed'], 67 | 'no-confusing-arrow': ['error', { allowParens: true }], 68 | 'no-duplicate-imports': 'error', 69 | 'no-useless-computed-key': 'error', 70 | 'no-useless-constructor': 'error', 71 | 'no-useless-rename': 'error', 72 | // "prefer-destructuring": "error", 73 | // "prefer-template": "error", 74 | 'capitalized-comments': ['error'], 75 | 'consistent-this': ['error', 'oThis'], 76 | 'eol-last': ['error', 'always'], 77 | 'id-length': ['error', { min: 2 }], 78 | 'key-spacing': ['error', { beforeColon: false, afterColon: true }], 79 | 'lines-around-comment': ['error', { beforeBlockComment: false, beforeLineComment: false }], 80 | 'lines-between-class-members': ['error', 'always'], 81 | 'no-array-constructor': 'error', 82 | 'max-depth': ['error', 4], 83 | 'max-lines-per-function': ['error', { max: 80, skipBlankLines: true, skipComments: true }], 84 | 'no-lonely-if': 'error', 85 | 'no-multiple-empty-lines': 'error', 86 | 'no-negated-condition': 'error', 87 | 'no-new-object': 'error', 88 | 'no-trailing-spaces': 'error', 89 | 'no-unneeded-ternary': 'error', 90 | 'operator-assignment': ['error', 'always'], 91 | 'padding-line-between-statements': ['error', { blankLine: 'always', prev: '*', next: 'return' }], 92 | 'spaced-comment': ['error', 'always', { exceptions: ['-', '+'] }] 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /test/cache_getobject.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy; 11 | if (testCachingEngine === 'redis') { 12 | configStrategy = require(rootPrefix + '/test/env/redis.json'); 13 | } else if (testCachingEngine === 'memcached') { 14 | configStrategy = require(rootPrefix + '/test/env/memcached.json'); 15 | } else if (testCachingEngine === 'none') { 16 | configStrategy = require(rootPrefix + '/test/env/in-memory.json'); 17 | } 18 | 19 | const engineType = configStrategy.cache.engine; 20 | 21 | function performTest(cacheObj, keySuffix) { 22 | describe('Cache GetObject ' + keySuffix, function() { 23 | keySuffix = keySuffix + '_' + new Date().getTime(); 24 | 25 | it('should return promise', async function() { 26 | let cKey = 'cache-key-object' + keySuffix, 27 | response = cacheObj.getObject(cKey); 28 | assert.typeOf(response, 'Promise'); 29 | }); 30 | 31 | it('should fail when key/value is not passed', async function() { 32 | let response = await cacheObj.getObject(); 33 | assert.equal(response.isSuccess(), false); 34 | }); 35 | 36 | it('should fail when key is undefined', async function() { 37 | let response = await cacheObj.getObject(undefined); 38 | assert.equal(response.isSuccess(), false); 39 | }); 40 | 41 | it('should fail when key is blank', async function() { 42 | let cKey = '', 43 | response = await cacheObj.getObject(cKey); 44 | assert.equal(response.isSuccess(), false); 45 | }); 46 | 47 | it('should fail when key is number', async function() { 48 | let cKey = 10, 49 | response = await cacheObj.getObject(cKey); 50 | assert.equal(response.isSuccess(), false); 51 | }); 52 | 53 | it('should fail when key has space', async function() { 54 | let cKey = 'a b' + keySuffix, 55 | response = await cacheObj.getObject(cKey); 56 | assert.equal(response.isSuccess(), false); 57 | }); 58 | 59 | it('should fail when key length is > 250 bytes', async function() { 60 | let cKey = Array(252).join('x'), 61 | response = await cacheObj.getObject(cKey); 62 | assert.equal(response.isSuccess(), false); 63 | }); 64 | 65 | it('should pass when value is not set', async function() { 66 | let cKey = 'cache-key-not-set' + keySuffix, 67 | response = await cacheObj.getObject(cKey); 68 | assert.equal(response.isSuccess(), true); 69 | assert.equal(response.data.response, null); 70 | }); 71 | 72 | it('should pass when value is object', async function() { 73 | let cKey = 'cache-key-object' + keySuffix, 74 | cValue = { a: 'a' }, 75 | responseSet = await cacheObj.setObject(cKey, cValue), 76 | response = await cacheObj.getObject(cKey); 77 | assert.equal(response.isSuccess(), true); 78 | assert.equal(typeof response.data.response, typeof cValue); 79 | assert.equal(JSON.stringify(response.data.response), JSON.stringify(cValue)); 80 | }); 81 | 82 | it('should pass when value is complex object', async function() { 83 | let cKey = 'cache-key-object' + keySuffix, 84 | cValue = { a: 'a', b: [12, 23], c: true, d: 1, e: { f: 'hi', g: 1 } }, 85 | responseSet = await cacheObj.setObject(cKey, cValue), 86 | response = await cacheObj.getObject(cKey); 87 | assert.equal(response.isSuccess(), true); 88 | assert.equal(typeof response.data.response, typeof cValue); 89 | assert.equal(JSON.stringify(response.data.response), JSON.stringify(cValue)); 90 | }); 91 | }); 92 | } 93 | 94 | ostCache = OSTCache.getInstance(configStrategy); 95 | cacheImplementer = ostCache.cacheInstance; 96 | 97 | performTest(cacheImplementer, 'ConsistentBehaviour'); 98 | performTest(cacheImplementer, 'InconsistentBehaviour'); 99 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Index File for @ostdotcom/cache 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const rootPrefix = '.', 8 | version = require(rootPrefix + '/package.json').version, 9 | OSTBase = require('@ostdotcom/base'), 10 | coreConstant = require(rootPrefix + '/config/coreConstant'); 11 | 12 | const InstanceComposer = OSTBase.InstanceComposer; 13 | 14 | require(rootPrefix + '/services/CacheInstance'); 15 | 16 | const OSTCache = function(configStrategy) { 17 | const oThis = this; 18 | 19 | if (!configStrategy) { 20 | throw 'Mandatory argument configStrategy missing'; 21 | } 22 | 23 | const instanceComposer = new InstanceComposer(configStrategy); 24 | oThis.ic = function() { 25 | return instanceComposer; 26 | }; 27 | }; 28 | 29 | OSTCache.prototype = { 30 | version: version 31 | }; 32 | 33 | Object.defineProperty(OSTCache.prototype, 'cacheInstance', { 34 | get: function() { 35 | const oThis = this; 36 | return oThis.ic().getInstanceFor(coreConstant.icNameSpace, 'getCacheInstance'); 37 | } 38 | }); 39 | 40 | const instanceMap = {}; 41 | 42 | /** 43 | * Creates the key for the instanceMap. 44 | * 45 | * @returns {string} 46 | * 47 | */ 48 | const getInstanceKey = function(configStrategy) { 49 | if (!configStrategy.hasOwnProperty('cache') || !configStrategy.cache.hasOwnProperty('engine')) { 50 | throw 'CACHING_ENGINE parameter is missing.'; 51 | } 52 | if (configStrategy.cache.engine === undefined) { 53 | throw 'CACHING_ENGINE parameter is empty.'; 54 | } 55 | 56 | // Grab the required details from the configStrategy. 57 | const cacheEngine = configStrategy.cache.engine.toString(); 58 | let isConsistentBehaviour = configStrategy.cache.consistentBehavior; 59 | 60 | // Sanitize isConsistentBehaviour 61 | isConsistentBehaviour = isConsistentBehaviour === undefined ? true : isConsistentBehaviour != '0'; 62 | 63 | // Stores the endpoint for key generation of instanceMap. 64 | let endpointDetails = null; 65 | 66 | // Generate endpointDetails for key generation of instanceMap. 67 | if (cacheEngine == 'redis') { 68 | const redisMandatoryParams = ['host', 'port', 'password', 'enableTsl']; 69 | 70 | // Check if all the mandatory connection parameters for Redis are available or not. 71 | for (let i = 0; i < redisMandatoryParams.length; i++) { 72 | if (!configStrategy.cache.hasOwnProperty(redisMandatoryParams[i])) { 73 | throw 'Redis - mandatory connection parameters missing.'; 74 | } 75 | if (configStrategy.cache[redisMandatoryParams[i]] === undefined) { 76 | throw 'Redis - connection parameters are empty.'; 77 | } 78 | } 79 | 80 | endpointDetails = 81 | configStrategy.cache.host.toLowerCase() + 82 | '-' + 83 | configStrategy.cache.port.toString() + 84 | '-' + 85 | configStrategy.cache.enableTsl.toString(); 86 | } else if (cacheEngine == 'memcached') { 87 | if (!configStrategy.cache.hasOwnProperty('servers')) { 88 | throw 'Memcached - mandatory connection parameters missing.'; 89 | } 90 | if (configStrategy.cache.servers === undefined) { 91 | throw 'MEMCACHE_SERVERS(configStrategy.cache.servers) parameter is empty. '; 92 | } 93 | endpointDetails = configStrategy.cache.servers.join(',').toLowerCase(); 94 | } else { 95 | endpointDetails = configStrategy.cache.namespace || ''; 96 | } 97 | 98 | return cacheEngine + '-' + isConsistentBehaviour.toString() + '-' + endpointDetails; 99 | }; 100 | 101 | let Factory = function() {}; 102 | 103 | Factory.prototype = { 104 | /** 105 | * Fetches a cache instance if available in instanceMap. If instance is not available in 106 | * instanceMap, it calls createCacheInstance() to create a new cache instance. 107 | * 108 | * @returns {cacheInstance} 109 | * 110 | */ 111 | getInstance: function(configStrategy) { 112 | let instanceKey = getInstanceKey(configStrategy); 113 | 114 | let _instance = instanceMap[instanceKey]; 115 | 116 | if (!_instance) { 117 | _instance = new OSTCache(configStrategy); 118 | instanceMap[instanceKey] = _instance; 119 | } 120 | 121 | return _instance; 122 | } 123 | }; 124 | 125 | const factory = new Factory(); 126 | OSTCache.getInstance = function() { 127 | return factory.getInstance.apply(factory, arguments); 128 | }; 129 | 130 | module.exports = OSTCache; 131 | -------------------------------------------------------------------------------- /test/cache_multiget.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy; 11 | if (testCachingEngine === 'redis') { 12 | configStrategy = require(rootPrefix + '/test/env/redis.json'); 13 | } else if (testCachingEngine === 'memcached') { 14 | configStrategy = require(rootPrefix + '/test/env/memcached.json'); 15 | } else if (testCachingEngine === 'none') { 16 | configStrategy = require(rootPrefix + '/test/env/in-memory.json'); 17 | } 18 | 19 | const engineType = configStrategy.cache.engine; 20 | 21 | function performTest(cacheObj, keySuffix) { 22 | describe('Cache MultiGet ' + keySuffix, function() { 23 | keySuffix = keySuffix + '_' + new Date().getTime(); 24 | 25 | it('should return promise', async function() { 26 | let cKey = ['cache-key' + keySuffix], 27 | response = cacheObj.multiGet(cKey); 28 | assert.typeOf(response, 'Promise'); 29 | }); 30 | 31 | it('should fail when key is not passed', async function() { 32 | let response = await cacheObj.multiGet(); 33 | assert.equal(response.isSuccess(), false); 34 | }); 35 | 36 | it('should fail when key is empty array', async function() { 37 | let response = await cacheObj.multiGet([]); 38 | assert.equal(response.isSuccess(), false); 39 | }); 40 | 41 | it('should fail when key is undefined', async function() { 42 | let response = await cacheObj.multiGet([undefined]); 43 | assert.equal(response.isSuccess(), false); 44 | }); 45 | 46 | it('should fail when key is blank', async function() { 47 | let cKey = [''], 48 | response = await cacheObj.multiGet(cKey); 49 | assert.equal(response.isSuccess(), false); 50 | }); 51 | 52 | it('should fail when key is number', async function() { 53 | let cKey = [10], 54 | response = await cacheObj.multiGet(cKey); 55 | assert.equal(response.isSuccess(), false); 56 | }); 57 | 58 | it('should fail when key has space', async function() { 59 | let cKey = ['a b' + keySuffix], 60 | response = await cacheObj.multiGet(cKey); 61 | assert.equal(response.isSuccess(), false); 62 | }); 63 | 64 | it('should fail when key length is > 250 bytes', async function() { 65 | let cKey = [Array(252).join('x')], 66 | response = await cacheObj.multiGet(cKey); 67 | assert.equal(response.isSuccess(), false); 68 | }); 69 | 70 | it('should pass when value is not set', async function() { 71 | let cKey = ['cache-key-not-set' + keySuffix], 72 | response = await cacheObj.multiGet(cKey); 73 | assert.equal(response.isSuccess(), true); 74 | assert.equal(response.data.response['cache-key-not-set'], null); 75 | }); 76 | 77 | it('should pass when non object values are get', async function() { 78 | let keyValue = {}; 79 | 80 | keyValue[`cache-key-string${keySuffix}`] = 'String Value'; 81 | keyValue[`cache-key-integer${keySuffix}`] = 10; 82 | keyValue[`cache-key-blank${keySuffix}`] = ''; 83 | 84 | for (let key in keyValue) { 85 | res = await cacheObj.set(key, keyValue[key]); 86 | } 87 | 88 | let lookupKeys = Object.keys(keyValue), 89 | response = await cacheObj.multiGet(lookupKeys); 90 | 91 | assert.equal(response.isSuccess(), true); 92 | for (let key in response.data.response) { 93 | assert.equal(response.data.response[key], keyValue[key]); 94 | } 95 | }); 96 | 97 | it('should return null when object values are get', async function() { 98 | let keyValue = {}; 99 | keyValue[`cache-key-array${keySuffix}`] = [1, 2, 3, 4]; 100 | keyValue[`cache-key-object${keySuffix}`] = { a: 1 }; 101 | 102 | for (let key in keyValue) { 103 | res = await cacheObj.set(key, keyValue[key]); 104 | } 105 | 106 | let lookupKeys = Object.keys(keyValue), 107 | response = await cacheObj.multiGet(lookupKeys); 108 | 109 | assert.equal(response.isSuccess(), true); 110 | for (let key in response.data.response) { 111 | assert.equal(response.data.response[key], null); 112 | } 113 | }); 114 | }); 115 | } 116 | 117 | ostCache = OSTCache.getInstance(configStrategy); 118 | cacheImplementer = ostCache.cacheInstance; 119 | 120 | performTest(cacheImplementer, 'ConsistentBehaviour'); 121 | performTest(cacheImplementer, 'InconsistentBehaviour'); 122 | -------------------------------------------------------------------------------- /test/cache_release_lock.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy1; 11 | let configStrategy2; 12 | if (testCachingEngine === 'redis') { 13 | configStrategy1 = require(rootPrefix + '/test/env/redis.json'); 14 | configStrategy2 = require(rootPrefix + '/test/env/redis2.json'); 15 | } else if (testCachingEngine === 'memcached') { 16 | configStrategy1 = require(rootPrefix + '/test/env/memcached.json'); 17 | configStrategy2 = require(rootPrefix + '/test/env/memcached2.json'); 18 | } else if (testCachingEngine === 'none') { 19 | configStrategy1 = require(rootPrefix + '/test/env/in-memory.json'); 20 | configStrategy2 = require(rootPrefix + '/test/env/in-memory2.json'); 21 | } 22 | 23 | let defaultTtl = 1000; // in seconds 24 | 25 | function performTest(cacheObj) { 26 | let keySuffix = '_' + new Date().getTime(); 27 | 28 | describe('Cache Release Lock ' + keySuffix, function() { 29 | it('should return promise', async function() { 30 | let cKey = 'cache-key-rl' + keySuffix, 31 | response = cacheObj.releaseLock(cKey); 32 | assert.typeOf(response, 'Promise'); 33 | }); 34 | 35 | it('should fail when key is not passed', async function() { 36 | let response = await cacheObj.releaseLock(); 37 | assert.equal(response.isSuccess(), false); 38 | }); 39 | 40 | it('should fail when key is undefined', async function() { 41 | let response = await cacheObj.releaseLock(undefined); 42 | assert.equal(response.isSuccess(), false); 43 | }); 44 | 45 | it('should fail when key is blank', async function() { 46 | let cKey = '', 47 | response = await cacheObj.releaseLock(cKey); 48 | assert.equal(response.isSuccess(), false); 49 | }); 50 | 51 | it('should fail when key is number', async function() { 52 | let cKey = 10, 53 | response = await cacheObj.releaseLock(cKey); 54 | assert.equal(response.isSuccess(), false); 55 | }); 56 | 57 | it('should fail when key has space', async function() { 58 | let cKey = 'a b' + keySuffix, 59 | response = await cacheObj.releaseLock(cKey); 60 | assert.equal(response.isSuccess(), false); 61 | }); 62 | 63 | it('should fail when key length is > 250 bytes', async function() { 64 | let cKey = Array(252).join('x'), 65 | response = await cacheObj.releaseLock(cKey); 66 | assert.equal(response.isSuccess(), false); 67 | }); 68 | 69 | it('should be able to reacquire lock after releasing', async function() { 70 | let cKey = 'cache-key-rel' + keySuffix, 71 | ttl = defaultTtl, 72 | response; 73 | 74 | // acquireLock 75 | response = await cacheObj.acquireLock(cKey, ttl); 76 | assert.equal(response.isSuccess(), true); 77 | assert.equal(response.data.response, true); 78 | 79 | // releaseLock 80 | response = await cacheObj.releaseLock(cKey); 81 | assert.equal(response.isSuccess(), true); 82 | assert.equal(response.data.response, true); 83 | 84 | // retry to acquire lock after it was released 85 | response = await cacheObj.acquireLock(cKey, ttl); 86 | assert.equal(response.isSuccess(), true); 87 | assert.equal(response.data.response, true); 88 | }); 89 | }); 90 | } 91 | 92 | function performMultipleTest(cacheObj1, cacheObj2) { 93 | let keySuffix = '_' + new Date().getTime(); 94 | 95 | describe('Cache Acquire Lock Across Multiple Cache Instances ' + keySuffix, function() { 96 | keySuffix = keySuffix + '_' + new Date().getTime(); 97 | 98 | it('should be able to reacquire lock on other cache instance even before ttl', async function() { 99 | let cKey1 = 'cache-key-mll1' + keySuffix, 100 | ttl = 6, // seconds 101 | response1 = await cacheObj1.releaseLock(cKey1, ttl), 102 | cKey2 = 'cache-key-mll2' + keySuffix, 103 | response2 = await cacheObj2.releaseLock(cKey2, ttl); 104 | assert.equal(response1.isSuccess(), true); 105 | assert.equal(response1.data.response, true); 106 | assert.equal(response2.isSuccess(), true); 107 | assert.equal(response2.data.response, true); 108 | }); 109 | }); 110 | } 111 | 112 | ostCache1 = OSTCache.getInstance(configStrategy1); 113 | cacheImplementer1 = ostCache1.cacheInstance; 114 | 115 | ostCache2 = OSTCache.getInstance(configStrategy2); 116 | cacheImplementer2 = ostCache2.cacheInstance; 117 | 118 | performTest(cacheImplementer1); 119 | 120 | performMultipleTest(cacheImplementer1, cacheImplementer2); 121 | -------------------------------------------------------------------------------- /test/cache_acquire_lock.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy1; 11 | let configStrategy2; 12 | if (testCachingEngine === 'redis') { 13 | configStrategy1 = require(rootPrefix + '/test/env/redis.json'); 14 | configStrategy2 = require(rootPrefix + '/test/env/redis2.json'); 15 | } else if (testCachingEngine === 'memcached') { 16 | configStrategy1 = require(rootPrefix + '/test/env/memcached.json'); 17 | configStrategy2 = require(rootPrefix + '/test/env/memcached2.json'); 18 | } else if (testCachingEngine === 'none') { 19 | configStrategy1 = require(rootPrefix + '/test/env/in-memory.json'); 20 | configStrategy2 = require(rootPrefix + '/test/env/in-memory2.json'); 21 | } 22 | 23 | let defaultTtl = 1000; // in seconds 24 | 25 | function performTest(cacheObj) { 26 | let keySuffix = '_' + new Date().getTime(); 27 | 28 | describe('Cache Acquire Lock ' + keySuffix, function() { 29 | it('should return promise', async function() { 30 | let cKey = 'cache-key-rp' + keySuffix, 31 | ttl = defaultTtl, 32 | response = cacheObj.acquireLock(cKey, ttl); 33 | assert.typeOf(response, 'Promise'); 34 | }); 35 | 36 | it('should return response true', async function() { 37 | let cKey = 'cache-key-rpt' + keySuffix, 38 | response = await cacheObj.acquireLock(cKey, defaultTtl); 39 | assert.equal(response.isSuccess(), true); 40 | assert.equal(response.data.response, true); 41 | }); 42 | 43 | it('should fail when key is not passed', async function() { 44 | let response = await cacheObj.acquireLock(); 45 | assert.equal(response.isSuccess(), false); 46 | }); 47 | 48 | it('should fail when key is undefined', async function() { 49 | let ttl = defaultTtl, 50 | response = await cacheObj.acquireLock(undefined, ttl); 51 | assert.equal(response.isSuccess(), false); 52 | }); 53 | 54 | it('should fail when key is blank', async function() { 55 | let cKey = '', 56 | ttl = defaultTtl, 57 | response = await cacheObj.acquireLock(cKey, ttl); 58 | assert.equal(response.isSuccess(), false); 59 | }); 60 | 61 | it('should fail when key is number', async function() { 62 | let cKey = 10, 63 | ttl = defaultTtl, 64 | response = await cacheObj.acquireLock(cKey, ttl); 65 | assert.equal(response.isSuccess(), false); 66 | }); 67 | 68 | it('should fail when key has space', async function() { 69 | let cKey = 'a b' + keySuffix, 70 | ttl = defaultTtl, 71 | response = await cacheObj.acquireLock(cKey, ttl); 72 | assert.equal(response.isSuccess(), false); 73 | }); 74 | 75 | it('should fail when key length is > 250 bytes', async function() { 76 | let cKey = Array(252).join('x'), 77 | ttl = defaultTtl, 78 | response = await cacheObj.acquireLock(cKey, ttl); 79 | assert.equal(response.isSuccess(), false); 80 | }); 81 | 82 | it('should auto release lock after ttl', async function() { 83 | let cKey = 'cache-key-arl' + keySuffix, 84 | ttl = 6, // seconds 85 | response = await cacheObj.acquireLock(cKey, ttl); 86 | assert.equal(response.isSuccess(), true); 87 | assert.equal(response.data.response, true); 88 | setTimeout(async function() { 89 | response = await cacheObj.acquireLock(cKey, ttl); 90 | assert.equal(response.isSuccess(), true); 91 | assert.equal(response.data.response, true); 92 | }, ttl * 1000); 93 | }); 94 | 95 | it('should not be able to require lock before ttl', async function() { 96 | // acquire lock 97 | let cKey = 'cache-key-brl' + keySuffix, 98 | ttl = 6, // seconds 99 | response = await cacheObj.acquireLock(cKey, ttl); 100 | assert.equal(response.isSuccess(), true); 101 | assert.equal(response.data.response, true); 102 | 103 | // retry to acquire lock before ttl. should fail 104 | response = await cacheObj.acquireLock(cKey, ttl); 105 | assert.equal(response.isSuccess(), false); 106 | }); 107 | }); 108 | } 109 | 110 | function performMultipleTest(cacheObj1, cacheObj2) { 111 | let keySuffix = '_' + new Date().getTime(); 112 | 113 | describe('Cache Acquire Lock Across Multiple Cache Instances ' + keySuffix, function() { 114 | keySuffix = keySuffix + '_' + new Date().getTime(); 115 | 116 | it('should be able to reacquire lock on other cache instance even before ttl', async function() { 117 | let cKey1 = 'cache-key-mll1' + keySuffix, 118 | ttl = 6, // seconds 119 | response1 = await cacheObj1.acquireLock(cKey1, ttl), 120 | cKey2 = 'cache-key-mll2' + keySuffix, 121 | response2 = await cacheObj2.acquireLock(cKey2, ttl); 122 | assert.equal(response1.isSuccess(), true); 123 | assert.equal(response1.data.response, true); 124 | assert.equal(response2.isSuccess(), true); 125 | assert.equal(response2.data.response, true); 126 | }); 127 | }); 128 | } 129 | 130 | ostCache1 = OSTCache.getInstance(configStrategy1); 131 | cacheImplementer1 = ostCache1.cacheInstance; 132 | 133 | ostCache2 = OSTCache.getInstance(configStrategy2); 134 | cacheImplementer2 = ostCache2.cacheInstance; 135 | 136 | performTest(cacheImplementer1); 137 | 138 | performMultipleTest(cacheImplementer1, cacheImplementer2); 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cache 2 | ============ 3 | [![Latest version](https://img.shields.io/npm/v/@ostdotcom/cache.svg?maxAge=3600)][npm] 4 | [![Build Status](https://travis-ci.org/ostdotcom/cache.svg?branch=develop)][travis] 5 | [![Downloads per month](https://img.shields.io/npm/dm/@ostdotcom/cache.svg?maxAge=3600)][npm] 6 | 7 | [npm]: https://www.npmjs.com/package/@ostdotcom/cache 8 | [travis]: https://travis-ci.org/ostdotcom/cache 9 | 10 | OST Cache is the central cache implementation for all OST products and can easily be plugged-in. 11 | 12 | It contains three caching engines. The decision of which caching engine to use is governed while creating the cache object. 13 | The caching engines implemented are: 14 | 15 | * Memcached 16 | * Redis 17 | * In-process (use with single threaded process in development mode only) 18 | 19 | ##### Constructor parameters: 20 | There is 1 parameter required while creating the cache implementer. 21 | 22 | * First parameter is mandatory and it specifies the configuration strategy to be used. An example of the configStrategy is: 23 | ```js 24 | configStrategy = { 25 | cache: { 26 | engine: "none/redis/memcache" 27 | } 28 | }; 29 | ``` 30 | 31 | Below are the examples: 32 | ```js 33 | // import the cache module 34 | const OSTCache = require('@ostdotcom/cache'); 35 | ``` 36 | ```js 37 | // configStrategy for redis engine 38 | configStrategy = { 39 | cache: { 40 | engine: "redis", 41 | host: "localhost", 42 | port: "6830", 43 | password: "dsdsdsd", 44 | enableTsl: "0", 45 | defaultTtl: 36000, 46 | consistentBehavior: "1" 47 | } 48 | } 49 | ```` 50 | 51 | ```js 52 | // configStrategy for memcached engine 53 | configStrategy = { 54 | cache: { 55 | engine: "memcached", 56 | servers: ["127.0.0.1:11211"], 57 | defaultTtl: 36000, 58 | consistentBehavior: "1" 59 | } 60 | } 61 | ```` 62 | ```js 63 | // configStrategy for in-memory engine 64 | configStrategy = { 65 | cache: { 66 | engine: "none", 67 | namespace: "A", 68 | defaultTtl: "36000", 69 | consistentBehavior: "1" 70 | } 71 | } 72 | ```` 73 | 74 | # Install 75 | 76 | ```bash 77 | npm install @ostdotcom/cache --save 78 | ``` 79 | 80 | # Examples: 81 | 82 | #### Create OST Cache Object: 83 | 84 | ```js 85 | OSTCache = require('@ostdotcom/cache'); 86 | OSTCache = OSTCache.getInstance(configStrategy); 87 | 88 | cacheImplementer = OSTCache.cacheInstance; 89 | ``` 90 | 91 | #### Store and retrieve data in cache using `set` and `get`: 92 | 93 | ```js 94 | cacheImplementer.set('testKey', 'testValue', 5000).then(function(cacheResponse){ 95 | if (cacheResponse.isSuccess()) { 96 | console.log(cacheResponse.data.response); 97 | } else { 98 | console.log(cacheResponse); 99 | } 100 | }); 101 | cacheImplementer.get('testKey').then(function(cacheResponse){ 102 | if (cacheResponse.isSuccess()) { 103 | console.log(cacheResponse.data.response); 104 | } else { 105 | console.log(cacheResponse); 106 | } 107 | }); 108 | ``` 109 | 110 | #### Manage objects in cache using `setObject` and `getObject`: 111 | 112 | ```js 113 | cacheImplementer.setObject('testObjKey', {dataK1: 'a', dataK2: 'b'}).then(function(cacheResponse){ 114 | if (cacheResponse.isSuccess()) { 115 | console.log(cacheResponse.data.response); 116 | } else { 117 | console.log(cacheResponse); 118 | } 119 | }); 120 | cacheImplementer.getObject('testObjKey').then(function(cacheResponse){ 121 | if (cacheResponse.isSuccess()) { 122 | console.log(cacheResponse.data.response); 123 | } else { 124 | console.log(cacheResponse); 125 | } 126 | }); 127 | ``` 128 | 129 | #### Retrieve multiple cache data using `multiGet`: 130 | 131 | ###### * NOTE: Redis returns null from `multiGet` for objects, even if a value is set in the cache; the other caching engines match this behaviour. 132 | 133 | ```js 134 | cacheImplementer.set('testKeyOne', 'One').then(console.log); 135 | cacheImplementer.set('testKeyTwo', 'Two').then(console.log); 136 | cacheImplementer.multiGet(['testKeyOne', 'testKeyTwo']).then(function(cacheResponse){ 137 | if (cacheResponse.isSuccess()) { 138 | console.log(cacheResponse.data.response); 139 | } else { 140 | console.log(cacheResponse); 141 | } 142 | }); 143 | ``` 144 | 145 | #### Delete cache using `del`: 146 | 147 | ```js 148 | cacheImplementer.set('testKey', 'testValue').then(console.log); 149 | cacheImplementer.del('testKey').then(function(cacheResponse){ 150 | if (cacheResponse.isSuccess()) { 151 | console.log(cacheResponse.data.response); 152 | } else { 153 | console.log(cacheResponse); 154 | } 155 | }); 156 | ``` 157 | 158 | #### Manage counters in cache using `increment` and `decrement`: 159 | 160 | ```js 161 | cacheImplementer.set('testCounterKey', 1).then(console.log); 162 | cacheImplementer.increment('testCounterKey', 10).then(function(cacheResponse){ 163 | if (cacheResponse.isSuccess()) { 164 | console.log(cacheResponse.data.response); 165 | } else { 166 | console.log(cacheResponse); 167 | } 168 | }); 169 | cacheImplementer.decrement('testCounterKey', 5).then(function(cacheResponse){ 170 | if (cacheResponse.isSuccess()) { 171 | console.log(cacheResponse.data.response); 172 | } else { 173 | console.log(cacheResponse); 174 | } 175 | }); 176 | ``` 177 | 178 | #### Change the cache expiry time using `touch`: 179 | 180 | ```js 181 | cacheImplementer.set('testKey', "testData").then(console.log); 182 | cacheImplementer.touch('testKey', 10).then(function(cacheResponse){ 183 | if (cacheResponse.isSuccess()) { 184 | console.log(cacheResponse.data.response); 185 | } else { 186 | console.log(cacheResponse); 187 | } 188 | }); 189 | ``` 190 | -------------------------------------------------------------------------------- /test/cache_touch.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy; 11 | if (testCachingEngine === 'redis') { 12 | configStrategy = require(rootPrefix + '/test/env/redis.json'); 13 | } else if (testCachingEngine === 'memcached') { 14 | configStrategy = require(rootPrefix + '/test/env/memcached.json'); 15 | } else if (testCachingEngine === 'none') { 16 | configStrategy = require(rootPrefix + '/test/env/in-memory.json'); 17 | } 18 | 19 | const engineType = configStrategy.cache.engine; 20 | 21 | function performTest(cacheObj, keySuffix) { 22 | describe('Cache Touch ' + keySuffix, function() { 23 | keySuffix = keySuffix + '_' + new Date().getTime(); 24 | 25 | it('should return promise', async function() { 26 | let cKey = 'cache-key-touch' + keySuffix, 27 | cValue = 100, 28 | response = cacheObj.touch(cKey, cValue); 29 | assert.typeOf(response, 'Promise'); 30 | }); 31 | 32 | it('should fail when key/expiry is not passed', async function() { 33 | let response = await cacheObj.touch(); 34 | assert.equal(response.isSuccess(), false); 35 | }); 36 | 37 | it('should fail when key is undefined', async function() { 38 | let cValue = 100, 39 | response = await cacheObj.touch(undefined, cValue); 40 | assert.equal(response.isSuccess(), false); 41 | }); 42 | 43 | it('should fail when key is blank', async function() { 44 | let cKey = '', 45 | cValue = 100, 46 | response = await cacheObj.touch(cKey, cValue); 47 | assert.equal(response.isSuccess(), false); 48 | }); 49 | 50 | it('should fail when key is number', async function() { 51 | let cKey = 10, 52 | cValue = 100, 53 | response = await cacheObj.touch(cKey, cValue); 54 | assert.equal(response.isSuccess(), false); 55 | }); 56 | 57 | it('should fail when key has space', async function() { 58 | let cKey = 'a b' + keySuffix, 59 | cValue = 100, 60 | response = await cacheObj.touch(cKey, cValue); 61 | assert.equal(response.isSuccess(), false); 62 | }); 63 | 64 | it('should fail when key length is > 250 bytes', async function() { 65 | let cKey = Array(252).join('x'), 66 | cValue = 100, 67 | response = await cacheObj.touch(cKey, cValue); 68 | assert.equal(response.isSuccess(), false); 69 | }); 70 | 71 | it('should fail when expiry is Object', async function() { 72 | let cKey = 'cache-key-touch' + keySuffix, 73 | cValue = { a: 1 }, 74 | response = await cacheObj.touch(cKey, cValue); 75 | assert.equal(response.isSuccess(), false); 76 | }); 77 | 78 | it('should fail when expiry is blank', async function() { 79 | let cKey = 'cache-key-touch' + keySuffix, 80 | cValue = '', 81 | response = await cacheObj.touch(cKey, cValue); 82 | assert.equal(response.isSuccess(), false); 83 | }); 84 | 85 | it('should fail when expiry is string', async function() { 86 | let cKey = 'cache-key-touch' + keySuffix, 87 | cValue = 'String Value', 88 | response = await cacheObj.touch(cKey, cValue); 89 | assert.equal(response.isSuccess(), false); 90 | }); 91 | 92 | it('should fail when key does not exist', async function() { 93 | let cKey = 'cache-key-touch-not-exist' + keySuffix, 94 | cValue = 100, 95 | response = await cacheObj.touch(cKey, cValue); 96 | assert.equal(response.isSuccess(), false); 97 | }); 98 | 99 | it('should fail when expiry is negative secs', async function() { 100 | let cKey = 'cache-key-touch' + keySuffix, 101 | cValue = 'my value', 102 | response = await cacheObj.set(cKey, cValue); 103 | assert.equal(response.isSuccess(), true); 104 | 105 | // touch by negative value 106 | expiry = -1; 107 | resObj = await cacheObj.touch(cKey, expiry); 108 | assert.equal(resObj.isSuccess(), false); 109 | }); 110 | 111 | it('should pass when expiry is 100 secs', async function() { 112 | let cKey = 'cache-key-touch' + keySuffix, 113 | cValue = 'my value', 114 | response = await cacheObj.set(cKey, cValue); 115 | assert.equal(response.isSuccess(), true); 116 | 117 | // touch by positive value 118 | expiry = 100; 119 | resObj = await cacheObj.touch(cKey, expiry); 120 | assert.equal(resObj.isSuccess(), true); 121 | }); 122 | 123 | it('should pass when expiry is 0 secs', async function() { 124 | let cKey = 'cache-key-touch' + keySuffix, 125 | cValue = 'my value', 126 | response = await cacheObj.set(cKey, cValue); 127 | assert.equal(response.isSuccess(), true); 128 | 129 | // touch by 0 value 130 | expiry = 0; 131 | resObj = await cacheObj.touch(cKey, expiry); 132 | assert.equal(resObj.isSuccess(), true); 133 | 134 | // check if key deleted 135 | resObj = await cacheObj.get(cKey); 136 | assert.equal(resObj.isSuccess(), true); 137 | 138 | if (cacheObj._isConsistentBehaviour) { 139 | assert.equal(resObj.data.response, null); 140 | } else { 141 | if (engineType === 'memcached') { 142 | assert.equal(resObj.data.response, cValue); 143 | } else { 144 | assert.equal(resObj.data.response, null); 145 | } 146 | } 147 | }); 148 | }); 149 | } 150 | 151 | ostCache = OSTCache.getInstance(configStrategy); 152 | cacheImplementer = ostCache.cacheInstance; 153 | 154 | performTest(cacheImplementer, 'ConsistentBehaviour'); 155 | performTest(cacheImplementer, 'InconsistentBehaviour'); 156 | -------------------------------------------------------------------------------- /services/CacheInstance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Depending on cacheEngine variable, the preferred caching engine is picked. This module acts as a 3 | * wrapper / factory for the cache layer. Following are the actual implementations of the cache layer methods:
4 | * 9 | * 10 | * @module services/CacheInstance 11 | */ 12 | 13 | const OSTBase = require('@ostdotcom/base'); 14 | 15 | const rootPrefix = '..', 16 | coreConstants = require(rootPrefix + '/config/coreConstant'), 17 | instanceMap = require(rootPrefix + '/lib/cache/existingInstance'); 18 | 19 | const InstanceComposer = OSTBase.InstanceComposer; 20 | 21 | require(rootPrefix + '/lib/cache/implementer/Redis'); 22 | require(rootPrefix + '/lib/cache/implementer/Memcached'); 23 | require(rootPrefix + '/lib/cache/implementer/InMemory'); 24 | 25 | /** 26 | * Class for cache instance. 27 | * 28 | * @class CacheInstance 29 | */ 30 | class CacheInstance { 31 | /** 32 | * Constructor for cache instance. 33 | * 34 | * @param {object} configStrategy 35 | * @param {object} configStrategy.cache 36 | * @param {string} configStrategy.cache.engine 37 | * @param {string/number} configStrategy.cache.consistentBehavior 38 | * @param {object} instanceComposer 39 | * 40 | * @returns {cacheInstance} 41 | */ 42 | constructor(configStrategy, instanceComposer) { 43 | const oThis = this; 44 | 45 | if (configStrategy.cache.engine === undefined) { 46 | throw new Error('OST_CACHE_ENGINE parameter missing.'); 47 | } 48 | 49 | // Grab the required details from the configStrategy. 50 | oThis.cacheEngine = configStrategy.cache.engine; 51 | oThis.isConsistentBehaviour = configStrategy.cache.consistentBehavior; 52 | 53 | // Sanitize the isConsistentBehaviour variable. 54 | oThis.isConsistentBehaviour = oThis.isConsistentBehaviour == undefined ? true : oThis.isConsistentBehaviour != '0'; 55 | 56 | // Stores the endpoint for key generation of instanceMap. 57 | oThis.endpointDetails = null; 58 | 59 | // Generate endpointDetails for key generation of instanceMap. 60 | if (oThis.cacheEngine === 'redis') { 61 | const redisMandatoryParams = ['host', 'port', 'password', 'enableTsl']; 62 | 63 | // Check if all the mandatory connection parameters for Redis are available or not. 64 | for (let key = 0; key < redisMandatoryParams.length; key++) { 65 | if (!Object.prototype.hasOwnProperty.call(configStrategy.cache, redisMandatoryParams[key])) { 66 | throw new Error('Redis one or more mandatory connection parameters missing.'); 67 | } 68 | } 69 | 70 | oThis.endpointDetails = 71 | configStrategy.cache.host.toLowerCase() + 72 | '-' + 73 | configStrategy.cache.port.toString() + 74 | '-' + 75 | configStrategy.cache.enableTsl.toString(); 76 | } else if (oThis.cacheEngine === 'memcached') { 77 | if (!Object.prototype.hasOwnProperty.call(configStrategy.cache, 'servers')) { 78 | throw new Error('Memcached mandatory connection parameters missing.'); 79 | } 80 | 81 | oThis.endpointDetails = configStrategy.cache.servers.join(',').toLowerCase(); 82 | } else { 83 | oThis.endpointDetails = `in-memory-${configStrategy.cache.namespace || ''}`; 84 | } 85 | 86 | return oThis.getInstance(instanceComposer); 87 | } 88 | 89 | /** 90 | * Fetches a cache instance if available in instanceMap. If instance is not available in 91 | * instanceMap, it calls createCacheInstance() to create a new cache instance. 92 | * 93 | * @param {object} instanceComposer 94 | * 95 | * @returns {cacheInstance} 96 | */ 97 | getInstance(instanceComposer) { 98 | const oThis = this; 99 | 100 | // Fetches the cache instance key to be used. 101 | const instanceKey = oThis.getMapKey(); 102 | 103 | if (Object.prototype.hasOwnProperty.call(instanceMap, instanceKey)) { 104 | return instanceMap[instanceKey]; 105 | } 106 | 107 | return oThis.createCacheInstance(instanceComposer); 108 | } 109 | 110 | /** 111 | * Creates the key for the instanceMap. 112 | * 113 | * @returns {string} 114 | */ 115 | getMapKey() { 116 | const oThis = this; 117 | 118 | return oThis.cacheEngine.toString() + '-' + oThis.isConsistentBehaviour.toString() + '-' + oThis.endpointDetails; 119 | } 120 | 121 | /** 122 | * Creates a new cache instance if not available in instanceMap. 123 | * 124 | * @returns {cacheInstance} 125 | */ 126 | createCacheInstance(instanceComposer) { 127 | const oThis = this; 128 | 129 | let implementerKlass = null; 130 | switch (oThis.cacheEngine) { 131 | case 'redis': { 132 | implementerKlass = instanceComposer.getShadowedClassFor(coreConstants.icNameSpace, 'RedisCacheImplementer'); 133 | break; 134 | } 135 | case 'memcached': { 136 | implementerKlass = instanceComposer.getShadowedClassFor(coreConstants.icNameSpace, 'MemcachedCacheImplementer'); 137 | break; 138 | } 139 | case 'none': { 140 | implementerKlass = instanceComposer.getShadowedClassFor(coreConstants.icNameSpace, 'InMemoryCacheImplementer'); 141 | break; 142 | } 143 | default: { 144 | throw new Error('invalid caching engine or not defined.'); 145 | } 146 | } 147 | 148 | const cacheInstance = new implementerKlass(oThis.isConsistentBehaviour); 149 | 150 | // Fetch the instanceKey. 151 | const instanceKey = oThis.getMapKey(); 152 | 153 | // Set the newly created instance in the map. 154 | instanceMap[instanceKey] = cacheInstance; 155 | 156 | return cacheInstance; 157 | } 158 | } 159 | 160 | InstanceComposer.registerAsObject(CacheInstance, coreConstants.icNameSpace, 'getCacheInstance', true); 161 | 162 | module.exports = CacheInstance; 163 | -------------------------------------------------------------------------------- /test/cache_increment.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy; 11 | if (testCachingEngine === 'redis') { 12 | configStrategy = require(rootPrefix + '/test/env/redis.json'); 13 | } else if (testCachingEngine === 'memcached') { 14 | configStrategy = require(rootPrefix + '/test/env/memcached.json'); 15 | } else if (testCachingEngine === 'none') { 16 | configStrategy = require(rootPrefix + '/test/env/in-memory.json'); 17 | } 18 | 19 | const engineType = configStrategy.cache.engine; 20 | 21 | function performTest(cacheObj, keySuffix) { 22 | describe('Cache Increment ' + keySuffix, function() { 23 | keySuffix = keySuffix + '_' + new Date().getTime(); 24 | 25 | it('should return promise', async function() { 26 | let cKey = 'cache-key-incr-counter' + keySuffix, 27 | cValue = 1, 28 | response = cacheObj.increment(cKey, cValue); 29 | assert.typeOf(response, 'Promise'); 30 | }); 31 | 32 | it('should fail when key/value is not passed', async function() { 33 | let response = await cacheObj.increment(); 34 | assert.equal(response.isSuccess(), false); 35 | }); 36 | 37 | it('should fail when key is undefined', async function() { 38 | let cValue = 1, 39 | response = await cacheObj.increment(undefined, cValue); 40 | assert.equal(response.isSuccess(), false); 41 | }); 42 | 43 | it('should fail when key is blank', async function() { 44 | let cKey = '', 45 | cValue = 1, 46 | response = await cacheObj.increment(cKey, cValue); 47 | assert.equal(response.isSuccess(), false); 48 | }); 49 | 50 | it('should fail when key is number', async function() { 51 | let cKey = 10, 52 | cValue = 1, 53 | response = await cacheObj.increment(cKey, cValue); 54 | assert.equal(response.isSuccess(), false); 55 | }); 56 | 57 | it('should fail when key has space', async function() { 58 | let cKey = 'a b' + keySuffix, 59 | cValue = 1, 60 | response = await cacheObj.increment(cKey, cValue); 61 | assert.equal(response.isSuccess(), false); 62 | }); 63 | 64 | it('should fail when key length is > 250 bytes', async function() { 65 | let cKey = Array(252).join('x'), 66 | cValue = 1, 67 | response = await cacheObj.increment(cKey, cValue); 68 | assert.equal(response.isSuccess(), false); 69 | }); 70 | 71 | it('should fail when value is Object', async function() { 72 | let cKey = 'cache-key-incr-counter' + keySuffix, 73 | cValue = { a: 1 }, 74 | response = await cacheObj.increment(cKey, cValue); 75 | assert.equal(response.isSuccess(), false); 76 | }); 77 | 78 | it('should fail when value is blank', async function() { 79 | let cKey = 'cache-key-incr-counter' + keySuffix, 80 | cValue = '', 81 | response = await cacheObj.increment(cKey, cValue); 82 | assert.equal(response.isSuccess(), false); 83 | }); 84 | 85 | it('should fail when value is string', async function() { 86 | let cKey = 'cache-key-incr-counter' + keySuffix, 87 | cValue = 'String Value', 88 | response = await cacheObj.increment(cKey, cValue); 89 | assert.equal(response.isSuccess(), false); 90 | }); 91 | 92 | it('should fail when key has non numeric value', async function() { 93 | let cKey = 'cache-key-incr-non-numeric' + keySuffix, 94 | cValue = 'hi', 95 | response = await cacheObj.set(cKey, cValue); 96 | console.log('response1------' + JSON.stringify(response)); 97 | assert.equal(response.isSuccess(), true); 98 | 99 | cValue = 10; 100 | response = await cacheObj.increment(cKey, cValue); 101 | console.log('response2------' + JSON.stringify(response)); 102 | assert.equal(response.isSuccess(), false); 103 | }); 104 | 105 | performTestWhenKeyDoesNotExists(cacheObj, keySuffix); 106 | 107 | it('should pass incremented by multiple integer values', async function() { 108 | let cKey = 'cache-key-incr-counter-key' + keySuffix, 109 | cValue = 10, 110 | response = await cacheObj.set(cKey, cValue); 111 | assert.equal(response.isSuccess(), true); 112 | 113 | // increment by default value 114 | resObj = await cacheObj.increment(cKey); 115 | assert.equal(resObj.isSuccess(), true); 116 | cValue += 1; 117 | assert.equal(resObj.data.response, cValue); 118 | 119 | // increment by negative value 120 | resObj = await cacheObj.increment(cKey, -1); 121 | assert.equal(resObj.isSuccess(), false); 122 | 123 | // increment by float value 124 | resObj = await cacheObj.increment(cKey, 1.2); 125 | assert.equal(resObj.isSuccess(), false); 126 | 127 | // Increment by 1 128 | let incrementBy = [1, 3, 2, 10, 100, 57]; 129 | for (let i = 0; i < incrementBy.length; i++) { 130 | resObj = await cacheObj.increment(cKey, incrementBy[i]); 131 | assert.equal(resObj.isSuccess(), true); 132 | cValue += incrementBy[i]; 133 | assert.equal(resObj.data.response, cValue); 134 | } 135 | }); 136 | }); 137 | } 138 | 139 | function performTestWhenKeyDoesNotExists(cacheObj, keySuffix) { 140 | const failCase = function() { 141 | it('should fail when key does not exist', async function() { 142 | let cKey = 'cache-key-incr-counter-not-exist' + keySuffix, 143 | cValue = 10, 144 | response = await cacheObj.increment(cKey, cValue); 145 | assert.equal(response.isSuccess(), false); 146 | }); 147 | }; 148 | 149 | const passCase = function() { 150 | it('should pass when key does not exist', async function() { 151 | let cKey = 'cache-key-incr-counter-not-exist' + keySuffix, 152 | cValue = 10, 153 | response = await cacheObj.increment(cKey, cValue); 154 | assert.equal(response.isSuccess(), true); 155 | assert.equal(response.data.response, cValue); 156 | }); 157 | }; 158 | 159 | if (cacheObj._isConsistentBehaviour) { 160 | failCase(); 161 | } else { 162 | if (engineType === 'redis') { 163 | passCase(); 164 | } else if (engineType === 'memcached') { 165 | failCase(); 166 | } else if (engineType === 'none') { 167 | passCase(); 168 | } 169 | } 170 | } 171 | 172 | ostCache = OSTCache.getInstance(configStrategy); 173 | cacheImplementer = ostCache.cacheInstance; 174 | 175 | performTest(cacheImplementer, 'ConsistentBehaviour'); 176 | performTest(cacheImplementer, 'InconsistentBehaviour'); 177 | -------------------------------------------------------------------------------- /test/cache_decrement.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCacheKlass = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy; 11 | if (testCachingEngine == 'redis') { 12 | configStrategy = require(rootPrefix + '/test/env/redis.json'); 13 | } else if (testCachingEngine == 'memcached') { 14 | configStrategy = require(rootPrefix + '/test/env/memcached.json'); 15 | } else if (testCachingEngine == 'none') { 16 | configStrategy = require(rootPrefix + '/test/env/in-memory.json'); 17 | } 18 | 19 | const engineType = configStrategy.cache.engine; 20 | 21 | function performTest(cacheObj, keySuffix) { 22 | describe('Cache Decrement ' + keySuffix, function() { 23 | keySuffix = keySuffix + '_' + new Date().getTime(); 24 | 25 | it('should return promise', async function() { 26 | let cKey = 'cache-key-decr-counter' + keySuffix, 27 | cValue = 1, 28 | response = cacheObj.decrement(cKey, cValue); 29 | assert.typeOf(response, 'Promise'); 30 | }); 31 | 32 | it('should fail when key/value is not passed', async function() { 33 | let response = await cacheObj.decrement(); 34 | assert.equal(response.isSuccess(), false); 35 | }); 36 | 37 | it('should fail when key is undefined', async function() { 38 | let cValue = 1, 39 | response = await cacheObj.decrement(undefined, cValue); 40 | assert.equal(response.isSuccess(), false); 41 | }); 42 | 43 | it('should fail when key is blank', async function() { 44 | let cKey = '', 45 | cValue = 1, 46 | response = await cacheObj.decrement(cKey, cValue); 47 | assert.equal(response.isSuccess(), false); 48 | }); 49 | 50 | it('should fail when key is number', async function() { 51 | let cKey = 10, 52 | cValue = 1, 53 | response = await cacheObj.decrement(cKey, cValue); 54 | assert.equal(response.isSuccess(), false); 55 | }); 56 | 57 | it('should fail when key has space', async function() { 58 | let cKey = 'a b', 59 | cValue = 1, 60 | response = await cacheObj.decrement(cKey, cValue); 61 | assert.equal(response.isSuccess(), false); 62 | }); 63 | 64 | it('should fail when key length is > 250 bytes', async function() { 65 | let cKey = Array(252).join('x'), 66 | cValue = 1, 67 | response = await cacheObj.decrement(cKey, cValue); 68 | assert.equal(response.isSuccess(), false); 69 | }); 70 | 71 | it('should fail when value is Object', async function() { 72 | let cKey = 'cache-key-decr-counter' + keySuffix, 73 | cValue = { a: 1 }, 74 | response = await cacheObj.decrement(cKey, cValue); 75 | assert.equal(response.isSuccess(), false); 76 | }); 77 | 78 | it('should fail when value is blank', async function() { 79 | let cKey = 'cache-key-decr-counter' + keySuffix, 80 | cValue = '', 81 | response = await cacheObj.decrement(cKey, cValue); 82 | assert.equal(response.isSuccess(), false); 83 | }); 84 | 85 | it('should fail when value is string', async function() { 86 | let cKey = 'cache-key-decr-counter' + keySuffix, 87 | cValue = 'String Value', 88 | response = await cacheObj.decrement(cKey, cValue); 89 | assert.equal(response.isSuccess(), false); 90 | }); 91 | 92 | it('should fail when key has non numeric value', async function() { 93 | let cKey = 'cache-key-decr-non-numeric' + keySuffix, 94 | cValue = 'hi', 95 | response = await cacheObj.set(cKey, cValue); 96 | assert.equal(response.isSuccess(), true); 97 | 98 | cValue = 10; 99 | response = await cacheObj.decrement(cKey, cValue); 100 | assert.equal(response.isSuccess(), false); 101 | }); 102 | 103 | performTestWhenKeyDoesNotExists(cacheObj, keySuffix); 104 | 105 | it('should pass decremented by multiple integer values', async function() { 106 | let cKey = 'cache-key-decr-counter-key' + keySuffix, 107 | cValue = 10000, 108 | response = await cacheObj.set(cKey, cValue); 109 | assert.equal(response.isSuccess(), true); 110 | 111 | // decrement by default value 112 | resObj = await cacheObj.decrement(cKey); 113 | assert.equal(resObj.isSuccess(), true); 114 | cValue -= 1; 115 | assert.equal(resObj.data.response, cValue); 116 | 117 | // decrement by negative value 118 | resObj = await cacheObj.decrement(cKey, -1); 119 | assert.equal(resObj.isSuccess(), false); 120 | 121 | // decrement by float value 122 | resObj = await cacheObj.decrement(cKey, 1.2); 123 | assert.equal(resObj.isSuccess(), false); 124 | 125 | // decrement by 1 126 | let decrementBy = [1, 3, 2, 10, 100, 57]; 127 | for (let i = 0; i < decrementBy.length; i++) { 128 | resObj = await cacheObj.decrement(cKey, decrementBy[i]); 129 | assert.equal(resObj.isSuccess(), true); 130 | cValue -= decrementBy[i]; 131 | assert.equal(resObj.data.response, cValue); 132 | } 133 | 134 | // decrement by bigger number value 135 | resObj = await cacheObj.decrement(cKey, 100000000); 136 | assert.equal(resObj.isSuccess(), true); 137 | 138 | if (['redis', 'none'].includes(engineType) && !cacheObj._isConsistentBehaviour) { 139 | assert.equal(resObj.data.response, cValue - 100000000); 140 | } else { 141 | cValue = 0; 142 | assert.equal(resObj.data.response, cValue); 143 | } 144 | }); 145 | }); 146 | } 147 | 148 | function performTestWhenKeyDoesNotExists(cacheObj, keySuffix) { 149 | const failCase = function() { 150 | it('should fail when key does not exist', async function() { 151 | let cKey = 'cache-key-decr-counter-not-exist' + keySuffix, 152 | cValue = 10, 153 | response = await cacheObj.decrement(cKey, cValue); 154 | assert.equal(response.isSuccess(), false); 155 | }); 156 | }; 157 | 158 | const passCase = function() { 159 | it('should pass when key does not exist', async function() { 160 | let cKey = 'cache-key-decr-counter-not-exist' + keySuffix, 161 | cValue = 10, 162 | response = await cacheObj.decrement(cKey, cValue); 163 | assert.equal(response.isSuccess(), true); 164 | assert.equal(response.data.response, -1 * cValue); 165 | }); 166 | }; 167 | 168 | if (cacheObj._isConsistentBehaviour) { 169 | failCase(); 170 | } else { 171 | if (engineType === 'redis') { 172 | passCase(); 173 | } else if (engineType === 'memcached') { 174 | failCase(); 175 | } else if (engineType === 'none') { 176 | passCase(); 177 | } 178 | } 179 | } 180 | 181 | ostCache = OSTCacheKlass.getInstance(configStrategy); 182 | cacheImplementer = ostCache.cacheInstance; 183 | 184 | performTest(cacheImplementer, 'ConsistentBehaviour'); 185 | performTest(cacheImplementer, 'InconsistentBehaviour'); 186 | -------------------------------------------------------------------------------- /test/cache_set.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy1; 11 | let configStrategy2; 12 | if (testCachingEngine === 'redis') { 13 | configStrategy1 = require(rootPrefix + '/test/env/redis.json'); 14 | configStrategy2 = require(rootPrefix + '/test/env/redis2.json'); 15 | } else if (testCachingEngine === 'memcached') { 16 | configStrategy1 = require(rootPrefix + '/test/env/memcached.json'); 17 | configStrategy2 = require(rootPrefix + '/test/env/memcached2.json'); 18 | } else if (testCachingEngine === 'none') { 19 | configStrategy1 = require(rootPrefix + '/test/env/in-memory.json'); 20 | configStrategy2 = require(rootPrefix + '/test/env/in-memory2.json'); 21 | // Config strategies are same as they won't change for in-memory. 22 | } 23 | 24 | const engineType = configStrategy1.cache.engine; 25 | 26 | function performTest(cacheObj, keySuffix) { 27 | describe('Cache Set ' + keySuffix, function() { 28 | keySuffix = keySuffix + '_' + new Date().getTime(); 29 | 30 | it('should return promise', async function() { 31 | let cKey = 'cache-key' + keySuffix, 32 | cValue = 'String Value', 33 | response = cacheObj.set(cKey, cValue); 34 | assert.typeOf(response, 'Promise'); 35 | }); 36 | 37 | it('should fail when key/value is not passed', async function() { 38 | let response = await cacheObj.set(); 39 | assert.equal(response.isSuccess(), false); 40 | }); 41 | 42 | it('should fail when key is undefined', async function() { 43 | let cValue = 'String Value', 44 | response = await cacheObj.set(undefined, cValue); 45 | assert.equal(response.isSuccess(), false); 46 | }); 47 | 48 | it('should fail when key is blank', async function() { 49 | let cKey = '', 50 | cValue = 'String Value', 51 | response = await cacheObj.set(cKey, cValue); 52 | assert.equal(response.isSuccess(), false); 53 | }); 54 | 55 | it('should fail when key is number', async function() { 56 | let cKey = 10, 57 | cValue = 'String Value', 58 | response = await cacheObj.set(cKey, cValue); 59 | assert.equal(response.isSuccess(), false); 60 | }); 61 | 62 | it('should fail when key has space', async function() { 63 | let cKey = 'a b' + keySuffix, 64 | cValue = 'String Value', 65 | response = await cacheObj.set(cKey, cValue); 66 | assert.equal(response.isSuccess(), false); 67 | }); 68 | 69 | it('should fail when key length is > 250 bytes', async function() { 70 | let cKey = Array(252).join('x'), 71 | cValue = 'String Value', 72 | response = await cacheObj.set(cKey, cValue); 73 | assert.equal(response.isSuccess(), false); 74 | }); 75 | 76 | if (engineType != 'redis') { 77 | it('should pass when value is Object', async function() { 78 | let cKey = 'cache-key' + keySuffix, 79 | cValue = { a: 1 }, 80 | response = await cacheObj.set(cKey, cValue); 81 | assert.equal(response.isSuccess(), true); 82 | assert.equal(response.data.response, true); 83 | }); 84 | } else { 85 | it('should fail when value is Object', async function() { 86 | let cKey = 'cache-key' + keySuffix, 87 | cValue = { a: 1 }, 88 | response = await cacheObj.set(cKey, cValue); 89 | assert.equal(response.isSuccess(), false); 90 | }); 91 | } 92 | 93 | it('should fail when value is undefined', async function() { 94 | let cKey = 'cache-key' + keySuffix, 95 | response = await cacheObj.set(cKey); 96 | assert.equal(response.isSuccess(), false); 97 | }); 98 | 99 | it('should fail when value size is > 1 MB', async function() { 100 | let cKey = 'cache-key' + keySuffix, 101 | cValue = Array(1050000).join('x'), 102 | response = await cacheObj.set(cKey, cValue); 103 | assert.equal(response.isSuccess(), false); 104 | }); 105 | 106 | it('should pass when value is string', async function() { 107 | let cKey = 'cache-key' + keySuffix, 108 | cValue = 'String Value', 109 | response = await cacheObj.set(cKey, cValue); 110 | assert.equal(response.isSuccess(), true); 111 | assert.equal(response.data.response, true); 112 | }); 113 | 114 | it('should pass when value is integer', async function() { 115 | let cKey = 'cache-key' + keySuffix, 116 | cValue = 10, 117 | response = await cacheObj.set(cKey, cValue); 118 | assert.equal(response.isSuccess(), true); 119 | assert.equal(response.data.response, true); 120 | }); 121 | 122 | it('should pass when value is blank', async function() { 123 | let cKey = 'cache-key' + keySuffix, 124 | cValue = '', 125 | response = await cacheObj.set(cKey, cValue); 126 | assert.equal(response.isSuccess(), true); 127 | assert.equal(response.data.response, true); 128 | }); 129 | 130 | it('should delete from cache after ttl', async function() { 131 | let cKey = 'cache-key' + keySuffix, 132 | cValue = 10, 133 | ttl = 6, // seconds 134 | response = await cacheObj.set(cKey, cValue, ttl); 135 | assert.equal(response.isSuccess(), true); 136 | assert.equal(response.data.response, true); 137 | setTimeout(async function() { 138 | response = await cacheObj.get(cKey); 139 | assert.equal(response.isSuccess(), true); 140 | assert.equal(response.data.response, null); 141 | }, ttl * 1000); 142 | }); 143 | }); 144 | } 145 | 146 | function performMultipleTest(cacheObj1, cacheObj2, keySuffix) { 147 | describe('Cache Multiple Set ' + keySuffix, function() { 148 | keySuffix = keySuffix + '_' + new Date().getTime(); 149 | 150 | it('should set values to different cache instances', async function() { 151 | let cKey1 = 'cache-key' + keySuffix, 152 | cValue1 = 'value1', 153 | response1 = await cacheObj1.set(cKey1, cValue1), 154 | cKey2 = 'cache-key' + keySuffix, 155 | cValue2 = 'value2', 156 | response2 = await cacheObj2.set(cKey2, cValue2); 157 | assert.equal(response1.isSuccess(), true); 158 | assert.equal(response1.data.response, true); 159 | assert.equal(response2.isSuccess(), true); 160 | assert.equal(response2.data.response, true); 161 | }); 162 | }); 163 | } 164 | 165 | ostCache1 = OSTCache.getInstance(configStrategy1); 166 | cacheImplementer1 = ostCache1.cacheInstance; 167 | 168 | ostCache2 = OSTCache.getInstance(configStrategy2); 169 | cacheImplementer2 = ostCache2.cacheInstance; 170 | 171 | performTest(cacheImplementer1, 'ConsistentBehaviour'); 172 | performTest(cacheImplementer1, 'InconsistentBehaviour'); 173 | performMultipleTest(cacheImplementer1, cacheImplementer2, 'ConsistentBehaviour'); 174 | performMultipleTest(cacheImplementer1, cacheImplementer2, 'InconsistentBehaviour'); 175 | -------------------------------------------------------------------------------- /test/cache_setobject.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy; 11 | if (testCachingEngine === 'redis') { 12 | configStrategy = require(rootPrefix + '/test/env/redis.json'); 13 | } else if (testCachingEngine === 'memcached') { 14 | configStrategy = require(rootPrefix + '/test/env/memcached.json'); 15 | } else if (testCachingEngine === 'none') { 16 | configStrategy = require(rootPrefix + '/test/env/in-memory.json'); 17 | } 18 | 19 | const engineType = configStrategy.cache.engine; 20 | 21 | function performTest(cacheObj, keySuffix) { 22 | describe('Cache SetObject ' + keySuffix, function() { 23 | keySuffix = keySuffix + '_' + new Date().getTime(); 24 | 25 | it('should return promise', async function() { 26 | let cKey = 'cache-key-object' + keySuffix, 27 | cValue = { a: 1 }, 28 | response = cacheObj.setObject(cKey, cValue); 29 | assert.typeOf(response, 'Promise'); 30 | }); 31 | 32 | it('should fail when key/value is not passed', async function() { 33 | let response = await cacheObj.setObject(); 34 | assert.equal(response.isSuccess(), false); 35 | }); 36 | 37 | it('should fail when key is undefined', async function() { 38 | let cValue = { a: 1 }, 39 | response = await cacheObj.setObject(undefined, cValue); 40 | assert.equal(response.isSuccess(), false); 41 | }); 42 | 43 | it('should fail when key is blank', async function() { 44 | let cKey = '', 45 | cValue = { a: 1 }, 46 | response = await cacheObj.setObject(cKey, cValue); 47 | assert.equal(response.isSuccess(), false); 48 | }); 49 | 50 | it('should fail when key is number', async function() { 51 | let cKey = 10, 52 | cValue = { a: 1 }, 53 | response = await cacheObj.setObject(cKey, cValue); 54 | assert.equal(response.isSuccess(), false); 55 | }); 56 | 57 | it('should fail when key has space', async function() { 58 | let cKey = 'a b' + keySuffix, 59 | cValue = { a: 1 }, 60 | response = await cacheObj.setObject(cKey, cValue); 61 | assert.equal(response.isSuccess(), false); 62 | }); 63 | 64 | it('should fail when key length is > 250 bytes', async function() { 65 | let cKey = Array(252).join('x'), 66 | cValue = { a: 1 }, 67 | response = await cacheObj.setObject(cKey, cValue); 68 | assert.equal(response.isSuccess(), false); 69 | }); 70 | 71 | it('should fail when value is not Object', async function() { 72 | let cKey = 'cache-key-object' + keySuffix, 73 | cValue = 'cache-value', 74 | response = await cacheObj.setObject(cKey, cValue); 75 | assert.equal(response.isSuccess(), false); 76 | }); 77 | 78 | it('should fail when value is undefined', async function() { 79 | let cKey = 'cache-key-object' + keySuffix, 80 | response = await cacheObj.setObject(cKey); 81 | assert.equal(response.isSuccess(), false); 82 | }); 83 | 84 | it('should fail when value size is > 1 MB', async function() { 85 | let cKey = 'cache-key-object' + keySuffix, 86 | cValue = { a: Array(1050000).join('x') }, 87 | response = await cacheObj.setObject(cKey, cValue); 88 | assert.equal(response.isSuccess(), false); 89 | }); 90 | 91 | it('should fail when value is string', async function() { 92 | let cKey = 'cache-key-object' + keySuffix, 93 | cValue = 'String Value', 94 | response = await cacheObj.setObject(cKey, cValue); 95 | assert.equal(response.isSuccess(), false); 96 | }); 97 | 98 | it('should fail when value is integer', async function() { 99 | let cKey = 'cache-key-object' + keySuffix, 100 | cValue = 10, 101 | response = await cacheObj.setObject(cKey, cValue); 102 | assert.equal(response.isSuccess(), false); 103 | }); 104 | 105 | it('should fail when value is blank', async function() { 106 | let cKey = 'cache-key-object' + keySuffix, 107 | cValue = '', 108 | response = await cacheObj.setObject(cKey, cValue); 109 | assert.equal(response.isSuccess(), false); 110 | }); 111 | 112 | performTestWhenValueIsArray(cacheObj, keySuffix); 113 | 114 | it('should pass when value is object', async function() { 115 | let cKey = 'cache-key-object' + keySuffix, 116 | cValue = { a: 'a' }, 117 | response = await cacheObj.setObject(cKey, cValue); 118 | assert.equal(response.isSuccess(), true); 119 | assert.equal(response.data.response, true); 120 | }); 121 | 122 | it('should pass when value is complex object', async function() { 123 | let cKey = 'cache-key-object' + keySuffix, 124 | cValue = { a: 'a', b: [12, 23], c: true, d: 1 }, 125 | response = await cacheObj.setObject(cKey, cValue); 126 | assert.equal(response.isSuccess(), true); 127 | assert.equal(response.data.response, true); 128 | }); 129 | 130 | it('should delete from cache after ttl (if cache engine is not redis)', async function() { 131 | let cKey = 'cache-key' + keySuffix, 132 | cValue = { a: 'a', b: [12, 23], c: true, d: 1 }, 133 | ttl = 6, // seconds 134 | response = await cacheObj.setObject(cKey, cValue, ttl); 135 | setTimeout(async function() { 136 | response = await cacheObj.getObject(cKey); 137 | assert.equal(response.isSuccess(), true); 138 | if (engineType != 'redis') { 139 | assert.equal(response.data.response, null); 140 | } else { 141 | assert.equal(response.data.response, true); 142 | } 143 | }, ttl * 1000); 144 | }); 145 | }); 146 | } 147 | 148 | function performTestWhenValueIsArray(cacheObj, keySuffix) { 149 | const failCase = function() { 150 | it('should fail when value is Array', async function() { 151 | let cKey = 'cache-key-object' + keySuffix, 152 | cValue = [12, 23], 153 | response = await cacheObj.setObject(cKey, cValue); 154 | assert.equal(response.isSuccess(), false); 155 | }); 156 | }; 157 | 158 | const passCase = function() { 159 | it('should pass when value is Array', async function() { 160 | let cKey = 'cache-key-object' + keySuffix, 161 | cValue = [12, 23], 162 | response = await cacheObj.setObject(cKey, cValue); 163 | assert.equal(response.isSuccess(), true); 164 | 165 | const getResponse = await cacheObj.getObject(cKey); 166 | assert.deepEqual(cValue, getResponse.data.response); 167 | }); 168 | }; 169 | 170 | if (cacheObj._isConsistentBehaviour) { 171 | failCase(); 172 | } else { 173 | if (engineType === 'redis') { 174 | failCase(); 175 | } else if (engineType === 'memcached') { 176 | passCase(); 177 | } else if (engineType === 'none') { 178 | passCase(); 179 | } 180 | } 181 | } 182 | 183 | ostCache = OSTCache.getInstance(configStrategy); 184 | cacheImplementer = ostCache.cacheInstance; 185 | 186 | performTest(cacheImplementer, 'ConsistentBehaviour'); 187 | performTest(cacheImplementer, 'InconsistentBehaviour'); 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /test/cache_get.js: -------------------------------------------------------------------------------- 1 | // Load external packages 2 | const chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | // Load cache service 6 | const rootPrefix = '..', 7 | OSTCache = require(rootPrefix + '/index'), 8 | testCachingEngine = process.env.TEST_CACHING_ENGINE; 9 | 10 | let configStrategy1; 11 | let configStrategy2; 12 | if (testCachingEngine === 'redis') { 13 | configStrategy1 = require(rootPrefix + '/test/env/redis.json'); 14 | configStrategy2 = require(rootPrefix + '/test/env/redis2.json'); 15 | } else if (testCachingEngine === 'memcached') { 16 | configStrategy1 = require(rootPrefix + '/test/env/memcached.json'); 17 | configStrategy2 = require(rootPrefix + '/test/env/memcached2.json'); 18 | } else if (testCachingEngine === 'none') { 19 | configStrategy1 = require(rootPrefix + '/test/env/in-memory.json'); 20 | configStrategy2 = require(rootPrefix + '/test/env/in-memory2.json'); 21 | // Config strategies are same as they won't change for in-memory. 22 | } 23 | 24 | const engineType = configStrategy1.cache.engine; 25 | 26 | function performTest(cacheObj, keySuffix) { 27 | describe('Cache Get ' + keySuffix, function() { 28 | keySuffix = keySuffix + '_' + new Date().getTime(); 29 | 30 | it('should return promise', function() { 31 | let cKey = 'cache-key' + keySuffix, 32 | response = cacheObj.get(cKey); 33 | assert.typeOf(response, 'Promise'); 34 | }); 35 | 36 | it('should fail when key is not passed', async function() { 37 | let response = await cacheObj.get(); 38 | assert.equal(response.isSuccess(), false); 39 | }); 40 | 41 | it('should fail when key is undefined', async function() { 42 | let response = await cacheObj.get(undefined); 43 | assert.equal(response.isSuccess(), false); 44 | }); 45 | 46 | it('should fail when key is blank', async function() { 47 | let cKey = '', 48 | response = await cacheObj.get(cKey); 49 | assert.equal(response.isSuccess(), false); 50 | }); 51 | 52 | it('should fail when key is number', async function() { 53 | let cKey = 10, 54 | response = await cacheObj.get(cKey); 55 | assert.equal(response.isSuccess(), false); 56 | }); 57 | 58 | it('should fail when key has space', async function() { 59 | let cKey = 'a b' + keySuffix, 60 | response = await cacheObj.get(cKey); 61 | assert.equal(response.isSuccess(), false); 62 | }); 63 | 64 | it('should fail when key length is > 250 bytes', async function() { 65 | let cKey = Array(252).join('x'), 66 | response = await cacheObj.get(cKey); 67 | assert.equal(response.isSuccess(), false); 68 | }); 69 | 70 | it('should pass when value is not get', async function() { 71 | let cKey = 'cache-key-not-get' + keySuffix, 72 | response = await cacheObj.get(cKey); 73 | assert.equal(response.isSuccess(), true); 74 | assert.equal(response.data.response, null); 75 | }); 76 | 77 | it('should pass when value is string', async function() { 78 | let cKey = 'cache-key' + keySuffix, 79 | cValue = 'String Value', 80 | responseSet = await cacheObj.set(cKey, cValue), 81 | response = await cacheObj.get(cKey); 82 | assert.equal(response.isSuccess(), true); 83 | assert.equal(response.data.response, cValue); 84 | }); 85 | 86 | it('should pass when value is integer', async function() { 87 | let cKey = 'cache-key' + keySuffix, 88 | cValue = 10, 89 | responseSet = await cacheObj.set(cKey, cValue), 90 | response = await cacheObj.get(cKey); 91 | assert.equal(response.isSuccess(), true); 92 | assert.equal(response.data.response, cValue); 93 | }); 94 | 95 | it('should pass when value is blank', async function() { 96 | let cKey = 'cache-key' + keySuffix, 97 | cValue = '', 98 | responseSet = await cacheObj.set(cKey, cValue), 99 | response = await cacheObj.get(cKey); 100 | assert.equal(response.isSuccess(), true); 101 | assert.equal(response.data.response, cValue); 102 | }); 103 | 104 | if (engineType != 'redis') { 105 | it('should pass when value is Object', async function() { 106 | let cKey = 'cache-key-object' + keySuffix, 107 | cValue = { a: 1 }, 108 | responseSet = await cacheObj.set(cKey, cValue), 109 | response = await cacheObj.get(cKey); 110 | assert.equal(response.isSuccess(), true); 111 | assert.equal(typeof response.data.response, typeof cValue); 112 | assert.equal(JSON.stringify(response.data.response), JSON.stringify(cValue)); 113 | }); 114 | 115 | it('should pass when value is Array', async function() { 116 | let cKey = 'cache-key-object' + keySuffix, 117 | cValue = [1, 2, 3, 4], 118 | responseSet = await cacheObj.set(cKey, cValue), 119 | response = await cacheObj.get(cKey); 120 | assert.equal(response.isSuccess(), true); 121 | assert.equal(typeof response.data.response, typeof cValue); 122 | assert.equal(JSON.stringify(response.data.response), JSON.stringify(cValue)); 123 | }); 124 | } else { 125 | it('should fail when value is Object', async function() { 126 | let cKey = 'cache-key-object' + keySuffix, 127 | cValue = { a: 1 }, 128 | responseSet = await cacheObj.setObject(cKey, cValue), 129 | response = await cacheObj.get(cKey); 130 | assert.equal(response.isSuccess(), false); 131 | }); 132 | 133 | it('should fail when value is Array', async function() { 134 | let cKey = 'cache-key-object' + keySuffix, 135 | cValue = [1, 2, 3, 4], 136 | responseSet = await cacheObj.set(cKey, cValue), 137 | response = await cacheObj.get(cKey); 138 | assert.equal(response.isSuccess(), false); 139 | }); 140 | } 141 | }); 142 | } 143 | 144 | function performMultipleTest(cacheObj1, cacheObj2, keySuffix) { 145 | describe('Cache Multiple Get ' + keySuffix, function() { 146 | keySuffix = keySuffix + '_' + new Date().getTime(); 147 | 148 | it('should pass and get different values from different cache instances for same key', async function() { 149 | let cKey1 = 'cache-key' + keySuffix, 150 | cValue1 = 'value1', 151 | responseSet1 = await cacheObj1.set(cKey1, cValue1), 152 | response1 = await cacheObj1.get(cKey1), 153 | cKey2 = 'cache-key' + keySuffix, 154 | cValue2 = 'value2', 155 | responseSet2 = await cacheObj2.set(cKey2, cValue2), 156 | response2 = await cacheObj2.get(cKey2); 157 | assert.equal(response1.isSuccess(), true); 158 | assert.equal(JSON.stringify(response1.data.response), JSON.stringify(cValue1)); 159 | assert.equal(response2.isSuccess(), true); 160 | assert.equal(JSON.stringify(response2.data.response), JSON.stringify(cValue2)); 161 | }); 162 | 163 | it('should pass and get different values from different cache instances for different keys', async function() { 164 | let cKey1 = 'cache-key1' + keySuffix, 165 | cValue1 = 'value1', 166 | responseSet1 = await cacheObj1.set(cKey1, cValue1), 167 | response1 = await cacheObj1.get(cKey1), 168 | cKey2 = 'cache-key2' + keySuffix, 169 | cValue2 = 'value2', 170 | responseSet2 = await cacheObj2.set(cKey2, cValue2), 171 | response2 = await cacheObj2.get(cKey2); 172 | assert.equal(response1.isSuccess(), true); 173 | assert.equal(JSON.stringify(response1.data.response), JSON.stringify(cValue1)); 174 | assert.equal(response2.isSuccess(), true); 175 | assert.equal(JSON.stringify(response2.data.response), JSON.stringify(cValue2)); 176 | }); 177 | 178 | it('should fail when proper cache instances are not used', async function() { 179 | let cKey1 = 'cache-key1' + keySuffix, 180 | cValue1 = 'value1', 181 | responseSet1 = await cacheObj1.set(cKey1, cValue1), 182 | cKey2 = 'cache-key2' + keySuffix, 183 | cValue2 = 'value2', 184 | responseSet2 = await cacheObj2.set(cKey2, cValue2), 185 | response1 = await cacheObj2.get(cKey1), 186 | response2 = await cacheObj1.get(cKey2); 187 | assert.equal(response1.isSuccess(), true); 188 | assert.equal(response1.data.response, null); 189 | assert.equal(response2.isSuccess(), true); 190 | assert.equal(response2.data.response, null); 191 | }); 192 | }); 193 | } 194 | 195 | ostCache1 = OSTCache.getInstance(configStrategy1); 196 | cacheImplementer1 = ostCache1.cacheInstance; 197 | 198 | ostCache2 = OSTCache.getInstance(configStrategy2); 199 | cacheImplementer2 = ostCache2.cacheInstance; 200 | 201 | performTest(cacheImplementer1, 'ConsistentBehaviour'); 202 | performTest(cacheImplementer1, 'InconsistentBehaviour'); 203 | performMultipleTest(cacheImplementer1, cacheImplementer2, 'ConsistentBehaviour'); 204 | performMultipleTest(cacheImplementer1, cacheImplementer2, 'InconsistentBehaviour'); 205 | -------------------------------------------------------------------------------- /lib/cache/implementer/InMemory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementation of the caching layer using in-process memory. 3 | * NOTE: This should only be used for dev env having only one worker process, 4 | * otherwise this will result in inconsistent cache. 5 | * 6 | * @module lib/cache/implementer/InMemory 7 | */ 8 | 9 | const OSTBase = require('@ostdotcom/base'); 10 | 11 | const rootPrefix = '../../..', 12 | cacheHelper = require(rootPrefix + '/lib/cache/helper'), 13 | coreConstants = require(rootPrefix + '/config/coreConstant'), 14 | responseHelper = require(rootPrefix + '/lib/formatter/response'); 15 | 16 | const InstanceComposer = OSTBase.InstanceComposer; 17 | 18 | require(rootPrefix + '/config/cache'); 19 | 20 | /** 21 | * Class to manage cache in memory. 22 | * 23 | * @class InMemoryCacheImplementer 24 | */ 25 | class InMemoryCacheImplementer { 26 | /** 27 | * Constructor to manage cache in memory. 28 | * 29 | * @param {boolean} isConsistentBehaviour: specifies if the cache behaviour be consistent accross all cache engines 30 | * 31 | * @constructor 32 | */ 33 | constructor(isConsistentBehaviour) { 34 | const oThis = this; 35 | 36 | const cacheConfig = oThis.ic().getInstanceFor(coreConstants.icNameSpace, 'CacheConfigHelper'); 37 | 38 | oThis._records = Object.create(null); 39 | 40 | oThis.defaultLifetime = Number(cacheConfig.DEFAULT_TTL); 41 | oThis._isConsistentBehaviour = isConsistentBehaviour; 42 | } 43 | 44 | /** 45 | * Get the cached value of a key. 46 | * 47 | * @param {string} key: cache key 48 | * 49 | * @returns {Promise}: On success, data.value has value. On failure, error details returned. 50 | */ 51 | get(key) { 52 | const oThis = this; 53 | 54 | return new Promise(function(onResolve) { 55 | // Validate key and value 56 | if (cacheHelper.validateCacheKey(key) === false) { 57 | const errObj = responseHelper.error({ 58 | internal_error_identifier: 'l_c_im_g_1', 59 | api_error_identifier: 'invalid_cache_key', 60 | error_config: cacheHelper.fetchErrorConfig(), 61 | debug_options: { key: key } 62 | }); 63 | 64 | return onResolve(errObj); 65 | } 66 | 67 | // Perform action. 68 | const record = oThis._getRecord(key); 69 | const val = record ? record.getValue() : null; 70 | 71 | return onResolve(responseHelper.successWithData({ response: val })); 72 | }); 73 | } 74 | 75 | /** 76 | * Get the stored object value for the given key. 77 | * 78 | * @param {string} key: cache key 79 | * 80 | * @returns {Promise} - On success, data.value has value. On failure, error details returned. 81 | */ 82 | getObject(key) { 83 | const oThis = this; 84 | 85 | // Perform action. 86 | return oThis.get(key); 87 | } 88 | 89 | /** 90 | * Set a new key value or update the existing key value in cache 91 | * 92 | * @param {string} key: cache key 93 | * @param {*} value: data to be cached 94 | * @param {number} [ttl]: cache expiry in seconds. default: DEFAULT_TTL 95 | * 96 | * @returns {Promise} - On success, data.value is true. On failure, error details returned. 97 | */ 98 | set(key, value, ttl) { 99 | const oThis = this; 100 | 101 | return new Promise(function(onResolve) { 102 | // Validate key and value 103 | if (cacheHelper.validateCacheKey(key) === false) { 104 | const errObj = responseHelper.error({ 105 | internal_error_identifier: 'l_c_im_s_1', 106 | api_error_identifier: 'invalid_cache_key', 107 | error_config: cacheHelper.fetchErrorConfig(), 108 | debug_options: { key: key } 109 | }); 110 | 111 | return onResolve(errObj); 112 | } 113 | if (cacheHelper.validateCacheValue(value) === false) { 114 | const errObj = responseHelper.error({ 115 | internal_error_identifier: 'l_c_im_s_2', 116 | api_error_identifier: 'invalid_cache_value', 117 | error_config: cacheHelper.fetchErrorConfig(), 118 | debug_options: { key: key } 119 | }); 120 | 121 | return onResolve(errObj); 122 | } 123 | if (cacheHelper.validateCacheExpiry(ttl) === false) { 124 | ttl = oThis.defaultLifetime; 125 | } 126 | 127 | // Perform action 128 | let record = oThis._getRecord(key); 129 | if (record) { 130 | record.setValue(value); 131 | } else { 132 | record = new Record(value, ttl); 133 | } 134 | oThis._records[key] = record; 135 | 136 | return onResolve(responseHelper.successWithData({ response: true })); 137 | }); 138 | } 139 | 140 | /** 141 | * Cache object in cache 142 | * 143 | * @param {string} key: cache key 144 | * @param {*} object: object to be cached 145 | * @param {number} [ttl]: cache expiry in seconds. default: DEFAULT_TTL 146 | * 147 | * @returns {Promise}: On success, data.value is true. On failure, error details returned. 148 | */ 149 | setObject(key, object, ttl) { 150 | const oThis = this; 151 | 152 | // Validate value. 153 | if (typeof object !== 'object') { 154 | const errObj = responseHelper.error({ 155 | internal_error_identifier: 'l_c_im_so_1', 156 | api_error_identifier: 'invalid_cache_value', 157 | error_config: cacheHelper.fetchErrorConfig() 158 | }); 159 | 160 | return Promise.resolve(errObj); 161 | } 162 | 163 | // NOTE: To support redis implementation don't allow array. 164 | if (oThis._isConsistentBehaviour && Array.isArray(object)) { 165 | const errObj = responseHelper.error({ 166 | internal_error_identifier: 'l_c_im_so_2', 167 | api_error_identifier: 'array_is_invalid_cache_value', 168 | error_config: cacheHelper.fetchErrorConfig() 169 | }); 170 | 171 | return Promise.resolve(errObj); 172 | } 173 | 174 | // Perform action. 175 | return oThis.set(key, object, ttl); 176 | } 177 | 178 | /** 179 | * Delete the key from cache. 180 | * 181 | * @param {string} key: cache key 182 | * 183 | * @returns {Promise}: On success, data.value is true. On failure, error details returned. 184 | */ 185 | del(key) { 186 | const oThis = this; 187 | 188 | return new Promise(function(onResolve) { 189 | // Validate key and value 190 | if (cacheHelper.validateCacheKey(key) === false) { 191 | const errObj = responseHelper.error({ 192 | internal_error_identifier: 'l_c_im_d_1', 193 | api_error_identifier: 'invalid_cache_key', 194 | error_config: cacheHelper.fetchErrorConfig(), 195 | debug_options: { key: key } 196 | }); 197 | 198 | return onResolve(errObj); 199 | } 200 | 201 | // Perform action. 202 | const record = oThis._getRecord(key); 203 | if (record) { 204 | delete oThis._records[key]; 205 | } 206 | 207 | return onResolve(responseHelper.successWithData({ response: true })); 208 | }); 209 | } 210 | 211 | /** 212 | * Get the values of specified keys. 213 | * 214 | * @param {array} keys: cache keys 215 | * 216 | * @returns {Promise}: On success, data.value is object of keys and values. On failure, error details returned. 217 | */ 218 | multiGet(keys) { 219 | const oThis = this; 220 | 221 | return new Promise(function(onResolve) { 222 | // Validate keys 223 | if (!Array.isArray(keys) || keys.length === 0) { 224 | const errObj = responseHelper.error({ 225 | internal_error_identifier: 'l_c_im_mg_1', 226 | api_error_identifier: 'cache_keys_non_array', 227 | error_config: cacheHelper.fetchErrorConfig() 228 | }); 229 | 230 | return onResolve(errObj); 231 | } 232 | for (let index = 0; index < keys.length; index++) { 233 | if (cacheHelper.validateCacheKey(keys[index]) === false) { 234 | const errObj = responseHelper.error({ 235 | internal_error_identifier: 'l_c_im_mg_2', 236 | api_error_identifier: 'invalid_cache_key', 237 | error_config: cacheHelper.fetchErrorConfig(), 238 | debug_options: { invalid_key: keys[index] } 239 | }); 240 | 241 | return onResolve(errObj); 242 | } 243 | } 244 | 245 | // Perform action. 246 | const retVal = {}; 247 | for (let index = 0; index < keys.length; index++) { 248 | const key = keys[index]; 249 | const record = oThis._getRecord(key); 250 | let val = record ? record.getValue() : null; 251 | // Match behaviour with redis. 252 | val = typeof val === 'object' ? null : val; 253 | retVal[key] = val; 254 | } 255 | onResolve(responseHelper.successWithData({ response: retVal })); 256 | }); 257 | } 258 | 259 | /** 260 | * Increment the numeric value for the given key, if key already exists. 261 | * 262 | * @param {string} key: cache key 263 | * @param {int} byValue: number by which cache need to be incremented. Default: 1 264 | * 265 | * @returns {Promise}: On success, data.value is true. On failure, error details returned. 266 | */ 267 | increment(key, byValue) { 268 | const oThis = this; 269 | 270 | byValue = byValue === undefined ? 1 : byValue; 271 | 272 | return new Promise(async function(onResolve) { 273 | // Validate key and value 274 | if (cacheHelper.validateCacheKey(key) === false) { 275 | const errObj = responseHelper.error({ 276 | internal_error_identifier: 'l_c_im_i_1', 277 | api_error_identifier: 'invalid_cache_key', 278 | error_config: cacheHelper.fetchErrorConfig(), 279 | debug_options: { key: key } 280 | }); 281 | 282 | return onResolve(errObj); 283 | } 284 | if (byValue !== parseInt(byValue, 10) || byValue < 1 || cacheHelper.validateCacheValue(byValue) === false) { 285 | const errObj = responseHelper.error({ 286 | internal_error_identifier: 'l_c_im_i_2', 287 | api_error_identifier: 'non_int_cache_value', 288 | error_config: cacheHelper.fetchErrorConfig(), 289 | debug_options: { byValue: byValue } 290 | }); 291 | 292 | return onResolve(errObj); 293 | } 294 | 295 | // Check if key exists or not. 296 | let record = oThis._getRecord(key); 297 | 298 | if (!record) { 299 | // NOTE: To support memcached implementation. 300 | if (oThis._isConsistentBehaviour) { 301 | const errObj = responseHelper.error({ 302 | internal_error_identifier: 'l_c_im_i_3', 303 | api_error_identifier: 'missing_cache_key', 304 | error_config: cacheHelper.fetchErrorConfig(), 305 | debug_options: {} 306 | }); 307 | 308 | return onResolve(errObj); 309 | } 310 | 311 | // Set the record. 312 | const setResponse = await oThis.set(key, 0); 313 | if (setResponse.isFailure()) { 314 | return onResolve(setResponse); 315 | } 316 | record = oThis._getRecord(key); 317 | if (!record) { 318 | const errObj = responseHelper.error({ 319 | internal_error_identifier: 'l_c_im_i_4', 320 | api_error_identifier: 'missing_cache_key', 321 | error_config: cacheHelper.fetchErrorConfig(), 322 | debug_options: {} 323 | }); 324 | 325 | return onResolve(errObj); 326 | } 327 | } 328 | 329 | // Check exiting value is numeric. 330 | let originalValue = record.getValue(); 331 | if (isNaN(originalValue)) { 332 | const errObj = responseHelper.error({ 333 | internal_error_identifier: 'l_c_im_i_5', 334 | api_error_identifier: 'non_numeric_cache_value', 335 | error_config: cacheHelper.fetchErrorConfig(), 336 | debug_options: {} 337 | }); 338 | 339 | return onResolve(errObj); 340 | } 341 | 342 | // Perform action. 343 | originalValue += byValue; 344 | record.setValue(originalValue); 345 | 346 | return onResolve(responseHelper.successWithData({ response: originalValue })); 347 | }); 348 | } 349 | 350 | /** 351 | * Decrement the numeric value for the given key, if key already exists. 352 | * 353 | * @param {string} key: cache key 354 | * @param {int} byValue: number by which cache need to be decremented. Default: 1 355 | * 356 | * @returns {Promise}: On success, data.value is true. On failure, error details returned. 357 | */ 358 | decrement(key, byValue) { 359 | const oThis = this; 360 | 361 | byValue = byValue === undefined ? 1 : byValue; 362 | 363 | return new Promise(async function(onResolve) { 364 | // Validate key and value 365 | if (cacheHelper.validateCacheKey(key) === false) { 366 | const errObj = responseHelper.error({ 367 | internal_error_identifier: 'l_c_im_d_1', 368 | api_error_identifier: 'invalid_cache_key', 369 | error_config: cacheHelper.fetchErrorConfig(), 370 | debug_options: { key: key } 371 | }); 372 | 373 | return onResolve(errObj); 374 | } 375 | if (byValue !== parseInt(byValue, 10) || byValue < 1 || cacheHelper.validateCacheValue(byValue) === false) { 376 | const errObj = responseHelper.error({ 377 | internal_error_identifier: 'l_c_im_d_2', 378 | api_error_identifier: 'non_int_cache_value', 379 | error_config: cacheHelper.fetchErrorConfig(), 380 | debug_options: { byValue: byValue } 381 | }); 382 | 383 | return onResolve(errObj); 384 | } 385 | 386 | // Check if key exists or not. 387 | let record = oThis._getRecord(key); 388 | 389 | if (!record) { 390 | // NOTE: To support memcached implementation 391 | if (oThis._isConsistentBehaviour) { 392 | const errObj = responseHelper.error({ 393 | internal_error_identifier: 'l_c_im_d_3', 394 | api_error_identifier: 'missing_cache_key', 395 | error_config: cacheHelper.fetchErrorConfig(), 396 | debug_options: {} 397 | }); 398 | 399 | return onResolve(errObj); 400 | } 401 | 402 | // Set the record. 403 | const setResponse = await oThis.set(key, 0); 404 | if (setResponse.isFailure()) { 405 | return onResolve(setResponse); 406 | } 407 | record = oThis._getRecord(key); 408 | if (!record) { 409 | const errObj = responseHelper.error({ 410 | internal_error_identifier: 'l_c_im_d_4', 411 | api_error_identifier: 'missing_cache_key', 412 | error_config: cacheHelper.fetchErrorConfig(), 413 | debug_options: {} 414 | }); 415 | 416 | return onResolve(errObj); 417 | } 418 | } 419 | 420 | // Check exiting value is numeric. 421 | let originalValue = record.getValue(); 422 | if (isNaN(originalValue)) { 423 | const errObj = responseHelper.error({ 424 | internal_error_identifier: 'l_c_im_d_5', 425 | api_error_identifier: 'non_numeric_cache_value', 426 | error_config: cacheHelper.fetchErrorConfig(), 427 | debug_options: {} 428 | }); 429 | 430 | return onResolve(errObj); 431 | } 432 | 433 | // NOTE: To support memcached implementation. 434 | byValue = originalValue < byValue && oThis._isConsistentBehaviour ? originalValue : byValue; 435 | 436 | // Perform action. 437 | originalValue += byValue * -1; 438 | record.setValue(originalValue); 439 | 440 | return onResolve(responseHelper.successWithData({ response: originalValue })); 441 | }); 442 | } 443 | 444 | /** 445 | * Change the expiry time of an existing cache key. 446 | * 447 | * @param {string} key: cache key 448 | * @param {int} lifetime: new cache expiry in number of seconds 449 | * 450 | * @returns {Promise}: On success, data.value is true. On failure, error details returned. 451 | */ 452 | touch(key, lifetime) { 453 | const oThis = this; 454 | 455 | return new Promise(function(onResolve) { 456 | // Validate key and value. 457 | if (cacheHelper.validateCacheKey(key) === false) { 458 | const errObj = responseHelper.error({ 459 | internal_error_identifier: 'l_c_im_t_1', 460 | api_error_identifier: 'invalid_cache_key', 461 | error_config: cacheHelper.fetchErrorConfig(), 462 | debug_options: { key: key } 463 | }); 464 | 465 | return onResolve(errObj); 466 | } 467 | if (lifetime !== parseInt(lifetime, 10) || lifetime < 0 || cacheHelper.validateCacheValue(lifetime) === false) { 468 | const errObj = responseHelper.error({ 469 | internal_error_identifier: 'l_c_im_t_2', 470 | api_error_identifier: 'cache_expiry_nan', 471 | error_config: cacheHelper.fetchErrorConfig(), 472 | debug_options: { lifetime: lifetime } 473 | }); 474 | 475 | return onResolve(errObj); 476 | } 477 | 478 | const record = oThis._getRecord(key); 479 | 480 | if (!record) { 481 | const errObj = responseHelper.error({ 482 | internal_error_identifier: 'l_c_im_t_3', 483 | api_error_identifier: 'missing_cache_key', 484 | error_config: cacheHelper.fetchErrorConfig(), 485 | debug_options: {} 486 | }); 487 | 488 | return onResolve(errObj); 489 | } 490 | 491 | // Perform action. 492 | record.setExpires(lifetime); 493 | 494 | return onResolve(responseHelper.successWithData({ response: true })); 495 | }); 496 | } 497 | 498 | /** 499 | * Internal method to get record Object for a given key. 500 | * 501 | * @param {string} key: cache key 502 | * 503 | * @returns {object} 504 | */ 505 | _getRecord(key) { 506 | let record = null; 507 | if (key in this._records) { 508 | record = this._records[key]; 509 | if (record.hasExpired()) { 510 | delete this._records[key]; 511 | record = null; 512 | } 513 | } 514 | 515 | return record; 516 | } 517 | 518 | /** 519 | * Acquire lock on a given key. 520 | * 521 | * @param {string} key: cache key 522 | * @param {int} [ttl]: (in seconds) the time after which lock would be auto released. default: DEFAULT_TTL 523 | * 524 | * @returns {Promise}: Success if lock was acquired, else fails with error. 525 | */ 526 | acquireLock(key, ttl) { 527 | const oThis = this; 528 | 529 | return new Promise(function(onResolve) { 530 | // Validate key and value 531 | if (cacheHelper.validateCacheKey(key) === false) { 532 | const errObj = responseHelper.error({ 533 | internal_error_identifier: 'l_c_im_al_1', 534 | api_error_identifier: 'invalid_cache_key', 535 | error_config: cacheHelper.fetchErrorConfig(), 536 | debug_options: { key: key } 537 | }); 538 | 539 | return onResolve(errObj); 540 | } 541 | 542 | if (cacheHelper.validateCacheExpiry(ttl) === false) { 543 | ttl = oThis.defaultLifetime; 544 | } 545 | 546 | const record = oThis._getRecord(key); 547 | 548 | if (record) { 549 | const errObj = responseHelper.error({ 550 | internal_error_identifier: 'l_c_im_al_2', 551 | api_error_identifier: 'acquire_lock_failed', 552 | error_config: cacheHelper.fetchErrorConfig(), 553 | debug_options: {} 554 | }); 555 | 556 | return onResolve(errObj); 557 | } 558 | 559 | // Perform action. 560 | return oThis.set(key, 'LOCKED', ttl).then(function(response) { 561 | if (response.isFailure()) { 562 | return onResolve(response); 563 | } 564 | 565 | return onResolve(responseHelper.successWithData({ response: true })); 566 | }); 567 | }); 568 | } 569 | 570 | /** 571 | * Release lock on a given key. 572 | * 573 | * @param {string} key: cache key 574 | * 575 | * @returns {Promise}: release lock response. 576 | */ 577 | releaseLock(key) { 578 | const oThis = this; 579 | 580 | return oThis.del(key); 581 | } 582 | 583 | /** 584 | * Delete all keys from cache. 585 | * 586 | * @returns {Promise} 587 | */ 588 | delAll() { 589 | return new Promise(function(onResolve) { 590 | const errObj = responseHelper.error({ 591 | internal_error_identifier: 'l_c_im_d_6', 592 | api_error_identifier: 'flush_all_not_supported', 593 | error_config: cacheHelper.fetchErrorConfig(), 594 | debug_options: {} 595 | }); 596 | 597 | return onResolve(errObj); 598 | }); 599 | } 600 | } 601 | 602 | const farFutureLifeTime = Date.now() + 1000 * 60 * 60 * 24 * 365 * 20; 603 | 604 | function Record(value, lifetimeInSec) { 605 | this.setValue(value); 606 | lifetimeInSec && this.setExpires(lifetimeInSec); 607 | } 608 | 609 | Record.prototype = { 610 | constructor: Record, 611 | 612 | /** 613 | * @property val Value of record. Defaults to null. 614 | */ 615 | val: null, 616 | 617 | /** 618 | * @property expires Expiry timestamp of record. Defaults to farFutureLifeTime (20 years from server start time). 619 | */ 620 | expires: Date.now() + farFutureLifeTime, 621 | 622 | /** 623 | * Sets the expiry timestamp of the record. 624 | * 625 | * @param {number} lifetimeInSec: life-time is seconds after which record is considered expired. 626 | */ 627 | setExpires: function(lifetimeInSec) { 628 | lifetimeInSec = Number(lifetimeInSec); 629 | if (isNaN(lifetimeInSec)) { 630 | lifetimeInSec = 0; 631 | } 632 | 633 | const lifetime = lifetimeInSec * 1000; 634 | if (lifetime <= 0) { 635 | this.expires = Date.now(); 636 | } else { 637 | this.expires = Date.now() + lifetime; 638 | } 639 | }, 640 | 641 | /** 642 | * @returns {boolean} returns true if the current time is greater than expiry timestamp. 643 | */ 644 | hasExpired: function() { 645 | return this.expires - Date.now() <= 0; 646 | }, 647 | 648 | /** 649 | * @returns {*} returns the value set of the record. 650 | */ 651 | getValue: function() { 652 | return this.val; 653 | }, 654 | 655 | /** 656 | * Sets the value of the record. 657 | * 658 | * @param {*} val: Value to set. 659 | */ 660 | setValue: function(val) { 661 | this.val = val; 662 | }, 663 | 664 | /** 665 | * Returns the serialized value of record. 666 | * If value is Object, serialized object is returned. 667 | * 668 | * @returns {string} serialized value 669 | */ 670 | toString: function() { 671 | if (this.val instanceof Object) { 672 | return JSON.stringify(this.val); 673 | } 674 | 675 | return String(this.val); 676 | } 677 | }; 678 | 679 | InstanceComposer.registerAsShadowableClass( 680 | InMemoryCacheImplementer, 681 | coreConstants.icNameSpace, 682 | 'InMemoryCacheImplementer' 683 | ); 684 | 685 | module.exports = InMemoryCacheImplementer; 686 | -------------------------------------------------------------------------------- /lib/cache/implementer/Memcached.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementation of the caching layer using Memcached. 3 | * 4 | * @module lib/cache/implementer/Memcached 5 | */ 6 | 7 | const Memcached = require('memcached'), 8 | OSTBase = require('@ostdotcom/base'); 9 | 10 | // 11 | // , poolSize: 10 // maximal parallel connections 12 | // , retries: 5 // Connection pool retries to pull connection from pool 13 | // , factor: 3 // Connection pool retry exponential backoff factor 14 | // , minTimeout: 1000 // Connection pool retry min delay before retrying 15 | // , maxTimeout: 60000 // Connection pool retry max delay before retrying 16 | // , randomize: false // Connection pool retry timeout randomization 17 | // 18 | // , reconnect: 18000000 // if dead, attempt reconnect each xx ms 19 | // , timeout: 5000 // after x ms the server should send a timeout if we can't connect 20 | // , failures: 5 // Number of times a server can have an issue before marked dead 21 | // , failuresTimeout: 300000 // Time after which `failures` will be reset to original value, since last failure 22 | // , retry: 30000 // When a server has an error, wait this amount of time before retrying 23 | // , idle: 5000 // Remove connection from pool when no I/O after `idle` ms 24 | 25 | Object.assign(Memcached.config, { 26 | poolSize: 200, 27 | retries: 5, 28 | factor: 1, 29 | reconnect: 10000, 30 | timeout: 500, 31 | failures: 5, 32 | failuresTimeout: 300000, 33 | retry: 500 34 | }); 35 | 36 | const rootPrefix = '../../..', 37 | cacheHelper = require(rootPrefix + '/lib/cache/helper'), 38 | coreConstants = require(rootPrefix + '/config/coreConstant'), 39 | responseHelper = require(rootPrefix + '/lib/formatter/response'), 40 | logger = require(rootPrefix + '/lib/logger/customConsoleLogger'); 41 | 42 | const InstanceComposer = OSTBase.InstanceComposer; 43 | 44 | require(rootPrefix + '/config/cache'); 45 | 46 | /** 47 | * Class for memcached implementer. 48 | * 49 | * @class MemcachedCacheImplementer 50 | */ 51 | class MemcachedCacheImplementer { 52 | /** 53 | * Constructor for memcached implementer. 54 | * 55 | * @param {boolean} isConsistentBehaviour: specifies if the cache behaviour be consistent across all cache engines 56 | * 57 | * @constructor 58 | */ 59 | constructor(isConsistentBehaviour) { 60 | const oThis = this; 61 | 62 | const cacheConfig = oThis.ic().getInstanceFor(coreConstants.icNameSpace, 'CacheConfigHelper'); 63 | 64 | oThis.client = new Memcached(cacheConfig.MEMCACHE_SERVERS); 65 | // Error handling. 66 | oThis.client.on('issue', function(details) { 67 | let loggingStatement = 'Issue with Memcache server. Trying to resolve!'; 68 | if (details) { 69 | loggingStatement += ` Details: ${JSON.stringify(details)}`; 70 | // If failures has been marked twice, then increase retry time to 5 seconds. 71 | // if (details.totalFailures && details.totalFailures >= 1) { 72 | // oThis.client.retry = 5000; 73 | // } 74 | } 75 | logger.error(loggingStatement); 76 | }); 77 | oThis.client.on('failure', function(details) { 78 | let loggingStatement = 'A server has been marked as failure or dead!'; 79 | if (details) { 80 | loggingStatement += ` Details: ${JSON.stringify(details)}`; 81 | } 82 | logger.error(loggingStatement); 83 | }); 84 | oThis.client.on('reconnecting', function(details) { 85 | let loggingStatement = 'We are going to attempt to reconnect the to the failed server!'; 86 | if (details && details.server && details.totalDownTime) { 87 | loggingStatement += ` Total downtime caused by server: ${details.server} : ${details.totalDownTime} ms."`; 88 | } 89 | logger.info(loggingStatement); 90 | }); 91 | oThis.client.on('remove', function(details) { 92 | let loggingStatement = 'Removing the server from our consistent hashing!'; 93 | if (details) { 94 | loggingStatement += ` Details: ${JSON.stringify(details)}`; 95 | } 96 | logger.error(loggingStatement); 97 | }); 98 | oThis.client.on('reconnect', function(details) { 99 | let loggingStatement = 'Successfully reconnected to the memcached server!'; 100 | if (details) { 101 | loggingStatement += ` Details: ${JSON.stringify(details)}`; 102 | } 103 | logger.info(loggingStatement); 104 | }); 105 | 106 | oThis.defaultLifetime = Number(cacheConfig.DEFAULT_TTL); 107 | 108 | oThis._isConsistentBehaviour = isConsistentBehaviour; 109 | } 110 | 111 | /** 112 | * Get the cached value of a key. 113 | * 114 | * @param {string} key: cache key 115 | * 116 | * @return {Promise}: On success, data.value has value. On failure, error details returned. 117 | */ 118 | get(key) { 119 | const oThis = this; 120 | 121 | return new Promise(function(onResolve) { 122 | // Error handling 123 | if (cacheHelper.validateCacheKey(key) === false) { 124 | const errObj = responseHelper.error({ 125 | internal_error_identifier: 'l_c_m_g_1', 126 | api_error_identifier: 'invalid_cache_key', 127 | error_config: cacheHelper.fetchErrorConfig(), 128 | debug_options: { key: key } 129 | }); 130 | 131 | return onResolve(errObj); 132 | } 133 | 134 | // Set callback method. 135 | const callback = function(err, data) { 136 | if (err) { 137 | const errObj = responseHelper.error({ 138 | internal_error_identifier: 'l_c_m_g_2', 139 | api_error_identifier: 'something_went_wrong', 140 | error_config: cacheHelper.fetchErrorConfig(), 141 | debug_options: { 142 | error: err 143 | } 144 | }); 145 | 146 | return onResolve(errObj); 147 | } 148 | 149 | return onResolve(responseHelper.successWithData({ response: data === undefined ? null : data })); 150 | }; 151 | 152 | // Perform action. 153 | oThis.client.get(key, callback); 154 | }); 155 | } 156 | 157 | /** 158 | * Get the stored object value for the given key. 159 | * 160 | * @param {string} key: cache key 161 | * 162 | * @return {Promise}: On success, data.value has value. On failure, error details returned. 163 | */ 164 | getObject(key) { 165 | const oThis = this; 166 | 167 | // Perform action. 168 | return oThis.get(key); 169 | } 170 | 171 | /** 172 | * Set a new key value or update the existing key value in cache 173 | * 174 | * @param {string} key: cache key 175 | * @param {*} value: data to be cached 176 | * @param {number} [ttl]: cache expiry in seconds. default: DEFAULT_TTL 177 | * 178 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 179 | */ 180 | set(key, value, ttl) { 181 | const oThis = this; 182 | 183 | return new Promise(function(onResolve) { 184 | // Error handling 185 | if (cacheHelper.validateCacheKey(key) === false) { 186 | const errObj = responseHelper.error({ 187 | internal_error_identifier: 'l_c_m_s_1', 188 | api_error_identifier: 'invalid_cache_key', 189 | error_config: cacheHelper.fetchErrorConfig(), 190 | debug_options: { key: key } 191 | }); 192 | 193 | return onResolve(errObj); 194 | } 195 | if (cacheHelper.validateCacheValue(value) === false) { 196 | const errObj = responseHelper.error({ 197 | internal_error_identifier: 'l_c_m_s_2', 198 | api_error_identifier: 'invalid_cache_value', 199 | error_config: cacheHelper.fetchErrorConfig(), 200 | debug_options: { key: key } 201 | }); 202 | 203 | return onResolve(errObj); 204 | } 205 | if (cacheHelper.validateCacheExpiry(ttl) === false) { 206 | ttl = oThis.defaultLifetime; 207 | } 208 | 209 | // Set callback method. 210 | const callback = function(err) { 211 | console.log('\n\n-----------MemcacheSetErrorOccured-----------key----', key); 212 | console.log('-----------MemcacheSetErrorOccured-------1--------err----', err); 213 | console.log('-----------MemcacheSetErrorOccured-------2--------JSON.stringify(err)----', JSON.stringify(err)); 214 | if (err) { 215 | const errObj = responseHelper.error({ 216 | internal_error_identifier: 'l_c_m_s_3', 217 | api_error_identifier: 'something_went_wrong', 218 | error_config: cacheHelper.fetchErrorConfig(), 219 | debug_options: { err: err } 220 | }); 221 | 222 | return onResolve(errObj); 223 | } 224 | 225 | return onResolve(responseHelper.successWithData({ response: true })); 226 | }; 227 | 228 | // Perform action. 229 | oThis.client.set(key, value, ttl, callback); 230 | }); 231 | } 232 | 233 | /** 234 | * Cache object in cache 235 | * 236 | * @param {string} key: cache key 237 | * @param {*} object: object to be cached 238 | * @param {number} [ttl]: cache expiry in seconds. default: DEFAULT_TTL 239 | * 240 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 241 | */ 242 | setObject(key, object, ttl) { 243 | const oThis = this; 244 | 245 | // Validate value. 246 | if (typeof object !== 'object') { 247 | const errObj = responseHelper.error({ 248 | internal_error_identifier: 'l_c_m_so_1', 249 | api_error_identifier: 'invalid_cache_value', 250 | error_config: cacheHelper.fetchErrorConfig(), 251 | debug_options: { key: key } 252 | }); 253 | 254 | return Promise.resolve(errObj); 255 | } 256 | 257 | // NOTE: To support redis implementation don't allow array. 258 | if (oThis._isConsistentBehaviour && Array.isArray(object)) { 259 | const errObj = responseHelper.error({ 260 | internal_error_identifier: 'l_c_m_so_2', 261 | api_error_identifier: 'array_is_invalid_cache_value', 262 | error_config: cacheHelper.fetchErrorConfig(), 263 | debug_options: { key: key } 264 | }); 265 | 266 | return Promise.resolve(errObj); 267 | } 268 | 269 | // Perform action. 270 | return oThis.set(key, object, ttl); 271 | } 272 | 273 | /** 274 | * Delete the key from cache. 275 | * 276 | * @param {string} key: cache key 277 | * 278 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 279 | */ 280 | del(key) { 281 | const oThis = this; 282 | 283 | return new Promise(function(onResolve) { 284 | // Error handling 285 | if (cacheHelper.validateCacheKey(key) === false) { 286 | const errObj = responseHelper.error({ 287 | internal_error_identifier: 'l_c_m_d_1', 288 | api_error_identifier: 'invalid_cache_key', 289 | error_config: cacheHelper.fetchErrorConfig(), 290 | debug_options: { key: key } 291 | }); 292 | 293 | return onResolve(errObj); 294 | } 295 | 296 | // Set callback method. 297 | const callback = function(err) { 298 | if (err) { 299 | const errObj = responseHelper.error({ 300 | internal_error_identifier: 'l_c_m_d_2', 301 | api_error_identifier: 'something_went_wrong', 302 | error_config: cacheHelper.fetchErrorConfig(), 303 | debug_options: { err: err } 304 | }); 305 | onResolve(errObj); 306 | } else { 307 | onResolve(responseHelper.successWithData({ response: true })); 308 | } 309 | }; 310 | 311 | // Perform action. 312 | oThis.client.del(key, callback); 313 | }); 314 | } 315 | 316 | /** 317 | * Get the values of specified keys. 318 | * 319 | * @param {array} keys: cache keys 320 | * 321 | * @return {Promise}: On success, data.value is object of keys and values. On failure, error details returned. 322 | */ 323 | multiGet(keys) { 324 | const oThis = this; 325 | 326 | return new Promise(function(onResolve) { 327 | // Error handling. 328 | if (!Array.isArray(keys) || keys.length === 0) { 329 | const errObj = responseHelper.error({ 330 | internal_error_identifier: 'l_c_m_mg_1', 331 | api_error_identifier: 'cache_keys_non_array', 332 | error_config: cacheHelper.fetchErrorConfig(), 333 | debug_options: { keys: keys } 334 | }); 335 | 336 | return onResolve(errObj); 337 | } 338 | for (let index = 0; index < keys.length; index++) { 339 | if (cacheHelper.validateCacheKey(keys[index]) === false) { 340 | const errObj = responseHelper.error({ 341 | internal_error_identifier: 'l_c_m_mg_2', 342 | api_error_identifier: 'invalid_cache_key', 343 | error_config: cacheHelper.fetchErrorConfig(), 344 | debug_options: { invalid_key: keys[index] } 345 | }); 346 | 347 | return onResolve(errObj); 348 | } 349 | } 350 | 351 | // Set callback method. 352 | const callback = function(err, data) { 353 | if (err) { 354 | const errObj = responseHelper.error({ 355 | internal_error_identifier: 'l_c_m_g_2', 356 | api_error_identifier: 'something_went_wrong', 357 | error_config: cacheHelper.fetchErrorConfig(), 358 | debug_options: { err: err } 359 | }); 360 | 361 | return onResolve(errObj); 362 | } 363 | // Match behaviour with redis. 364 | for (let index = 0; index < keys.length; index++) { 365 | data[keys[index]] = 366 | typeof data[keys[index]] === 'object' || data[keys[index]] === undefined ? null : data[keys[index]]; 367 | } 368 | onResolve(responseHelper.successWithData({ response: data })); 369 | }; 370 | 371 | // Perform action. 372 | oThis.client.getMulti(keys, callback); 373 | }); 374 | } 375 | 376 | /** 377 | * Increment the numeric value for the given key, if key already exists. 378 | * 379 | * @param {string} key: cache key 380 | * @param {int} byValue: number by which cache need to be incremented. Default: 1 381 | * 382 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 383 | */ 384 | increment(key, byValue) { 385 | const oThis = this; 386 | 387 | byValue = byValue === undefined ? 1 : byValue; 388 | 389 | return new Promise(function(onResolve) { 390 | // Validate key and value 391 | if (cacheHelper.validateCacheKey(key) === false) { 392 | const errObj = responseHelper.error({ 393 | internal_error_identifier: 'l_c_m_i_1', 394 | api_error_identifier: 'invalid_cache_key', 395 | error_config: cacheHelper.fetchErrorConfig(), 396 | debug_options: { key: key } 397 | }); 398 | 399 | return onResolve(errObj); 400 | } 401 | if (byValue !== parseInt(byValue, 10) || byValue < 1 || cacheHelper.validateCacheValue(byValue) === false) { 402 | const errObj = responseHelper.error({ 403 | internal_error_identifier: 'l_c_m_i_2', 404 | api_error_identifier: 'non_int_cache_value', 405 | error_config: cacheHelper.fetchErrorConfig(), 406 | debug_options: { byValue: byValue } 407 | }); 408 | 409 | return onResolve(errObj); 410 | } 411 | 412 | // Set callback method. 413 | const callback = function(err, data) { 414 | if (err) { 415 | const errObj = responseHelper.error({ 416 | internal_error_identifier: 'l_c_m_i_3', 417 | api_error_identifier: 'something_went_wrong', 418 | error_config: cacheHelper.fetchErrorConfig(), 419 | debug_options: { err: err } 420 | }); 421 | 422 | return onResolve(errObj); 423 | } 424 | if (data === false) { 425 | const errObj = responseHelper.error({ 426 | internal_error_identifier: 'l_c_m_i_4', 427 | api_error_identifier: 'missing_cache_key', 428 | error_config: cacheHelper.fetchErrorConfig(), 429 | debug_options: {} 430 | }); 431 | 432 | return onResolve(errObj); 433 | } 434 | 435 | return onResolve(responseHelper.successWithData({ response: data })); 436 | }; 437 | 438 | // Perform action. 439 | oThis.client.incr(key, byValue, callback); 440 | }); 441 | } 442 | 443 | /** 444 | * Change the expiry time of an existing cache key. 445 | * 446 | * @param {string} key: cache key 447 | * @param {int} lifetime: new cache expiry in number of seconds 448 | * 449 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 450 | */ 451 | touch(key, lifetime) { 452 | const oThis = this; 453 | 454 | return new Promise(function(onResolve) { 455 | // Validate key and value 456 | if (cacheHelper.validateCacheKey(key) === false) { 457 | const errObj = responseHelper.error({ 458 | internal_error_identifier: 'l_c_m_t_1', 459 | api_error_identifier: 'invalid_cache_key', 460 | error_config: cacheHelper.fetchErrorConfig(), 461 | debug_options: { key: key } 462 | }); 463 | 464 | return onResolve(errObj); 465 | } 466 | if (lifetime !== parseInt(lifetime, 10) || lifetime < 0 || cacheHelper.validateCacheValue(lifetime) === false) { 467 | const errObj = responseHelper.error({ 468 | internal_error_identifier: 'l_c_m_t_2', 469 | api_error_identifier: 'cache_expiry_nan', 470 | error_config: cacheHelper.fetchErrorConfig(), 471 | debug_options: { lifetime: lifetime } 472 | }); 473 | 474 | return onResolve(errObj); 475 | } 476 | 477 | // Set callback method. 478 | const callback = function(err, data) { 479 | if (err) { 480 | const errObj = responseHelper.error({ 481 | internal_error_identifier: 'l_c_m_t_3', 482 | api_error_identifier: 'something_went_wrong', 483 | error_config: cacheHelper.fetchErrorConfig(), 484 | debug_options: { err: err } 485 | }); 486 | 487 | return onResolve(errObj); 488 | } 489 | if (data === false) { 490 | const errObj = responseHelper.error({ 491 | internal_error_identifier: 'l_c_m_t_4', 492 | api_error_identifier: 'missing_cache_key', 493 | error_config: cacheHelper.fetchErrorConfig(), 494 | debug_options: {} 495 | }); 496 | 497 | return onResolve(errObj); 498 | } 499 | 500 | return onResolve(responseHelper.successWithData({ response: data })); 501 | }; 502 | 503 | // NOTE: To support redis implementation. 504 | lifetime = lifetime === 0 && oThis._isConsistentBehaviour ? -1 : lifetime; 505 | 506 | // Perform action. 507 | oThis.client.touch(key, lifetime, callback); 508 | }); 509 | } 510 | 511 | /** 512 | * Decrement the numeric value for the given key, if key already exists. 513 | * 514 | * @param {string} key: cache key 515 | * @param {int} byValue: number by which cache need to be decremented. Default: 1 516 | * 517 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 518 | */ 519 | decrement(key, byValue) { 520 | const oThis = this; 521 | 522 | byValue = byValue === undefined ? 1 : byValue; 523 | 524 | return new Promise(function(onResolve) { 525 | // Validate key and value 526 | if (cacheHelper.validateCacheKey(key) === false) { 527 | const errObj = responseHelper.error({ 528 | internal_error_identifier: 'l_c_m_d_1', 529 | api_error_identifier: 'invalid_cache_key', 530 | error_config: cacheHelper.fetchErrorConfig(), 531 | debug_options: { key: key } 532 | }); 533 | 534 | return onResolve(errObj); 535 | } 536 | if (byValue !== parseInt(byValue, 10) || byValue < 1 || cacheHelper.validateCacheValue(byValue) === false) { 537 | const errObj = responseHelper.error({ 538 | internal_error_identifier: 'l_c_m_d_2', 539 | api_error_identifier: 'non_numeric_cache_value', 540 | error_config: cacheHelper.fetchErrorConfig(), 541 | debug_options: { byValue: byValue } 542 | }); 543 | 544 | return onResolve(errObj); 545 | } 546 | 547 | // Set callback method. 548 | const callback = function(err, data) { 549 | if (err) { 550 | const errObj = responseHelper.error({ 551 | internal_error_identifier: 'l_c_m_d_3', 552 | api_error_identifier: 'something_went_wrong', 553 | error_config: cacheHelper.fetchErrorConfig(), 554 | debug_options: { err: err } 555 | }); 556 | 557 | return onResolve(errObj); 558 | } 559 | if (data === false) { 560 | const errObj = responseHelper.error({ 561 | internal_error_identifier: 'l_c_m_d_4', 562 | api_error_identifier: 'missing_cache_key', 563 | error_config: cacheHelper.fetchErrorConfig(), 564 | debug_options: {} 565 | }); 566 | 567 | return onResolve(errObj); 568 | } 569 | 570 | return onResolve(responseHelper.successWithData({ response: data })); 571 | }; 572 | 573 | // Perform action. 574 | oThis.client.decr(key, byValue, callback); 575 | }); 576 | } 577 | 578 | /** 579 | * Acquire lock on a given key. 580 | * 581 | * @param {string} key: cache key 582 | * @param {int} [ttl]: (in seconds) the time after which lock would be auto released. default: DEFAULT_TTL 583 | * 584 | * @return {Promise}: success if lock was acquired, else fails with error. 585 | */ 586 | acquireLock(key, ttl) { 587 | const oThis = this; 588 | 589 | return new Promise(function(onResolve) { 590 | // Error handling 591 | if (cacheHelper.validateCacheKey(key) === false) { 592 | const errObj = responseHelper.error({ 593 | internal_error_identifier: 'l_c_m_al_1', 594 | api_error_identifier: 'invalid_cache_key', 595 | error_config: cacheHelper.fetchErrorConfig(), 596 | debug_options: { key: key } 597 | }); 598 | 599 | return onResolve(errObj); 600 | } 601 | 602 | if (cacheHelper.validateCacheExpiry(ttl) === false) { 603 | ttl = oThis.defaultLifetime; 604 | } 605 | 606 | // Set callback method. 607 | const callback = function(err) { 608 | if (err) { 609 | const errObj = responseHelper.error({ 610 | internal_error_identifier: 'l_c_m_al_2', 611 | api_error_identifier: 'acquire_lock_failed', 612 | error_config: cacheHelper.fetchErrorConfig(), 613 | debug_options: { err: err } 614 | }); 615 | 616 | return onResolve(errObj); 617 | } 618 | 619 | return onResolve(responseHelper.successWithData({ response: true })); 620 | }; 621 | 622 | // Perform action 623 | oThis.client.add(key, 'LOCKED', ttl, callback); 624 | }); 625 | } 626 | 627 | /** 628 | * Release lock on a given key. 629 | * 630 | * @param {string} key: cache key 631 | * 632 | * @return {Promise}: release lock response. 633 | */ 634 | releaseLock(key) { 635 | const oThis = this; 636 | 637 | return oThis.del(key); 638 | } 639 | 640 | /** 641 | * Delete all keys from cache. 642 | * 643 | * @return {Promise} 644 | */ 645 | delAll() { 646 | const oThis = this; 647 | 648 | return new Promise(function(onResolve) { 649 | oThis.client.flush(function(err) { 650 | if (err) { 651 | const errObj = responseHelper.error({ 652 | internal_error_identifier: 'l_c_r_t_5', 653 | api_error_identifier: 'something_went_wrong', 654 | error_config: cacheHelper.fetchErrorConfig(), 655 | debug_options: { err: err } 656 | }); 657 | 658 | return onResolve(errObj); 659 | } 660 | 661 | return onResolve(responseHelper.successWithData()); 662 | }); 663 | }); 664 | } 665 | } 666 | 667 | InstanceComposer.registerAsShadowableClass( 668 | MemcachedCacheImplementer, 669 | coreConstants.icNameSpace, 670 | 'MemcachedCacheImplementer' 671 | ); 672 | 673 | module.exports = MemcachedCacheImplementer; 674 | -------------------------------------------------------------------------------- /lib/cache/implementer/Redis.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementation of the caching layer using Redis. 3 | * A persistent Redis connection per Node js worker is maintained and this connection is singleton.

4 | * 5 | * @module lib/cache/implementer/Redis 6 | */ 7 | 8 | const redis = require('redis'), 9 | OSTBase = require('@ostdotcom/base'); 10 | 11 | const rootPrefix = '../../..', 12 | cacheHelper = require(rootPrefix + '/lib/cache/helper'), 13 | coreConstants = require(rootPrefix + '/config/coreConstant'), 14 | responseHelper = require(rootPrefix + '/lib/formatter/response'), 15 | logger = require(rootPrefix + '/lib/logger/customConsoleLogger'); 16 | 17 | const InstanceComposer = OSTBase.InstanceComposer; 18 | 19 | require(rootPrefix + '/config/cache'); 20 | 21 | /** 22 | * Class for redis cache implementer. 23 | * 24 | * @class RedisCacheImplementer 25 | */ 26 | class RedisCacheImplementer { 27 | /** 28 | * Constructor for redis cache implementer. 29 | * 30 | * @param {boolean} isConsistentBehaviour: specifies if the cache behaviour be consistent across all cache engines 31 | * 32 | * @constructor 33 | */ 34 | constructor(isConsistentBehaviour) { 35 | const oThis = this; 36 | 37 | const cacheConfig = oThis.ic().getInstanceFor(coreConstants.icNameSpace, 'CacheConfigHelper'); 38 | 39 | oThis.clientOptions = { 40 | host: cacheConfig.REDIS_HOST, 41 | port: cacheConfig.REDIS_PORT, 42 | password: cacheConfig.REDIS_PASS, 43 | tls: cacheConfig.REDIS_TLS_ENABLED 44 | }; 45 | 46 | oThis.client = redis.createClient(oThis.clientOptions); 47 | 48 | oThis.client.on('error', function(err) { 49 | logger.error('Redis Error ', err); 50 | }); 51 | 52 | oThis.defaultLifetime = Number(cacheConfig.DEFAULT_TTL); 53 | 54 | oThis._isConsistentBehaviour = isConsistentBehaviour; 55 | } 56 | 57 | /** 58 | * Get the cached value of a key. 59 | * 60 | * @param {string} key: cache key 61 | * 62 | * @return {Promise}: On success, data.value has value. On failure, error details returned. 63 | */ 64 | get(key) { 65 | const oThis = this; 66 | 67 | return new Promise(function(onResolve) { 68 | // Error handling. 69 | if (cacheHelper.validateCacheKey(key) === false) { 70 | const errObj = responseHelper.error({ 71 | internal_error_identifier: 'l_c_r_g_1', 72 | api_error_identifier: 'invalid_cache_key', 73 | error_config: cacheHelper.fetchErrorConfig(), 74 | debug_options: { key: key } 75 | }); 76 | 77 | return onResolve(errObj); 78 | } 79 | 80 | // Set callback method. 81 | const callback = function(err, data) { 82 | if (err) { 83 | const errObj = responseHelper.error({ 84 | internal_error_identifier: 'l_c_r_g_2', 85 | api_error_identifier: 'something_went_wrong', 86 | error_config: cacheHelper.fetchErrorConfig(), 87 | debug_options: { err: err } 88 | }); 89 | 90 | return onResolve(errObj); 91 | } 92 | 93 | return onResolve(responseHelper.successWithData({ response: data })); 94 | }; 95 | 96 | // Perform action. 97 | oThis.client.get(key, callback); 98 | }); 99 | } 100 | 101 | /** 102 | * Get the stored object value for the given key. 103 | * 104 | * @param {string} key: cache key 105 | * 106 | * @return {Promise}: On success, data.value has value. On failure, error details returned. 107 | */ 108 | getObject(key) { 109 | const oThis = this; 110 | 111 | return new Promise(function(onResolve) { 112 | // Error handling 113 | if (cacheHelper.validateCacheKey(key) === false) { 114 | const errObj = responseHelper.error({ 115 | internal_error_identifier: 'l_c_r_go_1', 116 | api_error_identifier: 'invalid_cache_key', 117 | error_config: cacheHelper.fetchErrorConfig(), 118 | debug_options: { key: key } 119 | }); 120 | 121 | return onResolve(errObj); 122 | } 123 | 124 | // Set callback method. 125 | const callback = function(err, data) { 126 | if (err) { 127 | const errObj = responseHelper.error({ 128 | internal_error_identifier: 'l_c_r_go_2', 129 | api_error_identifier: 'something_went_wrong', 130 | error_config: cacheHelper.fetchErrorConfig(), 131 | debug_options: { err: err } 132 | }); 133 | 134 | return onResolve(errObj); 135 | } 136 | // Format data. 137 | for (const dataKey in data) { 138 | data[dataKey] = JSON.parse(data[dataKey]); 139 | } 140 | 141 | return onResolve(responseHelper.successWithData({ response: data })); 142 | }; 143 | 144 | // Perform action. 145 | oThis.client.hgetall(key, callback); 146 | }); 147 | } 148 | 149 | /** 150 | * Set a new key value or update the existing key value in cache. 151 | * 152 | * @param {string} key: cache key 153 | * @param {*} value: data to be cached 154 | * @param {number} [ttl]: cache expiry in seconds. default: DEFAULT_TTL 155 | * 156 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 157 | */ 158 | set(key, value, ttl) { 159 | const oThis = this; 160 | 161 | return new Promise(function(onResolve) { 162 | // Error handling 163 | if (cacheHelper.validateCacheKey(key) === false) { 164 | const errObj = responseHelper.error({ 165 | internal_error_identifier: 'l_c_r_s_1', 166 | api_error_identifier: 'invalid_cache_key', 167 | error_config: cacheHelper.fetchErrorConfig(), 168 | debug_options: { key: key } 169 | }); 170 | 171 | return onResolve(errObj); 172 | } 173 | if (typeof value === 'object' || cacheHelper.validateCacheValue(value) === false) { 174 | const errObj = responseHelper.error({ 175 | internal_error_identifier: 'l_c_r_s_2', 176 | api_error_identifier: 'invalid_cache_value', 177 | error_config: cacheHelper.fetchErrorConfig(), 178 | debug_options: { key: key } 179 | }); 180 | 181 | return onResolve(errObj); 182 | } 183 | if (cacheHelper.validateCacheExpiry(ttl) === false) { 184 | ttl = oThis.defaultLifetime; 185 | } 186 | 187 | // Set callback method. 188 | const callback = function(err) { 189 | if (err) { 190 | const errObj = responseHelper.error({ 191 | internal_error_identifier: 'l_c_r_s_3', 192 | api_error_identifier: 'something_went_wrong', 193 | error_config: cacheHelper.fetchErrorConfig(), 194 | debug_options: { err: err } 195 | }); 196 | 197 | return onResolve(errObj); 198 | } 199 | 200 | return onResolve(responseHelper.successWithData({ response: true })); 201 | }; 202 | 203 | // Perform action. 204 | oThis.client.set(key, value, 'EX', ttl, callback); 205 | }); 206 | } 207 | 208 | /** 209 | * Cache object in cache. 210 | * 211 | * @param {string} key: cache key 212 | * @param {*} object: object to be cached 213 | * 214 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 215 | */ 216 | setObject(key, object) { 217 | const oThis = this; 218 | 219 | return new Promise(async function(onResolve) { 220 | // Error handling 221 | if (cacheHelper.validateCacheKey(key) === false) { 222 | const errObj = responseHelper.error({ 223 | internal_error_identifier: 'l_c_r_so_1', 224 | api_error_identifier: 'invalid_cache_key', 225 | error_config: cacheHelper.fetchErrorConfig(), 226 | debug_options: { key: key } 227 | }); 228 | 229 | return onResolve(errObj); 230 | } 231 | if (typeof object !== 'object' || Array.isArray(object) || cacheHelper.validateCacheValue(object) === false) { 232 | const errObj = responseHelper.error({ 233 | internal_error_identifier: 'l_c_r_so_2', 234 | api_error_identifier: 'invalid_cache_value', 235 | error_config: cacheHelper.fetchErrorConfig(), 236 | debug_options: {} 237 | }); 238 | 239 | return onResolve(errObj); 240 | } 241 | 242 | // NOTE: hmset is always merge the value of the object, never overwrite key. So, delete before set. 243 | await oThis.del(key); 244 | 245 | // Set callback method. 246 | const callback = function(err) { 247 | if (err) { 248 | const errObj = responseHelper.error({ 249 | internal_error_identifier: 'l_c_r_so_3', 250 | api_error_identifier: 'something_went_wrong', 251 | error_config: cacheHelper.fetchErrorConfig(), 252 | debug_options: { err: err } 253 | }); 254 | 255 | return onResolve(errObj); 256 | } 257 | 258 | return onResolve(responseHelper.successWithData({ response: true })); 259 | }; 260 | 261 | // Format data. 262 | const arrayRepresentation = []; 263 | for (const objectKey in object) { 264 | arrayRepresentation.push(objectKey); 265 | arrayRepresentation.push(JSON.stringify(object[objectKey])); 266 | } 267 | 268 | // Perform action. 269 | // NOTE: redis hmset does not support custom TTl as of now handle it when it does. 270 | oThis.client.hmset(key, arrayRepresentation, callback); 271 | }); 272 | } 273 | 274 | /** 275 | * Delete the key from cache. 276 | * 277 | * @param {string} key: cache key 278 | * 279 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 280 | */ 281 | del(key) { 282 | const oThis = this; 283 | 284 | return new Promise(function(onResolve) { 285 | // Error handling 286 | if (cacheHelper.validateCacheKey(key) === false) { 287 | const errObj = responseHelper.error({ 288 | internal_error_identifier: 'l_c_r_d_1', 289 | api_error_identifier: 'invalid_cache_key', 290 | error_config: cacheHelper.fetchErrorConfig(), 291 | debug_options: { key: key } 292 | }); 293 | 294 | return onResolve(errObj); 295 | } 296 | 297 | // Set callback method. 298 | const callback = function(err) { 299 | if (err) { 300 | const errObj = responseHelper.error({ 301 | internal_error_identifier: 'l_c_r_d_2', 302 | api_error_identifier: 'something_went_wrong', 303 | error_config: cacheHelper.fetchErrorConfig(), 304 | debug_options: { err: err } 305 | }); 306 | 307 | return onResolve(errObj); 308 | } 309 | 310 | return onResolve(responseHelper.successWithData({ response: true })); 311 | }; 312 | 313 | // Perform action. 314 | oThis.client.del(key, callback); 315 | }); 316 | } 317 | 318 | /** 319 | * Get the values of specified keys. 320 | * NOTE: Object cache retrieval is not support with multiGet. It returns null value, even if value is set in cache. 321 | * 322 | * @param {array} keys: cache keys 323 | * 324 | * @return {Promise}: On success, data.value is object of keys and values. On failure, error details returned. 325 | */ 326 | multiGet(keys) { 327 | const oThis = this; 328 | 329 | return new Promise(function(onResolve) { 330 | // Error handling 331 | if (!Array.isArray(keys) || keys.length === 0) { 332 | const errObj = responseHelper.error({ 333 | internal_error_identifier: 'l_c_r_mg_1', 334 | api_error_identifier: 'array_is_invalid_cache_value', 335 | error_config: cacheHelper.fetchErrorConfig(), 336 | debug_options: {} 337 | }); 338 | 339 | return onResolve(errObj); 340 | } 341 | for (let index = 0; index < keys.length; index++) { 342 | if (cacheHelper.validateCacheKey(keys[index]) === false) { 343 | const errObj = responseHelper.error({ 344 | internal_error_identifier: 'l_c_r_mg_2', 345 | api_error_identifier: 'invalid_cache_key', 346 | error_config: cacheHelper.fetchErrorConfig(), 347 | debug_options: { key: keys[index] } 348 | }); 349 | 350 | return onResolve(errObj); 351 | } 352 | } 353 | 354 | // Set callback method. 355 | const callback = function(err, data) { 356 | if (err) { 357 | const errObj = responseHelper.error({ 358 | internal_error_identifier: 'l_c_r_mg_3', 359 | api_error_identifier: 'something_went_wrong', 360 | error_config: cacheHelper.fetchErrorConfig(), 361 | debug_options: { err: err } 362 | }); 363 | 364 | return onResolve(errObj); 365 | } 366 | const retVal = {}; 367 | for (let index = 0; index < keys.length; index++) { 368 | retVal[keys[index]] = data[index]; 369 | } 370 | 371 | return onResolve(responseHelper.successWithData({ response: retVal })); 372 | }; 373 | 374 | // Perform action. 375 | oThis.client.mget(keys, callback); 376 | }); 377 | } 378 | 379 | /** 380 | * Increment the numeric value for the given key, if key already exists. 381 | * 382 | * @param {string} key: cache key 383 | * @param {int} byValue: number by which cache need to be incremented. Default: 1 384 | * 385 | * @return {Promise} - On success, data.value is true. On failure, error details returned. 386 | */ 387 | increment(key, byValue) { 388 | const oThis = this; 389 | 390 | byValue = byValue === undefined ? 1 : byValue; 391 | 392 | return new Promise(function(onResolve) { 393 | // Validate key and value 394 | if (cacheHelper.validateCacheKey(key) === false) { 395 | const errObj = responseHelper.error({ 396 | internal_error_identifier: 'l_c_r_i_1', 397 | api_error_identifier: 'invalid_cache_key', 398 | error_config: cacheHelper.fetchErrorConfig(), 399 | debug_options: { key: key } 400 | }); 401 | 402 | return onResolve(errObj); 403 | } 404 | if (byValue !== parseInt(byValue, 10) || byValue < 1 || cacheHelper.validateCacheValue(byValue) === false) { 405 | const errObj = responseHelper.error({ 406 | internal_error_identifier: 'l_c_r_i_2', 407 | api_error_identifier: 'non_int_cache_value', 408 | error_config: cacheHelper.fetchErrorConfig(), 409 | debug_options: { byValue: byValue } 410 | }); 411 | 412 | return onResolve(errObj); 413 | } 414 | 415 | // Set callback method. 416 | const callback = function(err, data) { 417 | if (err) { 418 | const errObj = responseHelper.error({ 419 | internal_error_identifier: 'l_c_r_i_3', 420 | api_error_identifier: 'something_went_wrong', 421 | error_config: cacheHelper.fetchErrorConfig(), 422 | debug_options: { err: err } 423 | }); 424 | 425 | return onResolve(errObj); 426 | } 427 | if (data === false) { 428 | const errObj = responseHelper.error({ 429 | internal_error_identifier: 'l_c_r_i_4', 430 | api_error_identifier: 'missing_cache_key', 431 | error_config: cacheHelper.fetchErrorConfig(), 432 | debug_options: {} 433 | }); 434 | 435 | return onResolve(errObj); 436 | } 437 | 438 | return onResolve(responseHelper.successWithData({ response: data })); 439 | }; 440 | 441 | if (oThis._isConsistentBehaviour) { 442 | // Perform action. 443 | const incrementValue = function(result) { 444 | if (result.isSuccess() && result.data.response > 0) { 445 | oThis.client.incrby(key, byValue, callback); 446 | } else { 447 | const errObj = responseHelper.error({ 448 | internal_error_identifier: 'l_c_m_i_5', 449 | api_error_identifier: 'missing_cache_key', 450 | error_config: cacheHelper.fetchErrorConfig(), 451 | debug_options: {} 452 | }); 453 | 454 | return onResolve(errObj); 455 | } 456 | }; 457 | 458 | // NOTE: To support memcached implementation. 459 | oThis.get(key).then(incrementValue); 460 | } else { 461 | oThis.client.incrby(key, byValue, callback); 462 | } 463 | }); 464 | } 465 | 466 | /** 467 | * Decrement the numeric value for the given key, if key already exists. 468 | * 469 | * @param {string} key:cache key 470 | * @param {int} byValue: number by which cache need to be decremented. Default: 1 471 | * 472 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 473 | */ 474 | decrement(key, byValue) { 475 | const oThis = this; 476 | 477 | byValue = byValue === undefined ? 1 : byValue; 478 | 479 | return new Promise(function(onResolve) { 480 | // Validate key and value 481 | if (cacheHelper.validateCacheKey(key) === false) { 482 | const errObj = responseHelper.error({ 483 | internal_error_identifier: 'l_c_r_d_1', 484 | api_error_identifier: 'invalid_cache_key', 485 | error_config: cacheHelper.fetchErrorConfig(), 486 | debug_options: { key: key } 487 | }); 488 | 489 | return onResolve(errObj); 490 | } 491 | if (byValue !== parseInt(byValue, 10) || byValue < 1 || cacheHelper.validateCacheValue(byValue) === false) { 492 | const errObj = responseHelper.error({ 493 | internal_error_identifier: 'l_c_r_d_2', 494 | api_error_identifier: 'non_int_cache_value', 495 | error_config: cacheHelper.fetchErrorConfig(), 496 | debug_options: { byValue: byValue } 497 | }); 498 | 499 | return onResolve(errObj); 500 | } 501 | 502 | // Set callback method. 503 | const callback = function(err, data) { 504 | if (err) { 505 | const errObj = responseHelper.error({ 506 | internal_error_identifier: 'l_c_r_d_3', 507 | api_error_identifier: 'something_went_wrong', 508 | error_config: cacheHelper.fetchErrorConfig(), 509 | debug_options: { err: err } 510 | }); 511 | 512 | return onResolve(errObj); 513 | } 514 | if (data === false) { 515 | const errObj = responseHelper.error({ 516 | internal_error_identifier: 'l_c_r_d_4', 517 | api_error_identifier: 'missing_cache_key', 518 | error_config: cacheHelper.fetchErrorConfig(), 519 | debug_options: {} 520 | }); 521 | 522 | return onResolve(errObj); 523 | } 524 | 525 | return onResolve(responseHelper.successWithData({ response: data })); 526 | }; 527 | 528 | if (oThis._isConsistentBehaviour) { 529 | // Perform action. 530 | const decrementValue = function(result) { 531 | if (result.isSuccess() && result.data.response > 0) { 532 | // NOTE: To support memcached implementation. 533 | byValue = result.data.response < byValue ? result.data.response : byValue; 534 | // Decrement. 535 | oThis.client.decrby(key, byValue, callback); 536 | } else { 537 | const errObj = responseHelper.error({ 538 | internal_error_identifier: 'l_c_m_d_5', 539 | api_error_identifier: 'missing_cache_key', 540 | error_config: cacheHelper.fetchErrorConfig(), 541 | debug_options: {} 542 | }); 543 | 544 | return onResolve(errObj); 545 | } 546 | }; 547 | 548 | // NOTE: To support memcached implementation. 549 | oThis.get(key).then(decrementValue); 550 | } else { 551 | oThis.client.decrby(key, byValue, callback); 552 | } 553 | }); 554 | } 555 | 556 | /** 557 | * Change the expiry time of an existing cache key. 558 | * 559 | * @param {string} key: cache key 560 | * @param {int} lifetime: new cache expiry in number of seconds 561 | * 562 | * @return {Promise}: On success, data.value is true. On failure, error details returned. 563 | */ 564 | touch(key, lifetime) { 565 | const oThis = this; 566 | 567 | return new Promise(function(onResolve) { 568 | // Validate key and value 569 | if (cacheHelper.validateCacheKey(key) === false) { 570 | const errObj = responseHelper.error({ 571 | internal_error_identifier: 'l_c_r_t_1', 572 | api_error_identifier: 'invalid_cache_key', 573 | error_config: cacheHelper.fetchErrorConfig(), 574 | debug_options: { key: key } 575 | }); 576 | 577 | return onResolve(errObj); 578 | } 579 | if (lifetime !== parseInt(lifetime, 10) || lifetime < 0 || cacheHelper.validateCacheValue(lifetime) === false) { 580 | const errObj = responseHelper.error({ 581 | internal_error_identifier: 'l_c_r_t_2', 582 | api_error_identifier: 'cache_expiry_nan', 583 | error_config: cacheHelper.fetchErrorConfig(), 584 | debug_options: { lifetime: lifetime } 585 | }); 586 | 587 | return onResolve(errObj); 588 | } 589 | 590 | // Set callback method. 591 | const callback = function(err, data) { 592 | if (err) { 593 | const errObj = responseHelper.error({ 594 | internal_error_identifier: 'l_c_r_t_3', 595 | api_error_identifier: 'something_went_wrong', 596 | error_config: cacheHelper.fetchErrorConfig(), 597 | debug_options: { err: err } 598 | }); 599 | 600 | return onResolve(errObj); 601 | } 602 | if (data === 0) { 603 | const errObj = responseHelper.error({ 604 | internal_error_identifier: 'l_c_r_t_4', 605 | api_error_identifier: 'missing_cache_key', 606 | error_config: cacheHelper.fetchErrorConfig(), 607 | debug_options: {} 608 | }); 609 | 610 | return onResolve(errObj); 611 | } 612 | 613 | return onResolve(responseHelper.successWithData({ response: data })); 614 | }; 615 | 616 | // Perform action. 617 | oThis.client.expire(key, lifetime, callback); 618 | }); 619 | } 620 | 621 | /** 622 | * Acquire lock on a given key. 623 | * 624 | * @param {string} key: cache key 625 | * @param {int} [ttl]: (in seconds) the time after which lock would be auto released. default: DEFAULT_TTL 626 | * 627 | * @return {Promise}: success if lock was acquired, else fails with error. 628 | */ 629 | acquireLock(key, ttl) { 630 | const oThis = this; 631 | 632 | return new Promise(function(onResolve) { 633 | // Error handling 634 | if (cacheHelper.validateCacheKey(key) === false) { 635 | const errObj = responseHelper.error({ 636 | internal_error_identifier: 'l_c_r_al_1', 637 | api_error_identifier: 'invalid_cache_key', 638 | error_config: cacheHelper.fetchErrorConfig(), 639 | debug_options: { key: key } 640 | }); 641 | 642 | return onResolve(errObj); 643 | } 644 | 645 | if (cacheHelper.validateCacheExpiry(ttl) === false) { 646 | ttl = oThis.defaultLifetime; 647 | } 648 | 649 | // Set callback method. 650 | const callback = function(err, data) { 651 | if (err) { 652 | const errObj = responseHelper.error({ 653 | internal_error_identifier: 'l_c_r_al_2', 654 | api_error_identifier: 'something_went_wrong', 655 | error_config: cacheHelper.fetchErrorConfig(), 656 | debug_options: { err: err } 657 | }); 658 | 659 | return onResolve(errObj); 660 | } 661 | if (data === 'OK') { 662 | return onResolve(responseHelper.successWithData({ response: true })); 663 | } 664 | const errObj = responseHelper.error({ 665 | internal_error_identifier: 'l_c_r_al_3', 666 | api_error_identifier: 'acquire_lock_failed', 667 | error_config: cacheHelper.fetchErrorConfig(), 668 | debug_options: { data: data } 669 | }); 670 | 671 | return onResolve(errObj); 672 | }; 673 | 674 | // Perform action. 675 | oThis.client.set(key, 'LOCKED', 'NX', 'EX', ttl, callback); 676 | }); 677 | } 678 | 679 | /** 680 | * Release lock on a given key. 681 | * 682 | * @param {string} key: cache key 683 | * 684 | * @return {Promise}: release lock response. 685 | */ 686 | releaseLock(key) { 687 | const oThis = this; 688 | 689 | return oThis.del(key); 690 | } 691 | 692 | /** 693 | * Delete all keys from cache. 694 | * 695 | * @return {Promise} 696 | */ 697 | delAll() { 698 | const oThis = this; 699 | 700 | return new Promise(function(onResolve) { 701 | oThis.client.flushdb(function(err) { 702 | if (err) { 703 | const errObj = responseHelper.error({ 704 | internal_error_identifier: 'l_c_r_t_5', 705 | api_error_identifier: 'flush_all_keys_failed', 706 | error_config: cacheHelper.fetchErrorConfig(), 707 | debug_options: {} 708 | }); 709 | 710 | return onResolve(errObj); 711 | } 712 | 713 | return onResolve(responseHelper.successWithData()); 714 | }); 715 | }); 716 | } 717 | } 718 | 719 | InstanceComposer.registerAsShadowableClass(RedisCacheImplementer, coreConstants.icNameSpace, 'RedisCacheImplementer'); 720 | 721 | module.exports = RedisCacheImplementer; 722 | --------------------------------------------------------------------------------