Skip to content

Commit 160e56d

Browse files
committed
Add new functionality
- Add a new `type` option which allows users of Redis 6.0 and newer to scan for keys of a specific type. - Add a new `limit` option to allow canceling a scan when we've found the requested number of keys or more. - Add the ability to cancel the scan via the eachScanCallback function parameter of the eachScan() method. - Update mocha tests and readme.
1 parent 1f3fae2 commit 160e56d

File tree

4 files changed

+207
-43
lines changed

4 files changed

+207
-43
lines changed

README.md

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ yarn add node-redis-scan
1919

2020
Instantiate this class with a [Node Redis client](https://github.com/NodeRedis/node_redis) and then perform key space scans! Redis also supports scanning through hashes, sets, and sorted sets with the `HSCAN`, `SSCAN`, and `ZSCAN` commands, respectively. This functionality is available by calling the appropriately named functions listed below.
2121

22+
### Table of contents
23+
24+
- [scan()](#the-scan-method)
25+
- [eachScan()](#the-eachscan-method)
26+
- [hscan(), eachHScan()](#the-hscan-and-eachhscan-methods)
27+
- [sscan(), eachSScan()](#the-sscan-and-eachsscan-methods)
28+
- [zscan(), eachZScan()](#the-zscan-and-eachzscan-methods)
29+
- [Canceling a scan before it has finished](#canceling-a-scan-before-it-has-finished)
30+
2231
### The `scan()` method
2332

2433
The `scan()` method provides the easiest way to scan your key space with a single callback that will be passed all matching keys. Depending on the size of your key space (millions of keys and beyond) this process might take many seconds or longer.
@@ -28,7 +37,7 @@ The `scan()` method provides the easiest way to scan your key space with a singl
2837
|Name|Type|Description|
2938
|-|-|-|
3039
|`pattern`|string|The Redis glob-style string pattern to match keys against.|
31-
|`options`|object _(optional)_|An object for configuring the precise scan parameters. Available options:<br><ul><li>`method` - String name for which underlying Redis scan method we want to use. Defaults to 'scan' and can be set to one of 'hscan', 'sscan', or 'zscan'.</li><li>`key` - The string name of the applicable key. Required if the `method` is set to 'hscan', 'sscan', or 'zscan'.</li><li>`count` - A number representing how much work Redis should do with each iteration of the given scan command. This is useful if you want to scan a huge key space faster. The trade off is lengthening the brief segments of time that Redis is locked doing work scanning. See the [Redis COUNT option documentation](https://redis.io/commands/scan#the-count-option).</li></ul>|
40+
|`options`|object _(optional)_|An object for configuring the precise scan parameters. Available options:<br><ul><li>`method` - String name for which underlying Redis scan method we want to use. Defaults to 'scan' and can be set to one of 'hscan', 'sscan', or 'zscan'.</li><li>`key` - The string name of the applicable key. Required if the `method` is set to 'hscan', 'sscan', or 'zscan'.</li><li>`count` - A number representing how much work Redis should do with each iteration of the given scan command. This is useful if you want to scan a huge key space faster. The trade off is lengthening the brief segments of time that Redis is locked doing work scanning. See the [Redis COUNT option documentation](https://redis.io/commands/scan#the-count-option).</li><li>`type` - A string name of a Redis key type. This is used for searching for keys of a certain type. See the [Redis TYPE option documentation](https://redis.io/commands/scan#the-type-option).</li><li>`limit` - A number representing a limit for how many results should be returned. Because of the nature of the Redis SCAN command plus the interaction with the `count` option we can never guarantee returning this exact limit. When the limit is reached _or exceeded_ the scan halts and is considered complete.</li></ul>|
3241
|`callback`|function|Invoked with (err, matchingKeys).|
3342

3443
**Example**
@@ -67,8 +76,8 @@ Matching keys are passed to the intermediate callback function after each iterat
6776
|Name|Type|Description|
6877
|-|-|-|
6978
|`pattern`|string|The Redis glob-style string pattern to match keys against.|
70-
|`options`|object _(optional)_|An object for configuring the precise scan parameters. Available options:<br><ul><li>`method` - String name for which underlying Redis scan method we want to use. Defaults to 'scan' and can be set to one of 'hscan', 'sscan', or 'zscan'.</li><li>`key` - The string name of the applicable key. Required if the `method` is set to 'hscan', 'sscan', or 'zscan'.</li><li>`count` - A number representing how much work Redis should do with each iteration of the given scan command. This is useful if you want to scan a huge key space faster. The trade off is lengthening the brief segments of time that Redis is locked doing work scanning. See the [Redis COUNT option documentation](https://redis.io/commands/scan#the-count-option).</li></ul>|
71-
|`eachScanCallback`|function|Invoked with (matchingKeys).|
79+
|`options`|object _(optional)_|An object for configuring the precise scan parameters. Available options:<br><ul><li>`method` - String name for which underlying Redis scan method we want to use. Defaults to 'scan' and can be set to one of 'hscan', 'sscan', or 'zscan'.</li><li>`key` - The string name of the applicable key. Required if the `method` is set to 'hscan', 'sscan', or 'zscan'.</li><li>`count` - A number representing how much work Redis should do with each iteration of the given scan command. This is useful if you want to scan a huge key space faster. The trade off is lengthening the brief segments of time that Redis is locked doing work scanning. See the [Redis COUNT option documentation](https://redis.io/commands/scan#the-count-option).</li><li>`type` - A string name of a Redis key type. This is used for searching for keys of a certain type. See the [Redis TYPE option documentation](https://redis.io/commands/scan#the-type-option).</li><li>`limit` - A number representing a limit for how many results should be returned. Because of the nature of the Redis SCAN command plus the interaction with the `count` option we can never guarantee returning this exact limit. When the limit is reached _or exceeded_ the scan halts and is considered complete.</li></ul>|
80+
|`eachScanCallback`|function|This intermediate callback is used for handling matching keys as they are returned. This function can also signal cancellation of the overall scan, if desired, by returning boolean `true`.<br><br>Invoked with (matchingKeys).|
7281
|`callback`|function|Invoked with (err, matchCount).|
7382

7483
**Example**
@@ -123,7 +132,7 @@ scanner.eachHScan('name-of-hash', 'some-pattern*', (matchingKeysValues) => {
123132
if (matchingKeysValues.length) {
124133
// Matching keys and values of the hash found after this
125134
// iteration of the HSCAN command.
126-
console.log(matchingKeys);
135+
console.log(matchingKeysValues);
127136
}
128137
}, (err, matchCount) => {
129138
if (err) throw(err);
@@ -176,6 +185,39 @@ scanner.zscan('name-of-sorted-set', 'some-pattern*', (err, matchingMembersScores
176185
// `eachZScan()` approach, which is similar to `eachScan()`
177186
```
178187

188+
### Canceling a scan before it has finished
189+
190+
Using the `limit` option with either the `scan()` or `eachScan()` method allows us to halt or cancel a scan after a certain "limit" of matching keys have been found. Note that we might receive _more_ matching keys than the specified `limit` because of the nature of the Redis SCAN command.
191+
192+
**Example**
193+
194+
```js
195+
scanner.scan('some-pattern*', {limit: 5}, (err, matchingKeys) => {
196+
if (err) throw(err);
197+
198+
// We'll have 5 or more matching keys here and the scan
199+
// will have been canceled before it completed.
200+
console.log(matchingKeys);
201+
});
202+
```
203+
204+
Alternatively, we can use the `eachScanCallback` function parameter of the `eachScan()` method for more fine-grained cancellation of a scan.
205+
206+
**Example**
207+
208+
```js
209+
scanner.eachScan(scanPattern, (matchingKeys) => {
210+
// Do something arbitrary and then return `true` to cancel the scan.
211+
if (Math.random() > 0.5) {
212+
return true;
213+
}
214+
}, (err, matchCount) => {
215+
if (err) throw(err);
216+
217+
console.log(`Found ${matchCount} keys after canceling the scan arbitrarily.`);
218+
});
219+
```
220+
179221
# Test
180222

181223
Tests are run via [Istanbul](https://github.com/istanbuljs/nyc) and [Mocha](https://github.com/mochajs/mocha). Clone the project then run:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-redis-scan",
3-
"version": "1.2.2",
3+
"version": "1.3.0",
44
"description": "A simple ES6 Redis key scanner for Node",
55
"keywords": [
66
"redis",

redis-scan.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,26 @@ class RedisScan {
4040
* with each iteration of the given SCAN command. Increase this to hundreds
4141
* or thousands to speed up scans of huge keyspaces.
4242
* See: https://redis.io/commands/scan#the-count-option
43+
* * `type` - A string name of a Redis key type. Used for searching
44+
* the entire Redis key space for keys of a certain type.
45+
* See: https://redis.io/commands/scan#the-type-option
46+
* * `limit` - A number representing a limit for how many results
47+
* should be returned. Because of the nature of the Redis SCAN
48+
* command and possible interaction with the `count` option we can
49+
* never guarantee returning this exact limit. As soon as we reach
50+
* or exceed the limit then we halt the scan and call the final
51+
* callback appropriately.
4352
*
4453
* @param {Function} eachScanCallback - A function called after each
4554
* call to the Redis `SCAN` command. Invoked with (matchingKeys).
55+
* If this function returns boolean `true` then this signals cancellation
56+
* of the scan. No further `SCAN` commands will be run and the final
57+
* callback will be called with the current count of matching keys.
58+
* This allows us to halt a scan before it has finished.
4659
*
4760
* @param {Function} [callback] - A function called after the full scan has
4861
* completed and all keys have been returned.
62+
* Invoked with (err, matchCount).
4963
*/
5064
eachScan(pattern, options, eachScanCallback, callback) {
5165
if (!callback) {
@@ -57,6 +71,8 @@ class RedisScan {
5771
const method = options.method || 'scan';
5872
const key = options.key || '';
5973
const count = options.count || 0;
74+
const type = options.type || '';
75+
const limit = options.limit || Infinity;
6076

6177
let matchesCount = 0;
6278

@@ -75,6 +91,11 @@ class RedisScan {
7591
parameters.push('COUNT', count);
7692
}
7793

94+
// Add any custom `TYPE` scan option.
95+
if (type) {
96+
parameters.push('TYPE', type);
97+
}
98+
7899
this.redisClient[method](...parameters, (err, data) => {
79100
if (err) {
80101
callback(err);
@@ -86,10 +107,13 @@ class RedisScan {
86107
const [cursor, matches] = data;
87108

88109
matchesCount += matches.length;
89-
eachScanCallback(matches);
110+
const cancel = eachScanCallback(matches);
90111

91-
// We're done once Redis returns 0 for the next cursor.
92-
if (cursor === '0') {
112+
// We're done when any of the following happen:
113+
// - Redis returns 0 for the next cursor
114+
// - We have the number of results desired via limit
115+
// - The return value of eachScanCallback is `true`
116+
if (cursor === '0' || matchesCount >= limit || cancel === true) {
93117
callback(null, matchesCount);
94118
} else {
95119
// Otherwise, call this function again AKA recurse
@@ -122,7 +146,7 @@ class RedisScan {
122146
*
123147
* @param {Function} [callback] - A function called after the full scan
124148
* of the Redis keyspace completes having searched for the given pattern.
125-
* Invoked with (err, keys).
149+
* Invoked with (err, matchingKeys).
126150
*/
127151
scan(pattern, options, callback) {
128152
if (!callback) {
@@ -159,7 +183,7 @@ class RedisScan {
159183
* @param {Function} [callback]
160184
*/
161185
hscan(key, pattern, callback) {
162-
this.scan(pattern, {method: 'hscan', key: key}, callback);
186+
this.scan(pattern, {method: 'hscan', key}, callback);
163187
}
164188

165189
/**
@@ -177,7 +201,7 @@ class RedisScan {
177201
* @param {Function} [callback]
178202
*/
179203
eachHScan(key, pattern, eachScanCallback, callback) {
180-
this.eachScan(pattern, {method: 'hscan', key: key}, eachScanCallback, callback);
204+
this.eachScan(pattern, {method: 'hscan', key}, eachScanCallback, callback);
181205
}
182206

183207
/**
@@ -193,7 +217,7 @@ class RedisScan {
193217
* @param {Function} [callback]
194218
*/
195219
sscan(key, pattern, callback) {
196-
this.scan(pattern, {method: 'sscan', key: key}, callback);
220+
this.scan(pattern, {method: 'sscan', key}, callback);
197221
}
198222

199223
/**
@@ -211,7 +235,7 @@ class RedisScan {
211235
* @param {Function} [callback]
212236
*/
213237
eachSScan(key, pattern, eachScanCallback, callback) {
214-
this.eachScan(pattern, {method: 'sscan', key: key}, eachScanCallback, callback);
238+
this.eachScan(pattern, {method: 'sscan', key}, eachScanCallback, callback);
215239
}
216240

217241
/**
@@ -228,7 +252,7 @@ class RedisScan {
228252
* @param {Function} [callback]
229253
*/
230254
zscan(key, pattern, callback) {
231-
this.scan(pattern, {method: 'zscan', key: key}, callback);
255+
this.scan(pattern, {method: 'zscan', key}, callback);
232256
}
233257

234258
/**
@@ -246,7 +270,7 @@ class RedisScan {
246270
* @param {Function} [callback]
247271
*/
248272
eachZScan(key, pattern, eachScanCallback, callback) {
249-
this.eachScan(pattern, {method: 'zscan', key: key}, eachScanCallback, callback);
273+
this.eachScan(pattern, {method: 'zscan', key}, eachScanCallback, callback);
250274
}
251275
}
252276

0 commit comments

Comments
 (0)