redis/tests/unit/cluster/misc.tcl
debing.sun 235e688b01
Some checks are pending
CI / test-ubuntu-latest (push) Waiting to run
CI / test-sanitizer-address (push) Waiting to run
CI / build-debian-old (push) Waiting to run
CI / build-macos-latest (push) Waiting to run
CI / build-32bit (push) Waiting to run
CI / build-libc-malloc (push) Waiting to run
CI / build-centos-jemalloc (push) Waiting to run
CI / build-old-chain-jemalloc (push) Waiting to run
Codecov / code-coverage (push) Waiting to run
External Server Tests / test-external-standalone (push) Waiting to run
External Server Tests / test-external-cluster (push) Waiting to run
External Server Tests / test-external-nodebug (push) Waiting to run
Spellcheck / Spellcheck (push) Waiting to run
RED-135816: Lookahead pre-fetching (#14440)
## Problem and Motivation
Currently, the client only parses one command, then executes it, then
parses new commands until the querybuf is consumed. Doing it this way
means we cannot perform memory prefetch when IO threads are not enabled,
and when IO threads are enabled, we can only parse the first command in
the IO thread, while the remaining command parsing still needs to be
done in the main thread.

This describes a limitation in the current Redis command processing
pipeline where:

Without IO threads: Commands are parsed and executed one by one
sequentially, preventing memory prefetching optimizations
With IO threads: Only the first command gets parsed in the IO thread,
but subsequent commands from the same client's query buffer must still
be parsed in the main thread

## Solution Overview

**Core Innovation**: Parse multiple user commands in advance through a
lookahead pipeline.

**Key Insight**: Since Redis already parses commands to extract keys, we
can do this parsing earlier and memory prefetch operations before the
command reaches execution, allowing multiple I/O operations to run in
parallel.

The bulk of the PR is a redesign of the command processing flow for both
standalone commands and transactional commands.

### High Level Command Processing Flow

#### Before This PR (processInputBuffer())

- While there is data in the client's query buffer:
- Read the data and try to parse a complete command
(processInlineBuffer() or processMultibulkBuffer()).
  - If the command is incomplete, exit and wait for more data.
- The Command is complete. Process and potentially execute it
(processCommandAndResetClient(), processCommand()):
    - Prepare for the next command (commandProcessed()).

### Major Changes in the Client's Structure

To support the new command processing flow:

- **New pendingCommand structure**: Since the previous flow processed
commands one at a time, it used the client structure to hold the current
(and only) parsed command arguments (argv/argc) and other metadata. In
the new design, multiple commands are processed, waiting for execution.
So, a new pendingCommand structure is introduced to hold a parsed
command's arguments and its metadata.
- **New pendingCommandList structure (pending_cmds)** that contains all
the pending commands with maintained order and includes a ready_len
counter that tracks the number of fully parsed commands ready for
execution. All commands are fully parsed except possibly the last one
(client's command order is maintained).
- **New pendingCommandPool structure (cmd_pool)** that manages a shared
pool for reusing pendingCommand objects to reduce memory allocation
overhead.

There is a configurable lookahead limit (server.lookahead) that controls
how many fully parsed commands (ready pending commands) to process ahead
of time.

#### New High Level Flow for Standalone Commands (processInputBuffer())

- While there is data in the client's query buffer or there are ready
pending commands:
- While there is data in the client's query buffer and we haven't
reached the lookahead limit:
- Read the data and try to parse a complete command
(processInlineBuffer() or processMultibulkBuffer()). Allocate a new
pending command if needed, store the command's metadata in the pending
command, and add the pending command to the client's pending commands
list.
    - If the command is incomplete, exit and wait for more data.
- The command is complete, we have a new ready pending command,
preprocess it (preprocessCommand()):
- Extract the keys of the command and store the results in the pending
command (extractKeysAndSlot()).
- If there are pending commands, continue executing them until the queue
is empty.

## Transaction Support

### Major Changes in Structures

- The multiState structure now contains an array of pendingCommand
pointers instead of multiCmd pointers.
- The multiCmd structure was deleted (no longer needed).

### New Transaction Support

- queueMultiCommand():
- The pending commands are moved from the client's pending_cmds list to
the multiState's commands array.

## Detailed Changes

### Additional Client Structure Changes

- Replaced argv_len_sum with all_argv_len_sum to reflect the total
memory consumed by all pending commands.

### Clients and Pending Commands Management

- Clients using pending commands now manage the command arguments via
the pendingCommand. Specifically, the memory occupied by argv.
- **Pending commands management functions**:
- `initPendingCommand()` initializes a newly allocated pending command.
- `freeClientPendingCommand()` frees a pending command of a client and
its associated resources.
- `freeClientPendingCommands()` receives the number of pending commands
to free and calls freeClientPendingCommand() to free them.

### Buffer Processing Changes

- `processInlineBuffer()`, once a full command is read, used to populate
the client's command fields (argc, argv, etc.). Now it creates and
populates a pendingCommand, and adds it to the client's pending_cmds
list.
- `processMultibulkBuffer()`: Similar changes to processInlineBuffer().
The difference is that a pending command may already exist from a
previous call to the function, so parsing will continue populating it
instead of creating a new one.
- `resetClientInternal()` used to receive a free_argv parameter and pass
it to freeClientArgvInternal(), which freed the client's argv if set,
and also reset client's command fields. It now receives the number of
pending commands to free and handles two cases:
- The client uses pending commands so they are freed by calling
freeClientPendingCommands().
- The client doesn't use pending commands (e.g., LUA client) so the
client's argv is freed by calling freeClientArgvInternal().
It then frees the client's command fields that freeClientArgvInternal()
doesn't free now.

### Other Changes

- Simulate lookahead command preprocessing when loading an AOF and
queuing transaction commands; This is necessary since
queueMultiCommand() now requires a pending command.
- The INVALID_CLUSTER_SLOT constant was defined to indicate an invalid
cluster slot. It is used to signal a cross-slot error in
preprocessCommand().
- getNodeByQuery() no longer performs cross-slot checks, relying instead
on the checks already performed in preprocessCommand(). It also no
longer calls getKeysFromCommand() as this was also done in
preprocessCommand().

### Debugging

- Added "debug lookahead" command to print the size of the lookahead
pipeline for each client.

## New Configuration

- **lookahead**: Runtime-configurable lookahead depth (default: 16)

## Security

- **Limit lookahead for unauthenticated clients to 1**. This is both to
reduce memory overhead, and to prevent errors; AUTH can affect the
handling of succeeding commands.


---------

Co-authored-by: Slava Koyfman <slava.koyfman@redis.com>
Co-authored-by: Oran Agra <oran@redis.com>
Co-authored-by: Udi Ron <udi.ron@redis.com>
Co-authored-by: moticless <moticless@github.com>
Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2025-10-23 00:16:32 +08:00

36 lines
1.3 KiB
Tcl

start_cluster 2 2 {tags {external:skip cluster}} {
test {Key lazy expires during key migration} {
R 0 DEBUG SET-ACTIVE-EXPIRE 0
set key_slot [R 0 CLUSTER KEYSLOT FOO]
R 0 set FOO BAR PX 10
set src_id [R 0 CLUSTER MYID]
set trg_id [R 1 CLUSTER MYID]
R 0 CLUSTER SETSLOT $key_slot MIGRATING $trg_id
R 1 CLUSTER SETSLOT $key_slot IMPORTING $src_id
after 11
assert_error {ASK*} {R 0 GET FOO}
R 0 ping
} {PONG}
test "Coverage: Basic cluster commands" {
assert_equal {OK} [R 0 CLUSTER saveconfig]
set id [R 0 CLUSTER MYID]
assert_equal {0} [R 0 CLUSTER count-failure-reports $id]
R 0 flushall
assert_equal {OK} [R 0 CLUSTER flushslots]
}
test "CROSSSLOT error for keys in different slots" {
# Test MSET with keys in different slots
assert_error {*CROSSSLOT Keys in request don't hash to the same slot*} {R 0 MSET foo bar baz qux}
# Test DEL with keys in different slots
assert_error {*CROSSSLOT Keys in request don't hash to the same slot*} {R 0 DEL foo bar}
# Test MGET with keys in different slots
assert_error {*CROSSSLOT Keys in request don't hash to the same slot*} {R 0 MGET foo bar}
}
}