Skip to content

[Pluggable storage] validation #598

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: pluggable_storage_polishing
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/storage/__tests__/ImpressionsCache/InRedis/node.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import sinon from 'sinon';
import KeyBuilder from '../../../Keys';
import ImpressionsCacheInRedis from '../../../ImpressionsCache/InRedis';
import SettingsFactory from '../../../../utils/settings';
import { CONSUMER_MODE } from '../../../../utils/constants';

tape('IMPRESSIONS CACHE IN REDIS / should incrementally store values', async function(assert) {
const settings = SettingsFactory({
mode: CONSUMER_MODE,
storage: {
type: 'REDIS',
prefix: 'ut_impr_cache'
Expand Down Expand Up @@ -87,6 +89,7 @@ tape('IMPRESSIONS CACHE IN REDIS / should incrementally store values', async fun
tape('IMPRESSIONS CACHE IN REDIS / should not resolve track before calling expire', async function(assert) {
const impressionsKey = 'ut_impr_cache_2.SPLITIO.impressions';
const settings = SettingsFactory({
mode: CONSUMER_MODE,
storage: {
type: 'REDIS',
prefix: 'ut_impr_cache_2'
Expand Down
41 changes: 38 additions & 3 deletions src/utils/__tests__/settings/node.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tape('SETTINGS / Redis options should be properly parsed', assert => {
core: {
authorizationKey: 'dummy token'
},
mode: CONSUMER_MODE,
storage: {
type: 'REDIS',
options: {
Expand All @@ -44,6 +45,7 @@ tape('SETTINGS / Redis options should be properly parsed', assert => {
core: {
authorizationKey: 'dummy token'
},
mode: CONSUMER_MODE,
storage: {
type: 'REDIS',
options: {
Expand Down Expand Up @@ -84,13 +86,15 @@ tape('SETTINGS / IPAddressesEnabled should be overwritable and true by default',
authorizationKey: 'dummy token',
IPAddressesEnabled: false
},
mode: CONSUMER_MODE
mode: CONSUMER_MODE,
storage: { type: 'REDIS' }
});
const settingsWithIPAddressEnabledAndConsumerMode = SettingsFactory({
core: {
authorizationKey: 'dummy token'
},
mode: CONSUMER_MODE
mode: CONSUMER_MODE,
storage: { type: 'PLUGGABLE' }
});

assert.equal(settingsWithIPAddressDisabled.core.IPAddressesEnabled, false, 'When creating a setting instance, it will have the provided value for IPAddressesEnabled');
Expand Down Expand Up @@ -122,4 +126,35 @@ tape('SETTINGS / streamingEnabled should be overwritable and true by default', a
assert.equal(settingsWithStreamingEnabled.streamingEnabled, true, 'If streamingEnabled is not provided, it will be true.');

assert.end();
});
});

tape('SETTINGS / Throws exception if no "REDIS" or "PLUGGABLE" storage is provided in consumer mode', (assert) => {
assert.throws(() => {
SettingsFactory({
core: {
authorizationKey: 'dummy token'
},
mode: CONSUMER_MODE,
storage: { type: 'invalid type' }
});
}, /A Pluggable or Redis storage is required on consumer mode/);

assert.end();
});

tape('SETTINGS / Fallback to InMemory storage if no valid storage is provided in standlone and localhost modes', (assert) => {
// 'REDIS' and 'PLUGGABLE' are not valid storages for standalone and localhost modes
const settings = [
SettingsFactory({
core: { authorizationKey: 'localhost' }, // localhost mode
storage: { type: 'REDIS' }
}), SettingsFactory({
core: { authorizationKey: 'dummy token' }, // standalone mode
storage: { type: 'PLUGGABLE' }
})
];

settings.forEach(setting => { assert.equal(setting.storage.type, 'MEMORY'); });

assert.end();
});
2 changes: 1 addition & 1 deletion src/utils/settings/storage/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const ParseStorageSettings = settings => {
if (type !== STORAGE_MEMORY && type !== STORAGE_LOCALSTORAGE ||
type === STORAGE_LOCALSTORAGE && !isLocalStorageAvailable()) {
fallbackToMemory();
log.warn('Invalid or unavailable storage. Fallbacking into MEMORY storage');
log.error('Invalid or unavailable storage. Fallbacking into MEMORY storage');
}

return {
Expand Down
29 changes: 21 additions & 8 deletions src/utils/settings/storage/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
**/
import { LOCALHOST_MODE, STORAGE_CUSTOM, STORAGE_MEMORY, STORAGE_REDIS } from '../../constants';
import { LOCALHOST_MODE, STORAGE_PLUGGABLE, STORAGE_MEMORY, STORAGE_REDIS, CONSUMER_MODE, STANDALONE_MODE } from '../../constants';
import logFactory from '../../../utils/logger';
const log = logFactory('splitio-settings');

export function parseRedisOptions(options) {
let {
Expand Down Expand Up @@ -75,24 +77,35 @@ const ParseStorageSettings = (settings) => {
prefix
};

// In other cases we can have MEMORY or REDIS
const fallbackToMemory = () => {
log.error('The provided storage is invalid. It requires consumer mode. Fallbacking into default MEMORY storage.');
return {
type: STORAGE_MEMORY,
prefix
};
};

// In other cases we can have MEMORY, REDIS or PLUGGABLE
switch (type) {
case STORAGE_REDIS: {
return {
case STORAGE_REDIS:
if (mode === STANDALONE_MODE) return fallbackToMemory();
else return {
type,
prefix,
options: parseRedisOptions(options)
};
}

// For custom storage, we return the original storage options, without updating
// For pluggable storage, we return the original storage options, without updating
// the prefix because it is done internally by the PluggableStorage module.
case STORAGE_CUSTOM:
return settings.storage;
case STORAGE_PLUGGABLE:
if (mode === STANDALONE_MODE) return fallbackToMemory();
else return settings.storage;

// For now, we don't have modifiers or settings for MEMORY in NodeJS
case STORAGE_MEMORY:
default: {
// Consumer mode requires an async storage
if (mode === CONSUMER_MODE) throw new Error('A Pluggable or Redis storage is required on consumer mode');
return {
type: STORAGE_MEMORY,
prefix
Expand Down
32 changes: 17 additions & 15 deletions ts-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ asyncSettings = {
core: {
authorizationKey: 'key'
},
mode: 'consumer',
storage: {
type: 'REDIS',
options: {}
Expand Down Expand Up @@ -183,6 +184,7 @@ asyncSettings = {
core: {
authorizationKey: 'key'
},
mode: 'consumer',
storage: {
type: 'PLUGGABLE',
wrapper: new MyWrapper()
Expand Down Expand Up @@ -310,23 +312,23 @@ tracked = client.track('myEventType', undefined, { prop1: 1, prop2: '2', prop3:
/*** Repeating tests for Async Client ***/

// Events constants we get (same as for sync client, just for interface checking)
const eventConstsAsymc: { [key: string]: SplitIO.Event } = client.Event;
splitEvent = client.Event.SDK_READY;
splitEvent = client.Event.SDK_READY_FROM_CACHE;
splitEvent = client.Event.SDK_READY_TIMED_OUT;
splitEvent = client.Event.SDK_UPDATE;
const eventConstsAsync: { [key: string]: SplitIO.Event } = asyncClient.Event;
splitEvent = asyncClient.Event.SDK_READY;
splitEvent = asyncClient.Event.SDK_READY_FROM_CACHE;
splitEvent = asyncClient.Event.SDK_READY_TIMED_OUT;
splitEvent = asyncClient.Event.SDK_UPDATE;

// Client implements methods from NodeJS.Events. (same as for sync client, just for interface checking)
client = client.on(splitEvent, () => { });
const a1: boolean = client.emit(splitEvent);
client = client.removeAllListeners(splitEvent);
client = client.removeAllListeners();
const b1: number = client.listenerCount(splitEvent);
nodeEventEmitter = client;
asyncClient = asyncClient.on(splitEvent, () => { });
const a1: boolean = asyncClient.emit(splitEvent);
asyncClient = asyncClient.removeAllListeners(splitEvent);
asyncClient = asyncClient.removeAllListeners();
const b1: number = asyncClient.listenerCount(splitEvent);
nodeEventEmitter = asyncClient;

// Ready and destroy (same as for sync client, just for interface checking)
const readyPromise1: Promise<void> = client.ready();
client.destroy();
const readyPromise1: Promise<void> = asyncClient.ready();
asyncClient.destroy();

// We can call getTreatment but always with a key.
asyncTreatment = asyncClient.getTreatment(splitKey, 'mySplit');
Expand Down Expand Up @@ -541,7 +543,6 @@ let fullNodeSettings: SplitIO.INodeSettings = {
}
};
fullNodeSettings.storage.type = 'MEMORY';
fullNodeSettings.mode = 'consumer';

let fullAsyncSettings: SplitIO.INodeAsyncSettings = {
core: {
Expand Down Expand Up @@ -572,12 +573,13 @@ let fullAsyncSettings: SplitIO.INodeAsyncSettings = {
prefix: 'PREFIX'
},
impressionListener: impressionListener,
mode: 'standalone',
mode: 'consumer',
debug: true,
sync: {
splitFilters: splitFilters
}
};
fullAsyncSettings.storage.type = 'PLUGGABLE';

// debug property can be a log level
fullBrowserSettings.debug = 'ERROR';
Expand Down
25 changes: 21 additions & 4 deletions types/splitio.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ interface INodeBasicSettings extends ISharedSettings {
prefix?: string
},
/**
* The SDK mode. Possible values are "standalone" (which is the default) and "consumer". For "localhost" mode, use "localhost" as authorizationKey.
* The SDK mode. Possible values are "standalone", which is the default when using a synchronous storage, like 'MEMORY' and 'LOCALSTORAGE',
* and "consumer", which must be set when using an asynchronous storage, like 'REDIS' and 'PLUGGABLE'. For "localhost" mode, use "localhost" as authorizationKey.
* @property {SDKMode} mode
* @default standalone
*/
Expand Down Expand Up @@ -1020,7 +1021,15 @@ declare namespace SplitIO {
* @default SPLITIO
*/
prefix?: string
}
},
/**
* The SDK mode. When using the default 'MEMORY' storage or `LOCALSTORAGE` as storage type, the only possible value is "standalone", which is the default.
* For "localhost" mode, use "localhost" as authorizationKey.
*
* @property {'standalone'} mode
* @default standalone
*/
mode?: 'standalone'
}
/**
* Settings interface with async storage for SDK instances created on NodeJS.
Expand All @@ -1047,12 +1056,20 @@ declare namespace SplitIO {
*/
wrapper?: Object,
/**
* Optional prefix to prevent any kind of data collision between SDK versions.
* Optional prefix added to the storage keys to prevent any kind of data collision between SDK versions.
* @property {string} prefix
* @default SPLITIO
*/
prefix?: string
}
},
/**
* The SDK mode. When using 'REDIS' or 'PLUGGABLE' storage type, the only possible value is "consumer", which is required.
*
* @see {@link @TODO}
*
* @property {'consumer'} mode
*/
mode: 'consumer'
}
/**
* This represents the interface for the SDK instance with synchronous storage.
Expand Down