UNPKG

@splitsoftware/splitio-commons

Version:
196 lines (195 loc) 9.71 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisAdapter = void 0; var tslib_1 = require("tslib"); var ioredis_1 = (0, tslib_1.__importDefault)(require("ioredis")); var lang_1 = require("../../utils/lang"); var thenable_1 = require("../../utils/promise/thenable"); var timeout_1 = require("../../utils/promise/timeout"); var sets_1 = require("../../utils/lang/sets"); var LOG_PREFIX = 'storage:redis-adapter: '; // If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis. var METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw']; var METHODS_TO_PROMISE_WRAP_EXEC = ['pipeline']; // Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere. var DEFAULT_OPTIONS = { connectionTimeout: 10000, operationTimeout: 5000 }; // Library specifics. var DEFAULT_LIBRARY_OPTIONS = { enableOfflineQueue: false, connectTimeout: DEFAULT_OPTIONS.connectionTimeout, lazyConnect: false }; /** * Redis adapter on top of the library of choice (written with ioredis) for some extra control. */ var RedisAdapter = /** @class */ (function (_super) { (0, tslib_1.__extends)(RedisAdapter, _super); function RedisAdapter(log, storageSettings) { if (storageSettings === void 0) { storageSettings = {}; } var _this = this; var options = RedisAdapter._defineOptions(storageSettings); // Call the ioredis constructor _this = _super.apply(this, RedisAdapter._defineLibrarySettings(options)) || this; _this.log = log; _this._options = options; _this._notReadyCommandsQueue = []; _this._runningCommands = new Set(); _this._listenToEvents(); _this._setTimeoutWrappers(); _this._setDisconnectWrapper(); return _this; } RedisAdapter.prototype._listenToEvents = function () { var _this = this; this.once('ready', function () { var commandsCount = _this._notReadyCommandsQueue ? _this._notReadyCommandsQueue.length : 0; _this.log.info(LOG_PREFIX + ("Redis connection established. Queued commands: " + commandsCount + ".")); _this._notReadyCommandsQueue && _this._notReadyCommandsQueue.forEach(function (queued) { _this.log.info(LOG_PREFIX + ("Executing queued " + queued.name + " command.")); queued.command().then(queued.resolve).catch(queued.reject); }); // After the SDK is ready for the first time we'll stop queueing commands. This is just so we can keep handling BUR for them. _this._notReadyCommandsQueue = undefined; }); this.once('close', function () { _this.log.info(LOG_PREFIX + 'Redis connection closed.'); }); }; RedisAdapter.prototype._setTimeoutWrappers = function () { var instance = this; var wrapCommand = function (originalMethod, methodName) { // The value of "this" in this function should be the instance actually executing the method. It might be the instance referred (the base one) // or it can be the instance of a Pipeline object. return function () { var params = arguments; var caller = this; function commandWrapper() { instance.log.debug(LOG_PREFIX + "Executing " + methodName + "."); var result = originalMethod.apply(caller, params); if ((0, thenable_1.thenable)(result)) { // For handling pending commands on disconnect, add to the set and remove once finished. // On sync commands there's no need, only thenables. instance._runningCommands.add(result); var cleanUpRunningCommandsCb = function () { instance._runningCommands.delete(result); }; // Both success and error remove from queue. result.then(cleanUpRunningCommandsCb, cleanUpRunningCommandsCb); return (0, timeout_1.timeout)(instance._options.operationTimeout, result).catch(function (err) { instance.log.error("" + LOG_PREFIX + methodName + " operation threw an error or exceeded configured timeout of " + instance._options.operationTimeout + "ms. Message: " + err); // Handling is not the adapter responsibility. throw err; }); } return result; } if (instance._notReadyCommandsQueue) { return new Promise(function (resolve, reject) { instance._notReadyCommandsQueue.unshift({ resolve: resolve, reject: reject, command: commandWrapper, name: methodName.toUpperCase() }); }); } else { return commandWrapper(); } }; }; // Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands. METHODS_TO_PROMISE_WRAP.forEach(function (methodName) { var originalFn = instance[methodName]; instance[methodName] = wrapCommand(originalFn, methodName); }); // Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away. METHODS_TO_PROMISE_WRAP_EXEC.forEach(function (methodName) { var originalFn = instance[methodName]; // "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable. instance[methodName] = function () { var res = originalFn.apply(instance, arguments); var originalExec = res.exec; res.exec = wrapCommand(originalExec, methodName + '.exec').bind(res); return res; }; }); }; RedisAdapter.prototype._setDisconnectWrapper = function () { var instance = this; var originalMethod = instance.disconnect; instance.disconnect = function disconnect() { var params = []; for (var _i = 0; _i < arguments.length; _i++) { params[_i] = arguments[_i]; } setTimeout(function deferredDisconnect() { if (instance._runningCommands.size > 0) { instance.log.info(LOG_PREFIX + ("Attempting to disconnect but there are " + instance._runningCommands.size + " commands still waiting for resolution. Defering disconnection until those finish.")); Promise.all((0, sets_1.setToArray)(instance._runningCommands)) .then(function () { instance.log.debug(LOG_PREFIX + 'Pending commands finished successfully, disconnecting.'); originalMethod.apply(instance, params); }) .catch(function (e) { instance.log.warn(LOG_PREFIX + ("Pending commands finished with error: " + e + ". Proceeding with disconnection.")); originalMethod.apply(instance, params); }); } else { instance.log.debug(LOG_PREFIX + 'No commands pending execution, disconnect.'); // Nothing pending, just proceed. originalMethod.apply(instance, params); } }, 10); }; }; /** * Receives the options and returns an array of parameters for the ioredis constructor. * Keeping both redis setup options for backwards compatibility. */ RedisAdapter._defineLibrarySettings = function (options) { var opts = (0, lang_1.merge)({}, DEFAULT_LIBRARY_OPTIONS); var result = [opts]; if (!(0, lang_1.isString)(options.url)) { (0, lang_1.merge)(opts, { host: options.host, port: options.port, db: options.db, password: options.pass }); } else { // If it IS the string URL, that'll be the first param for ioredis. result.unshift(options.url); } if (options.connectionTimeout) { (0, lang_1.merge)(opts, { connectTimeout: options.connectionTimeout }); } if (options.tls) { (0, lang_1.merge)(opts, { tls: options.tls }); } return result; }; /** * Parses the options into what we care about. */ RedisAdapter._defineOptions = function (_a) { var connectionTimeout = _a.connectionTimeout, operationTimeout = _a.operationTimeout, url = _a.url, host = _a.host, port = _a.port, db = _a.db, pass = _a.pass, tls = _a.tls; var parsedOptions = { connectionTimeout: connectionTimeout, operationTimeout: operationTimeout, url: url, host: host, port: port, db: db, pass: pass, tls: tls }; return (0, lang_1.merge)({}, DEFAULT_OPTIONS, parsedOptions); }; return RedisAdapter; }(ioredis_1.default)); exports.RedisAdapter = RedisAdapter;