RedisDriver.keys() runs KEYS, which walks the whole keyspace in one command and blocks the server while it does. Every other client of that Redis waits until it finishes, however few keys match.
Measurement
Redis 8.8.0 holding 1,000,000 keys. One client calls driver.keys('__stacks_tag__:<32 hex>:*'), which matches nothing, while another sends PING in a loop:
| call | worst PING | |
|---|---|---|
| idle (control) | - | 2 ms |
keys(pattern), 3 runs | 76-80 ms | 75-79 ms |
So the blocked time is the scan of the whole keyspace, not of the matches, and it grows with the size of the database. At 10M keys that is most of a second during which the server answers nothing else.
Where it is reached
keys(pattern)directly.@stacksjs/cachenow flushes a tag by listing that tag's index entries (stacksjs/stacks#3073), so every tag flush on Redis is one of these.flush()with a prefix:keys('*'), then a singleDELwith every key as an argument.getStats():keys('*')on every call.
Fix
Walk the keyspace with SCAN cursor MATCH pattern COUNT n until the cursor comes back as 0, de-duplicating, since SCAN may return a key twice. The total work is the same, but each step is short and other clients are served between steps. flush() deletes in batches rather than with one DEL of everything.
Found while auditing @stacksjs/cache for 1.0 (stacksjs/stacks#1169).