Skip to content
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

feat: add noNamespaceAffectsAll option #1243

Merged
Merged
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
7 changes: 4 additions & 3 deletions packages/redis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ NOTE: If you plan to do many clears or deletes, it is recommended to read the [P
# Performance Considerations

With namespaces being prefix based it is critical to understand some of the performance considerations we have made:
* `clear()` - We use the `SCAN` command to iterate over keys. This is a non-blocking command that is more efficient than `KEYS`. In addition we are using `UNLINK` by default instead of `DEL`. Even with that if you are iterating over a large dataset it can still be slow. It is highly recommended to use the `namespace` option to limit the keys that are being cleared and if possible to not use the `clear()` method in high performance environments.
* `clear()` - We use the `SCAN` command to iterate over keys. This is a non-blocking command that is more efficient than `KEYS`. In addition we are using `UNLINK` by default instead of `DEL`. Even with that if you are iterating over a large dataset it can still be slow. It is highly recommended to use the `namespace` option to limit the keys that are being cleared and if possible to not use the `clear()` method in high performance environments. If you don't set namespaces, you can enable `noNamespaceAffectsAll` to clear all keys using the `FLUSHDB` command which is faster and can be used in production environments.

* `delete()` - By default we are now using `UNLINK` instead of `DEL` for deleting keys. This is a non-blocking command that is more efficient than `DEL`. If you are deleting a large number of keys it is recommended to use the `deleteMany()` method instead of `delete()`.

Expand Down Expand Up @@ -217,6 +217,7 @@ const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });
* **keyPrefixSeparator** - The separator to use between the namespace and key.
* **clearBatchSize** - The number of keys to delete in a single batch.
* **useUnlink** - Use the `UNLINK` command for deleting keys isntead of `DEL`.
* **noNamespaceAffectsAll**: Whether to allow clearing all keys when no namespace is set (default is `false`).
* **set** - Set a key.
* **setMany** - Set multiple keys.
* **get** - Get a key.
Expand All @@ -225,9 +226,9 @@ const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });
* **hasMany** - Check if multiple keys exist.
* **delete** - Delete a key.
* **deleteMany** - Delete multiple keys.
* **clear** - Clear all keys in the namespace. If the namespace is not set it will clear all keys that are not prefixed with a namespace.
* **clear** - Clear all keys in the namespace. If the namespace is not set it will clear all keys that are not prefixed with a namespace unless `noNamespaceAffectsAll` is set to `true`.
* **disconnect** - Disconnect from the Redis server.
* **iterator** - Create a new iterator for the keys. If the namespace is not set it will iterate over all keys that are not prefixed with a namespace.
* **iterator** - Create a new iterator for the keys. If the namespace is not set it will iterate over all keys that are not prefixed with a namespace unless `noNamespaceAffectsAll` is set to `true`.

# Migrating from v3 to v4

Expand Down
38 changes: 37 additions & 1 deletion packages/redis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export type KeyvRedisOptions = {
*/
useUnlink?: boolean;

/**
* Whether to allow clearing all keys when no namespace is set.
* If set to true and no namespace is set, iterate() will return all keys.
* Defaults to `false`.
*/
noNamespaceAffectsAll?: boolean;
};

