-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Description
Expected Behavior
When using pipeline with mixed results (some keys exist, some don't), each command should have its own correct error state:
Commands for non-existent keys: val="", err=redis.Nil
Commands for existing keys: val="value", err=nil
This was the behavior in v9.12.1 and earlier versions.
Current Behavior
In v9.13.0+, when a pipeline contains a command that returns redis.Nil (e.g., GET on non-existent key), subsequent commands that successfully return values are incorrectly marked with redis.Nil error.
Example:
Command 1 (non-existent key): val="", err=redis.Nil ✅ Correct
Command 2 (existing key): val="value2", err=redis.Nil ❌ Should be err=nil
Command 3 (existing key): val="value3", err=redis.Nil ❌ Should be err=nil
Possible Solution
The issue appears to be related to PR #3496 which changed how pipeline errors are handled. The redis.Nil error from one command seems to be polluting subsequent commands' error states, even though those commands have valid values.
The fix likely needs to ensure that each command maintains its own independent error state during pipeline result processing.
Steps to Reproduce
- Create a Redis client with go-redis v9.13.0 or later
- Set up test data where some keys exist and some don't:
redis.Set(ctx, "test:key2", "value2", time.Minute)
redis.Set(ctx, "test:key3", "value3", time.Minute)
// test:key1 intentionally not set
- Execute a pipeline with GET commands for all three keys:
pipeline := rdb.Pipeline()
cmd1 := pipeline.Get(ctx, "test:key1") // Non-existent
cmd2 := pipeline.Get(ctx, "test:key2") // Exists
cmd3 := pipeline.Get(ctx, "test:key3") // Exists
_, _ = pipeline.Exec(ctx)
- Check each command's result:
val2, err2 := cmd2.Result()
// BUG: val2="value2" but err2=redis.Nil (should be nil)
Test case
func TestPipelineNilErrorPollution(t *testing.T) {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
defer rdb.Close()
rdb.Del(ctx, "test:key1", "test:key2", "test:key3")
rdb.Set(ctx, "test:key2", "value2", time.Minute)
rdb.Set(ctx, "test:key3", "value3", time.Minute)
pipeline := rdb.Pipeline()
cmd1 := pipeline.Get(ctx, "test:key1")
cmd2 := pipeline.Get(ctx, "test:key2")
cmd3 := pipeline.Get(ctx, "test:key3")
_, execErr := pipeline.Exec(ctx)
t.Logf("execErr: %v", execErr)
val1, err1 := cmd1.Result()
t.Logf("cmd1: val=%q, err=%v, err==redis.Nil=%v", val1, err1, err1 == redis.Nil)
// Expected: val1="", err1=redis.Nil
val2, err2 := cmd2.Result()
t.Logf("cmd2: val=%q, err=%v, err==redis.Nil=%v", val2, err2, err2 == redis.Nil)
// Expected: val2="value2", err2=nil
val3, err3 := cmd3.Result()
t.Logf("cmd3: val=%q, err=%v, err==redis.Nil=%v", val3, err3, err3 == redis.Nil)
// Expected: val3="value3", err3=nil
if val2 == "value2" && err2 != nil {
t.Errorf("BUG: cmd2 has value but error is %v", err2)
}
}