UNPKG

@splitsoftware/splitio-commons

Version:
244 lines (243 loc) 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DefinitionsCacheInRedis = void 0; var tslib_1 = require("tslib"); var lang_1 = require("../../utils/lang"); var constants_1 = require("./constants"); var AbstractDefinitionsCacheAsync_1 = require("../AbstractDefinitionsCacheAsync"); var sets_1 = require("../../utils/lang/sets"); /** * Discard errors for an answer of multiple operations. */ function processPipelineAnswer(results) { return results ? results.reduce(function (accum, errValuePair) { if (errValuePair[0] === null) accum.push(errValuePair[1]); return accum; }, []) : []; } /** * IDefinitionsCacheAsync implementation that stores definitions in Redis. * Supported by Node.js */ var DefinitionsCacheInRedis = /** @class */ (function (_super) { tslib_1.__extends(DefinitionsCacheInRedis, _super); function DefinitionsCacheInRedis(log, keys, redis, splitFiltersValidation) { var _this = _super.call(this) || this; _this.log = log; _this.redis = redis; _this.keys = keys; _this.setsFilter = splitFiltersValidation ? splitFiltersValidation.groupedFilters.bySet : []; // There is no need to listen for redis 'error' event, because in that case ioredis calls will be rejected and handled by redis storage adapters. // But it is done just to avoid getting the ioredis message `Unhandled error event`. _this.redis.on('error', function (e) { _this.redisError = e; }); _this.redis.on('connect', function () { _this.redisError = undefined; }); return _this; } DefinitionsCacheInRedis.prototype._decrementCounts = function (definition) { var _this = this; var ttKey = this.keys.buildTrafficTypeKey(definition.trafficTypeName); return this.redis.decr(ttKey).then(function (count) { if (count === 0) return _this.redis.del(ttKey); }); }; DefinitionsCacheInRedis.prototype._incrementCounts = function (definition) { var ttKey = this.keys.buildTrafficTypeKey(definition.trafficTypeName); return this.redis.incr(ttKey); }; DefinitionsCacheInRedis.prototype._updateSets = function (definitionName, setsOfRemovedDefinition, setsOfAddedDefinition) { var _this = this; var removeFromSets = (0, sets_1.returnDifference)(setsOfRemovedDefinition, setsOfAddedDefinition); var addToSets = (0, sets_1.returnDifference)(setsOfAddedDefinition, setsOfRemovedDefinition); if (this.setsFilter.length > 0) { addToSets = addToSets.filter(function (set) { return _this.setsFilter.some(function (filterSet) { return filterSet === set; }); }); } var items = [definitionName]; return Promise.all(tslib_1.__spreadArray(tslib_1.__spreadArray([], removeFromSets.map(function (setName) { return _this.redis.srem(_this.keys.buildSetKey(setName), items); }), true), addToSets.map(function (setName) { return _this.redis.sadd(_this.keys.buildSetKey(setName), items); }), true)); }; /** * Add a given definition. * The returned promise is resolved when the operation success * or rejected if it fails (e.g., redis operation fails) */ DefinitionsCacheInRedis.prototype.add = function (definition) { var _this = this; var name = definition.name; var definitionKey = this.keys.buildDefinitionKey(name); return this.redis.get(definitionKey).then(function (definitionFromStorage) { // handling parsing error var parsedPreviousDefinition, stringifiedNewDefinition; try { parsedPreviousDefinition = definitionFromStorage ? JSON.parse(definitionFromStorage) : undefined; stringifiedNewDefinition = JSON.stringify(definition); } catch (e) { throw new Error('Error parsing feature flag definition: ' + e); } return _this.redis.set(definitionKey, stringifiedNewDefinition).then(function () { // avoid unnecessary increment/decrement operations if (parsedPreviousDefinition && parsedPreviousDefinition.trafficTypeName === definition.trafficTypeName) return; // update traffic type counts return _this._incrementCounts(definition).then(function () { if (parsedPreviousDefinition) return _this._decrementCounts(parsedPreviousDefinition); }); }).then(function () { return _this._updateSets(name, parsedPreviousDefinition && parsedPreviousDefinition.sets, definition.sets); }); }).then(function () { return true; }); }; /** * Remove a given definition. * The returned promise is resolved when the operation success, with true or false indicating if the definition existed (and was removed) or not. * or rejected if it fails (e.g., redis operation fails). */ DefinitionsCacheInRedis.prototype.remove = function (name) { var _this = this; return this.get(name).then(function (definition) { if (definition) { return _this._decrementCounts(definition).then(function () { return _this._updateSets(name, definition.sets); }); } }).then(function () { return _this.redis.del(_this.keys.buildDefinitionKey(name)).then(function (status) { return status === 1; }); }); }; /** * Get definition or null if it's not defined. * Returned promise is rejected if redis operation fails. */ DefinitionsCacheInRedis.prototype.get = function (name) { if (this.redisError) { this.log.error(constants_1.LOG_PREFIX + this.redisError); return Promise.reject(this.redisError); } return this.redis.get(this.keys.buildDefinitionKey(name)) .then(function (maybeDefinition) { return maybeDefinition && JSON.parse(maybeDefinition); }); }; /** * Set till number. * The returned promise is resolved when the operation success, * or rejected if it fails. */ DefinitionsCacheInRedis.prototype.setChangeNumber = function (changeNumber) { return this.redis.set(this.keys.buildDefinitionsTillKey(), changeNumber + '').then(function (status) { return status === 'OK'; }); }; /** * Get till number or -1 if it's not defined. * The returned promise is resolved with the changeNumber or -1 if it doesn't exist or a redis operation fails. * The promise will never be rejected. */ DefinitionsCacheInRedis.prototype.getChangeNumber = function () { var _this = this; return this.redis.get(this.keys.buildDefinitionsTillKey()).then(function (value) { var i = parseInt(value, 10); return (0, lang_1.isNaNNumber)(i) ? -1 : i; }).catch(function (e) { _this.log.error(constants_1.LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); return -1; }); }; /** * Get list of all definitions. * The returned promise is resolved with the list of definitions, * or rejected if redis operation fails. */ // @TODO we need to benchmark which is the maximun number of commands we could pipeline without kill redis performance. DefinitionsCacheInRedis.prototype.getAll = function () { var _this = this; return this.redis.keys(this.keys.searchPatternForDefinitionKeys()) .then(function (listOfKeys) { return _this.redis.pipeline(listOfKeys.map(function (k) { return ['get', k]; })).exec(); }) .then(processPipelineAnswer) .then(function (definitions) { return definitions.map(function (definition) { return JSON.parse(definition); }); }); }; /** * Get list of definition names. * The returned promise is resolved with the list of names, * or rejected if redis operation fails. */ DefinitionsCacheInRedis.prototype.getNames = function () { var _this = this; return this.redis.keys(this.keys.searchPatternForDefinitionKeys()).then(function (listOfKeys) { return listOfKeys.map(_this.keys.extractKey); }); }; /** * Get list of definition names related to a given list of set names. * The returned promise is resolved with the list of names per set, * or rejected if the pipelined redis operation fails (e.g., timeout). */ DefinitionsCacheInRedis.prototype.getNamesBySets = function (sets) { var _this = this; return this.redis.pipeline(sets.map(function (set) { return ['smembers', _this.keys.buildSetKey(set)]; })).exec() .then(function (results) { return results ? results.map(function (_a, index) { var e = _a[0], value = _a[1]; if (e === null) return value; _this.log.error(constants_1.LOG_PREFIX + "Could not read result from get members of set ".concat(sets[index], " due to an error: ").concat(e)); }) : []; }) .then(function (namesBySets) { return namesBySets.map(function (namesBySet) { return new Set(namesBySet); }); }); }; /** * Check traffic type existence. * The returned promise is resolved with a boolean indicating whether the TT exist or not. * In case of redis operation failure, the promise resolves with a true value, assuming that the TT might exist. * It will never be rejected. */ DefinitionsCacheInRedis.prototype.trafficTypeExists = function (trafficType) { var _this = this; // If there is a number there should be > 0, otherwise the TT is considered as not existent. return this.redis.get(this.keys.buildTrafficTypeKey(trafficType)) .then(function (ttCount) { if (ttCount === null) return false; // if entry doesn't exist, means that TT doesn't exist ttCount = parseInt(ttCount, 10); if (!(0, lang_1.isFiniteNumber)(ttCount) || ttCount < 0) { _this.log.info(constants_1.LOG_PREFIX + "Could not validate traffic type existence of ".concat(trafficType, " due to data corruption of some sorts.")); return false; } return ttCount > 0; }) .catch(function (e) { _this.log.error(constants_1.LOG_PREFIX + "Could not validate traffic type existence of ".concat(trafficType, " due to an error: ").concat(e, ".")); // If there is an error, bypass the validation so the event can get tracked. return true; }); }; // @TODO remove or implement. It is not being used. DefinitionsCacheInRedis.prototype.clear = function () { return Promise.resolve(); }; /** * Fetches multiple definitions. * Returned promise is rejected if redis operation fails. */ DefinitionsCacheInRedis.prototype.getMany = function (names) { var _a; var _this = this; if (this.redisError) { this.log.error(constants_1.LOG_PREFIX + this.redisError); return Promise.reject(this.redisError); } var keys = names.map(function (name) { return _this.keys.buildDefinitionKey(name); }); return (_a = this.redis).mget.apply(_a, keys).then(function (stringifiedDefinitions) { var definitions = {}; names.forEach(function (name, idx) { var definition = stringifiedDefinitions[idx]; definitions[name] = definition && JSON.parse(definition); }); return Promise.resolve(definitions); }) .catch(function (e) { _this.log.error(constants_1.LOG_PREFIX + "Could not grab feature flags due to an error: ".concat(e, ".")); return Promise.reject(e); }); }; return DefinitionsCacheInRedis; }(AbstractDefinitionsCacheAsync_1.AbstractDefinitionsCacheAsync)); exports.DefinitionsCacheInRedis = DefinitionsCacheInRedis;