export type KeyvRedisPropertyOptions = KeyvRedisOptions & {
Expand Down Expand Up @@ -63,6 +69,7 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
private _keyPrefixSeparator = '::';
private _clearBatchSize = 1000;
private _useUnlink = true;
private _noNamespaceAffectsAll = false;

/**
* KeyvRedis constructor.
Expand Down Expand Up @@ -114,6 +121,7 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
namespace: this._namespace,
keyPrefixSeparator: this._keyPrefixSeparator,
clearBatchSize: this._clearBatchSize,
noNamespaceAffectsAll: this._noNamespaceAffectsAll,
dialect: 'redis',
url,
};
Expand Down Expand Up @@ -188,6 +196,23 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
this._useUnlink = value;
}

/**
* Get if no namespace affects all keys.
* Whether to allow clearing all keys when no namespace is set.
* If set to true and no namespace is set, iterate() will return all keys.
* @default false
*/
public get noNamespaceAffectsAll(): boolean {
return this._noNamespaceAffectsAll;
}

/**
* Set if not namespace affects all keys.
*/
public set noNamespaceAffectsAll(value: boolean) {
this._noNamespaceAffectsAll = value;
}

/**
* Get the Redis URL used to connect to the server. This is used to get a connected client.
*/
Expand Down Expand Up @@ -419,7 +444,7 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
cursor = result.cursor.toString();
let {keys} = result;

if (!namespace) {
if (!namespace && !this._noNamespaceAffectsAll) {
keys = keys.filter(key => !key.includes(this._keyPrefixSeparator));
}

Expand Down Expand Up @@ -448,6 +473,12 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter

private async clearNamespace(namespace?: string): Promise<void> {
try {
if (!namespace && this._noNamespaceAffectsAll) {
const client = await this.getClient() as RedisClientType;
await client.flushDb();
return;
}

let cursor = '0';
const batchSize = this._clearBatchSize;
const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : '*';
Expand Down Expand Up @@ -479,6 +510,7 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
}
}
} while (cursor !== '0');

/* c8 ignore next 3 */
} catch (error) {
this.emit('error', error);
Expand Down Expand Up @@ -517,6 +549,10 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
if (options.useUnlink !== undefined) {
this._useUnlink = options.useUnlink;
}

if (options.noNamespaceAffectsAll !== undefined) {
this._noNamespaceAffectsAll = options.noNamespaceAffectsAll;
}
}

