UNPKG

@splitsoftware/splitio-commons

Version:
241 lines (240 loc) 11.7 kB
import { __extends, __spreadArray } from "tslib"; import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import { LOG_PREFIX } from './constants'; import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync'; import { returnDifference } from '../../utils/lang/sets'; /** * Discard errors for an answer of multiple operations. */ function processPipelineAnswer(results) { return results.reduce(function (accum, errValuePair) { if (errValuePair[0] === null) accum.push(errValuePair[1]); return accum; }, []); } /** * ISplitsCacheAsync implementation that stores split definitions in Redis. * Supported by Node.js */ var SplitsCacheInRedis = /** @class */ (function (_super) { __extends(SplitsCacheInRedis, _super); function SplitsCacheInRedis(log, keys, redis, splitFiltersValidation) { var _this = _super.call(this) || this; _this.log = log; _this.redis = redis; _this.keys = keys; _this.flagSetsFilter = 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; } SplitsCacheInRedis.prototype._decrementCounts = function (split) { var _this = this; var ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); return this.redis.decr(ttKey).then(function (count) { if (count === 0) return _this.redis.del(ttKey); }); }; SplitsCacheInRedis.prototype._incrementCounts = function (split) { var ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); return this.redis.incr(ttKey); }; SplitsCacheInRedis.prototype._updateFlagSets = function (featureFlagName, flagSetsOfRemovedFlag, flagSetsOfAddedFlag) { var _this = this; var removeFromFlagSets = returnDifference(flagSetsOfRemovedFlag, flagSetsOfAddedFlag); var addToFlagSets = returnDifference(flagSetsOfAddedFlag, flagSetsOfRemovedFlag); if (this.flagSetsFilter.length > 0) { addToFlagSets = addToFlagSets.filter(function (flagSet) { return _this.flagSetsFilter.some(function (filterFlagSet) { return filterFlagSet === flagSet; }); }); } var items = [featureFlagName]; return Promise.all(__spreadArray(__spreadArray([], removeFromFlagSets.map(function (flagSetName) { return _this.redis.srem(_this.keys.buildFlagSetKey(flagSetName), items); }), true), addToFlagSets.map(function (flagSetName) { return _this.redis.sadd(_this.keys.buildFlagSetKey(flagSetName), items); }), true)); }; /** * Add a given split. * The returned promise is resolved when the operation success * or rejected if it fails (e.g., redis operation fails) */ SplitsCacheInRedis.prototype.addSplit = function (split) { var _this = this; var name = split.name; var splitKey = this.keys.buildSplitKey(name); return this.redis.get(splitKey).then(function (splitFromStorage) { // handling parsing error var parsedPreviousSplit, stringifiedNewSplit; try { parsedPreviousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : undefined; stringifiedNewSplit = JSON.stringify(split); } catch (e) { throw new Error('Error parsing feature flag definition: ' + e); } return _this.redis.set(splitKey, stringifiedNewSplit).then(function () { // avoid unnecessary increment/decrement operations if (parsedPreviousSplit && parsedPreviousSplit.trafficTypeName === split.trafficTypeName) return; // update traffic type counts return _this._incrementCounts(split).then(function () { if (parsedPreviousSplit) return _this._decrementCounts(parsedPreviousSplit); }); }).then(function () { return _this._updateFlagSets(name, parsedPreviousSplit && parsedPreviousSplit.sets, split.sets); }); }).then(function () { return true; }); }; /** * Remove a given split. * The returned promise is resolved when the operation success, with true or false indicating if the split existed (and was removed) or not. * or rejected if it fails (e.g., redis operation fails). */ SplitsCacheInRedis.prototype.removeSplit = function (name) { var _this = this; return this.getSplit(name).then(function (split) { if (split) { return _this._decrementCounts(split).then(function () { return _this._updateFlagSets(name, split.sets); }); } }).then(function () { return _this.redis.del(_this.keys.buildSplitKey(name)).then(function (status) { return status === 1; }); }); }; /** * Get split definition or null if it's not defined. * Returned promise is rejected if redis operation fails. */ SplitsCacheInRedis.prototype.getSplit = function (name) { if (this.redisError) { this.log.error(LOG_PREFIX + this.redisError); return Promise.reject(this.redisError); } return this.redis.get(this.keys.buildSplitKey(name)) .then(function (maybeSplit) { return maybeSplit && JSON.parse(maybeSplit); }); }; /** * Set till number. * The returned promise is resolved when the operation success, * or rejected if it fails. */ SplitsCacheInRedis.prototype.setChangeNumber = function (changeNumber) { return this.redis.set(this.keys.buildSplitsTillKey(), 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. */ SplitsCacheInRedis.prototype.getChangeNumber = function () { var _this = this; return this.redis.get(this.keys.buildSplitsTillKey()).then(function (value) { var i = parseInt(value, 10); return isNaNNumber(i) ? -1 : i; }).catch(function (e) { _this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); return -1; }); }; /** * Get list of all split definitions. * The returned promise is resolved with the list of split 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. SplitsCacheInRedis.prototype.getAll = function () { var _this = this; return this.redis.keys(this.keys.searchPatternForSplitKeys()) .then(function (listOfKeys) { return _this.redis.pipeline(listOfKeys.map(function (k) { return ['get', k]; })).exec(); }) .then(processPipelineAnswer) .then(function (splitDefinitions) { return splitDefinitions.map(function (splitDefinition) { return JSON.parse(splitDefinition); }); }); }; /** * Get list of split names. * The returned promise is resolved with the list of split names, * or rejected if redis operation fails. */ SplitsCacheInRedis.prototype.getSplitNames = function () { var _this = this; return this.redis.keys(this.keys.searchPatternForSplitKeys()).then(function (listOfKeys) { return listOfKeys.map(_this.keys.extractKey); }); }; /** * Get list of feature flag names related to a given list of flag set names. * The returned promise is resolved with the list of feature flag names per flag set, * or rejected if the pipelined redis operation fails (e.g., timeout). */ SplitsCacheInRedis.prototype.getNamesByFlagSets = function (flagSets) { var _this = this; return this.redis.pipeline(flagSets.map(function (flagSet) { return ['smembers', _this.keys.buildFlagSetKey(flagSet)]; })).exec() .then(function (results) { return results.map(function (_a, index) { var e = _a[0], value = _a[1]; if (e === null) return value; _this.log.error(LOG_PREFIX + ("Could not read result from get members of flag set " + flagSets[index] + " due to an error: " + e)); }); }) .then(function (namesByFlagSets) { return namesByFlagSets.map(function (namesByFlagSet) { return new Set(namesByFlagSet); }); }); }; /** * 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. */ SplitsCacheInRedis.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 (!isFiniteNumber(ttCount) || ttCount < 0) { _this.log.info(LOG_PREFIX + ("Could not validate traffic type existence of " + trafficType + " due to data corruption of some sorts.")); return false; } return ttCount > 0; }) .catch(function (e) { _this.log.error(LOG_PREFIX + ("Could not validate traffic type existence of " + trafficType + " due to an error: " + 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. SplitsCacheInRedis.prototype.clear = function () { return Promise.resolve(); }; /** * Fetches multiple splits definitions. * Returned promise is rejected if redis operation fails. */ SplitsCacheInRedis.prototype.getSplits = function (names) { var _a; var _this = this; if (this.redisError) { this.log.error(LOG_PREFIX + this.redisError); return Promise.reject(this.redisError); } var keys = names.map(function (name) { return _this.keys.buildSplitKey(name); }); return (_a = this.redis).mget.apply(_a, keys).then(function (splitDefinitions) { var splits = {}; names.forEach(function (name, idx) { var split = splitDefinitions[idx]; splits[name] = split && JSON.parse(split); }); return Promise.resolve(splits); }) .catch(function (e) { _this.log.error(LOG_PREFIX + ("Could not grab feature flags due to an error: " + e + ".")); return Promise.reject(e); }); }; return SplitsCacheInRedis; }(AbstractSplitsCacheAsync)); export { SplitsCacheInRedis };