Skip to content

fix(decoder): copy collection in ContainsDecoder to prevent IndexOutOfBoundsException during watchdog renewal (#7402) - #7404

Open
ronitgupta138 wants to merge 1 commit into
redisson:masterfrom
ronitgupta138:fix/contains-decoder-alias-copy
Open

ronitgupta138 wants to merge 1 commit into
redisson:masterfrom
ronitgupta138:fix/contains-decoder-alias-copy

Conversation

@ronitgupta138

Copy link
Copy Markdown
Contributor

Problem

In ContainsDecoder, passing a List previously skipped copying and stored the instance directly:

public ContainsDecoder(Collection<T> args) {
    if (args instanceof List) {
        this.args = (List<T>) args;      // aliased caller's list
    } else {
        this.args = new ArrayList<>(args);
    }
}

During lock watchdog renewal in LockTask.renew(), the caller dispatches the command and immediately registers a completion callback that mutates the same keys list:

new RedisCommand<>("EVAL", new ContainsDecoder<>(keys)),
...
return f.thenCompose(existingNames -> {
    keys.removeAll(existingNames);
    ...
});

When a response arrives late or is decoded across a retry/channel switch after the callback has already mutated the collection, decode() iterates over parts matching the original number of keys, but accesses args.get(index) from the shrunken list, throwing:

java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
    at java.base/java.util.ArrayList.get(ArrayList.java:428)
    at org.redisson.client.protocol.decoder.ContainsDecoder.decode(ContainsDecoder.java:50)

Because this exception aborts decoding, the renewal future never completes, stalling the watchdog loop and allowing held locks to expire under lockWatchdogTimeout.

Solution

Always defensive-copy the collection into a new ArrayList in ContainsDecoder constructor:

public ContainsDecoder(Collection<T> args) {
    this.args = new ArrayList<>(args);
}

Added JUnit 5 regression test suite ContainsDecoderTest verifying:

  1. Mutating the caller's key list after decoder creation does not affect decoding or throw IndexOutOfBoundsException.
  2. Standard decode operations return the expected matched keys.

Fixes #7402.

…fBoundsException during watchdog renewal (redisson#7402)

Signed-off-by: Ronit Gupta <[email protected]>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 18:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mrniko

mrniko commented Nov 2, 2026

Copy link
Copy Markdown
Member

same comment to this fix #7403 (review)

@ronitgupta138

Copy link
Copy Markdown
Contributor Author

Hi @mrniko,

Thanks for the feedback. In normal sequential execution, decode() indeed completes before onSuccess runs.

However, in CommandAsyncService.syncedEval():

if (ex instanceof RedisNoScriptException) {
    MasterSlaveEntry entry = connectionManager.getEntry(key);
    return loadScript(entry.getClient(), script).thenCompose(r3 ->
            syncedEval(timeout, syncMode, retry, key, codec, evalCommandType, script, keys, params));
}

When Redis returns NOSCRIPT (e.g. during cluster failovers or when script cache is flushed), syncedEval catches the exception and retries using the exact same evalCommandType (holding new ContainsDecoder<>(keys)). If the chunk callback or cancellation has already removed from keys, the subsequent decode() attempt on retry operates on a shortened list, producing the IndexOutOfBoundsException reported in #7402.

Snapshotting the list protects against mutable state leaking into retried or re-executed decoders.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

IndexOutOfBoundsException in ContainsDecoder.decode from lock watchdog renewal (mutable args list aliased)

3 participants