diff --git a/src/cluster.c b/src/cluster.c index b5802317c..a4e9080b3 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -1367,6 +1367,36 @@ unsigned int keyHashSlot(char *key, int keylen) { return crc16(key+s+1,e-s-1) & 0x3FFF; } +/* If it can be inferred that the given glob-style pattern, as implemented in + * stringmatchlen() in util.c, only can match keys belonging to a single slot, + * that slot is returned. Otherwise -1 is returned. */ +int patternHashSlot(char *pattern, int length) { + int s = -1; /* index of the first '{' */ + + for (int i = 0; i < length; i++) { + if (pattern[i] == '*' || pattern[i] == '?' || pattern[i] == '[') { + /* Wildcard or character class found. Keys can be in any slot. */ + return -1; + } else if (pattern[i] == '\\') { + /* Escaped character. Computing slot in this case is not + * implemented. We would need a temp buffer. */ + return -1; + } else if (s == -1 && pattern[i] == '{') { + /* Opening brace '{' found. */ + s = i; + } else if (s >= 0 && pattern[i] == '}' && i == s + 1) { + /* Empty tag '{}' found. The whole key is hashed. Ignore braces. */ + s = -2; + } else if (s >= 0 && pattern[i] == '}') { + /* Non-empty tag '{...}' found. Hash what's between braces. */ + return crc16(pattern + s + 1, i - s - 1) & 0x3FFF; + } + } + + /* The pattern matches a single key. Hash the whole pattern. */ + return crc16(pattern, length) & 0x3FFF; +} + /* ----------------------------------------------------------------------------- * CLUSTER node API * -------------------------------------------------------------------------- */ diff --git a/src/cluster.h b/src/cluster.h index 210052ae3..0340349b2 100644 --- a/src/cluster.h +++ b/src/cluster.h @@ -403,6 +403,7 @@ unsigned long getClusterConnectionsCount(void); int clusterSendModuleMessageToTarget(const char *target, uint64_t module_id, uint8_t type, const char *payload, uint32_t len); void clusterPropagatePublish(robj *channel, robj *message, int sharded); unsigned int keyHashSlot(char *key, int keylen); +int patternHashSlot(char *pattern, int length); void clusterUpdateMyselfFlags(void); void clusterUpdateMyselfIp(void); void slotToChannelAdd(sds channel); diff --git a/src/db.c b/src/db.c index 220c7bbee..b7df85021 100644 --- a/src/db.c +++ b/src/db.c @@ -1255,11 +1255,17 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) { .pattern = use_pattern ? pat : NULL, .sampled = 0, }; + + /* A pattern may restrict all matching keys to one cluster slot. */ + int onlyslot = -1; + if (o == NULL && use_pattern && server.cluster_enabled) { + onlyslot = patternHashSlot(pat, patlen); + } do { /* In cluster mode there is a separate dictionary for each slot. * If cursor is empty, we should try exploring next non-empty slot. */ if (o == NULL) { - cursor = dbScan(c->db, DB_MAIN, cursor, scanCallback, NULL, &data); + cursor = dbScan(c->db, DB_MAIN, cursor, onlyslot, scanCallback, NULL, &data); } else { cursor = dictScan(ht, cursor, scanCallback, &data); } @@ -1426,14 +1432,30 @@ dictEntry *dbFind(redisDb *db, void *key, dbKeyType keyType){ * it performs a dictScan over the appropriate `keyType` dictionary of `db`. * 3. If the slot is entirely scanned i.e. the cursor has reached 0, the next non empty slot is discovered. * The slot information is embedded into the cursor and returned. + * + * To restrict the scan to a single cluster slot, pass a valid slot as + * 'onlyslot', otherwise pass -1. */ -unsigned long long dbScan(redisDb *db, dbKeyType keyType, unsigned long long v, dictScanFunction *fn, int (dictScanValidFunction)(dict *d), void *privdata) { +unsigned long long dbScan(redisDb *db, dbKeyType keyType, unsigned long long v, + int onlyslot, dictScanFunction *fn, + int (dictScanValidFunction)(dict *d), void *privdata) { dict *d; unsigned long long cursor = 0; /* During main dictionary traversal in cluster mode, 48 lower bits in the cursor are used for positioning in the HT. * Following 14 bits are used for the slot number, ranging from 0 to 2^14-1. * Slot is always 0 at the start of iteration and can be incremented only in cluster mode. */ int slot = getAndClearSlotIdFromCursor(&v); + if (onlyslot >= 0) { + if (slot < onlyslot) { + /* Fast-forward to onlyslot. */ + serverAssert(onlyslot < CLUSTER_SLOTS); + slot = onlyslot; + v = 0; + } else if (slot > onlyslot) { + /* The cursor is already past onlyslot. */ + return 0; + } + } if (keyType == DB_MAIN) d = db->dict[slot]; else if (keyType == DB_EXPIRES) @@ -1449,6 +1471,8 @@ unsigned long long dbScan(redisDb *db, dbKeyType keyType, unsigned long long v, } /* scanning done for the current dictionary or if the scanning wasn't possible, move to the next slot. */ if (cursor == 0 || !is_dict_valid) { + if (onlyslot >= 0) + return 0; slot = dbGetNextNonEmptySlot(db, slot, keyType); } if (slot == -1) { diff --git a/src/expire.c b/src/expire.c index 79f10f2d6..3a51f8c0d 100644 --- a/src/expire.c +++ b/src/expire.c @@ -275,7 +275,7 @@ void activeExpireCycle(int type) { long checked_buckets = 0; while (data.sampled < num && checked_buckets < max_buckets) { - db->expires_cursor = dbScan(db, DB_EXPIRES, db->expires_cursor, expireScanCallback, isExpiryDictValidForSamplingCb, &data); + db->expires_cursor = dbScan(db, DB_EXPIRES, db->expires_cursor, -1, expireScanCallback, isExpiryDictValidForSamplingCb, &data); if (db->expires_cursor == 0) { break; } diff --git a/src/module.c b/src/module.c index c49e03bf0..0428ac59c 100644 --- a/src/module.c +++ b/src/module.c @@ -10981,7 +10981,7 @@ int RM_Scan(RedisModuleCtx *ctx, RedisModuleScanCursor *cursor, RedisModuleScanC } int ret = 1; ScanCBData data = { ctx, privdata, fn }; - cursor->cursor = dbScan(ctx->client->db, DB_MAIN, cursor->cursor, moduleScanCallback, NULL, &data); + cursor->cursor = dbScan(ctx->client->db, DB_MAIN, cursor->cursor, -1, moduleScanCallback, NULL, &data); if (cursor->cursor == 0) { cursor->done = 1; ret = 0; diff --git a/src/server.h b/src/server.h index 4a3eedc4c..c373e4dd4 100644 --- a/src/server.h +++ b/src/server.h @@ -3120,7 +3120,9 @@ int calculateKeySlot(sds key); unsigned long dbBuckets(redisDb *db, dbKeyType keyType); size_t dbMemUsage(redisDb *db, dbKeyType keyType); dictEntry *dbFind(redisDb *db, void *key, dbKeyType keyType); -unsigned long long dbScan(redisDb *db, dbKeyType keyType, unsigned long long cursor, dictScanFunction *fn, int (dictScanValidFunction)(dict *d), void *privdata); +unsigned long long dbScan(redisDb *db, dbKeyType keyType, unsigned long long cursor, + int onlyslot, dictScanFunction *fn, + int (dictScanValidFunction)(dict *d), void *privdata); int dbExpand(const redisDb *db, uint64_t db_size, dbKeyType keyType, int try_expand); unsigned long long cumulativeKeyCountRead(redisDb *db, int idx, dbKeyType keyType); int getFairRandomSlot(redisDb *db, dbKeyType keyType); diff --git a/tests/unit/scan.tcl b/tests/unit/scan.tcl index 1ec38ddbc..5e093d90d 100644 --- a/tests/unit/scan.tcl +++ b/tests/unit/scan.tcl @@ -430,6 +430,30 @@ proc test_scan {type} { } } } + + test "{$type} SCAN MATCH pattern implies cluster slot" { + # Tests the code path for an optimization for patterns like "{foo}-*" + # which implies that all matching keys belong to one slot. + r flushdb + for {set j 0} {$j < 100} {incr j} { + r set "{foo}-$j" "foo"; # slot 12182 + r set "{bar}-$j" "bar"; # slot 5061 + r set "{boo}-$j" "boo"; # slot 13142 + } + + set cursor 0 + set keys {} + while 1 { + set res [r scan $cursor match "{foo}-*"] + set cursor [lindex $res 0] + set k [lindex $res 1] + lappend keys {*}$k + if {$cursor == 0} break + } + + set keys [lsort -unique $keys] + assert_equal 100 [llength $keys] + } } start_server {tags {"scan network standalone"}} {