private initClient(): void {
Expand Down
4 changes: 2 additions & 2 deletions packages/redis/test/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('KeyvRedis Cluster', () => {
expect(keyvRedis.isCluster()).toBe(true);
});

test('shoudl be able to set the redis cluster client', async () => {
test('should be able to set the redis cluster client', async () => {
const cluster = createCluster(defaultClusterOptions);

const keyvRedis = new KeyvRedis();
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('KeyvRedis Cluster', () => {
await keyvRedis.delete('test-cl1');
});

test('should thrown an error on clear', async () => {
test('should throw an error on clear', async () => {
const cluster = createCluster(defaultClusterOptions);

const keyvRedis = new KeyvRedis(cluster);
Expand Down
95 changes: 92 additions & 3 deletions packages/redis/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
} from 'vitest';
import {createClient, type RedisClientType} from 'redis';
import {delay} from '@keyv/test-suite';
import KeyvRedis, {createKeyv, createCluster} from '../src/index.js';
import KeyvRedis, {createKeyv} from '../src/index.js';

describe('KeyvRedis', () => {
test('should be a class', () => {
Expand Down Expand Up @@ -60,12 +60,14 @@ describe('KeyvRedis', () => {
keyPrefixSeparator: '->',
clearBatchSize: 100,
useUnlink: true,
noNamespaceAffectsAll: true,
};
const keyvRedis = new KeyvRedis(uri, options);
expect(keyvRedis.namespace).toBe('test');
expect(keyvRedis.keyPrefixSeparator).toBe('->');
expect(keyvRedis.clearBatchSize).toBe(100);
expect(keyvRedis.useUnlink).toBe(true);
expect(keyvRedis.noNamespaceAffectsAll).toBe(true);
});

test('should be able to get and set properties', () => {
Expand All @@ -82,10 +84,12 @@ describe('KeyvRedis', () => {

test('should be able to get and set opts', async () => {
const keyvRedis = new KeyvRedis();
keyvRedis.opts = {namespace: 'test', keyPrefixSeparator: ':1', clearBatchSize: 2000};
keyvRedis.opts = {
namespace: 'test', keyPrefixSeparator: ':1', clearBatchSize: 2000, noNamespaceAffectsAll: true,
};

expect(keyvRedis.opts).toEqual({
namespace: 'test', keyPrefixSeparator: ':1', clearBatchSize: 2000, dialect: 'redis', url: 'redis://localhost:6379',
namespace: 'test', keyPrefixSeparator: ':1', clearBatchSize: 2000, dialect: 'redis', url: 'redis://localhost:6379', noNamespaceAffectsAll: true,
});
});
});
Expand Down Expand Up @@ -278,6 +282,36 @@ describe('KeyvRedis Namespace', () => {
await keyvRedis.disconnect();
});

test('should not clear all with no namespace if noNamespaceAffectsAll is false', async () => {
const keyvRedis = new KeyvRedis();
keyvRedis.noNamespaceAffectsAll = false;

keyvRedis.namespace = 'ns1';
await keyvRedis.set('foo91', 'bar');
keyvRedis.namespace = undefined;
await keyvRedis.set('foo912', 'bar2');
await keyvRedis.set('foo913', 'bar3');
await keyvRedis.clear();
keyvRedis.namespace = 'ns1';
const value = await keyvRedis.get('foo91');
expect(value).toBeDefined();
});

test('should clear all with no namespace if noNamespaceAffectsAll is true', async () => {
const keyvRedis = new KeyvRedis();
keyvRedis.noNamespaceAffectsAll = true;

keyvRedis.namespace = 'ns1';
await keyvRedis.set('foo91', 'bar');
keyvRedis.namespace = undefined;
await keyvRedis.set('foo912', 'bar2');
await keyvRedis.set('foo913', 'bar3');
await keyvRedis.clear();
keyvRedis.namespace = 'ns1';
const value = await keyvRedis.get('foo91');
expect(value).toBeUndefined();
});

test('should clear namespace but not other ones', async () => {
const keyvRedis = new KeyvRedis();
const client = await keyvRedis.getClient() as RedisClientType;
Expand Down Expand Up @@ -379,4 +413,59 @@ describe('KeyvRedis Iterators', () => {

await keyvRedis.disconnect();
});

test('should be able to iterate over all keys if namespace is undefined and noNamespaceAffectsAll is true', async () => {
const keyvRedis = new KeyvRedis();
keyvRedis.noNamespaceAffectsAll = true;

keyvRedis.namespace = 'ns1';
await keyvRedis.set('foo1', 'bar1');
keyvRedis.namespace = 'ns2';
await keyvRedis.set('foo2', 'bar2');
keyvRedis.namespace = undefined;
await keyvRedis.set('foo3', 'bar3');

const keys = [];
const values = [];
for await (const [key, value] of keyvRedis.iterator()) {
keys.push(key);
values.push(value);
}

expect(keys).toContain('ns1::foo1');
expect(keys).toContain('ns2::foo2');
expect(keys).toContain('foo3');
expect(values).toContain('bar1');
expect(values).toContain('bar2');
expect(values).toContain('bar3');
});

test('should only iterate over keys with no namespace if name is undefined set and noNamespaceAffectsAll is false', async () => {
const keyvRedis = new KeyvRedis();
keyvRedis.noNamespaceAffectsAll = false;

keyvRedis.namespace = 'ns1';
await keyvRedis.set('foo1', 'bar1');
keyvRedis.namespace = 'ns2';
await keyvRedis.set('foo2', 'bar2');
keyvRedis.namespace = undefined;
await keyvRedis.set('foo3', 'bar3');

const keys = [];
const values = [];
for await (const [key, value] of keyvRedis.iterator()) {
keys.push(key);
values.push(value);
}

expect(keys).toContain('foo3');
expect(values).toContain('bar3');

expect(keys).not.toContain('foo1');
expect(keys).not.toContain('ns1::foo1');
expect(keys).not.toContain('ns2::foo2');
expect(keys).not.toContain('foo2');
expect(values).not.toContain('bar1');
expect(values).not.toContain('bar2');
});
});
Loading