Skip to content

Pipeline commands with values incorrectly have redis.Nil error after v9.13.0 (PR #3496) #3556

@chrisytw

Description

@chrisytw

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

  1. Create a Redis client with go-redis v9.13.0 or later
  2. 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
  1. 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)
  1. 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)
	}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions