Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/residual-review-findings/0045d5f.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Residual Review Findings

- **P2** — `rueidiscompatmock/adapter.go:567` — Repeated in-TTL cached reads diverge from real client-side caching: the DoCache stub is always-miss — filed: https://github.com/redis/rueidis/issues/1022

---

Source run context:
- Review run: ce-code-review run-id `20260816-094523-053cdd06` (artifact dir `/tmp/compound-engineering-501/ce-code-review/20260816-094523-053cdd06`)
- Branch: `fix/rueidiscompatmock-cache-mock` @ `0045d5f`
- Plan: `docs/plans/2026-08-16-002-rueidiscompatmock-cache-mock-plan.md`
- Finding type: `settled_conflict` — stamped against KTD2 (Ignore the TTL argument in matching; session-settled). Report-only: the alternative arm (in-memory TTL cache in the stub) is the rejected alternative and is not applied; the compatible arm (document the always-miss divergence) is the ticket's suggested fix.
- Owner: human (advisory-grade; not in the downstream-resolver actionable queue)
Comment thread
SomSamantray marked this conversation as resolved.
Outdated
12 changes: 12 additions & 0 deletions rueidiscompatmock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,16 @@ p := rdb.Pipeline()
p.Get(ctx, "k1")
p.Set(ctx, "k2", "v2", 0)
p.Exec(ctx)
```

### Client-side caching example

Cached commands (`Cache(ttl)`) use the same `Expect*` calls as non-cached commands.
The TTL argument is not matched against expectations.

```golang
compatmock.ExpectGet("key").SetVal("val")

cached := rdb.Cache(100 * time.Millisecond)
cached.Get(context.Background(), "key")
```
6 changes: 6 additions & 0 deletions rueidiscompatmock/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,12 @@ func (m *clientMock) wire() {
return m.consume(len(cmds), cmds)
}).
AnyTimes()
m.raw.EXPECT().
DoCache(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, cmd rueidis.Cacheable, _ time.Duration) rueidis.RedisResult {
return m.consume(1, []rueidis.Completed{rueidis.Completed(cmd)})[0]
}).
AnyTimes()
}

func (m *clientMock) consume(n int, cmds []rueidis.Completed) []rueidis.RedisResult {
Expand Down
132 changes: 132 additions & 0 deletions rueidiscompatmock/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3066,6 +3066,138 @@ func wantAnyErr(err error) error {
return nil
}

func TestCacheCompatGet(t *testing.T) {
ctx := context.Background()
rdb, cm := NewClientMock()
defer rdb.Close()

cm.ExpectGet("key").SetVal("v")
Comment thread
SomSamantray marked this conversation as resolved.
Outdated
cmd := rdb.Cache(100 * time.Millisecond).Get(ctx, "key")
Comment thread
SomSamantray marked this conversation as resolved.
Outdated
if got := cmd.Val(); got != "v" {
t.Fatalf("expected %q, got %q", "v", got)
}
if err := cm.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected err %v", err)
}
}

func TestCacheCompatViaNewAdapter(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.Background()
raw := mock.NewClient(ctrl)
cm := NewAdapter(raw)
rdb := rueidiscompat.NewAdapter(raw)

cm.ExpectGet("key").SetVal("v")
cmd := rdb.Cache(time.Second).Get(ctx, "key")
if got := cmd.Val(); got != "v" {
t.Fatalf("expected %q, got %q", "v", got)
}
if err := cm.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected err %v", err)
}
}

func TestCacheCompatMultiCommand(t *testing.T) {
ctx := context.Background()
rdb, cm := NewClientMock()
defer rdb.Close()

cm.ExpectMGet("k1", "k2").SetVal([]any{"v1", "v2"})
cm.ExpectHGetAll("h").SetVal(map[string]string{"f": "v"})
cm.ExpectGetRange("s", 0, 1).SetVal("ab")

cache := rdb.Cache(time.Minute)
if got := cache.MGet(ctx, "k1", "k2").Val(); !reflect.DeepEqual(got, []any{"v1", "v2"}) {
t.Fatalf("unexpected MGet result %v", got)
}
if got := cache.HGetAll(ctx, "h").Val(); !reflect.DeepEqual(got, map[string]string{"f": "v"}) {
t.Fatalf("unexpected HGetAll result %v", got)
}
if got := cache.GetRange(ctx, "s", 0, 1).Val(); got != "ab" {
t.Fatalf("unexpected GetRange result %q", got)
}
if err := cm.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected err %v", err)
}
}

func TestCacheCompatCacheableWrapped(t *testing.T) {
ctx := context.Background()
rdb, cm := NewClientMock()
defer rdb.Close()

cm.ExpectEvalRO("return 1", []string{}).SetValInt(1)
cmd := rdb.Cache(time.Second).EvalRO(ctx, "return 1", []string{})
if got := cmd.Val(); got != int64(1) {
t.Fatalf("unexpected EvalRO result %v", got)
}
if err := cm.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected err %v", err)
}
}

func TestCacheCompatRedisNil(t *testing.T) {
ctx := context.Background()
rdb, cm := NewClientMock()
defer rdb.Close()

cm.ExpectGet("key").RedisNil()
if err := rdb.Cache(time.Second).Get(ctx, "key").Err(); !errors.Is(err, rueidiscompat.Nil) {
t.Fatalf("expected Nil, got %v", err)
}
if err := cm.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected err %v", err)
}
}

func TestCacheCompatSetErr(t *testing.T) {
ctx := context.Background()
rdb, cm := NewClientMock()
defer rdb.Close()

cm.ExpectGet("key").SetErr(errors.New("any"))
if err := wantAnyErr(rdb.Cache(time.Second).Get(ctx, "key").Err()); err != nil {
t.Fatal(err)
}
if err := cm.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected err %v", err)
}
}

func TestCacheCompatTTLIrrelevant(t *testing.T) {
ctx := context.Background()
rdb, cm := NewClientMock()
defer rdb.Close()

cm.ExpectGet("k").SetVal("a")
cm.ExpectGet("k").SetVal("b")
if got := rdb.Cache(time.Millisecond).Get(ctx, "k").Val(); got != "a" {
t.Fatalf("expected %q, got %q", "a", got)
}
if got := rdb.Cache(time.Hour).Get(ctx, "k").Val(); got != "b" {
t.Fatalf("expected %q, got %q", "b", got)
}
if err := cm.ExpectationsWereMet(); err != nil {
t.Fatalf("unexpected err %v", err)
}
}

func TestCacheCompatUnmatched(t *testing.T) {
ctx := context.Background()
rdb, cm := NewClientMock()
defer rdb.Close()

err := rdb.Cache(time.Second).Get(ctx, "key").Err()
if err == nil || !strings.Contains(err.Error(), "no expectation for command") {
t.Fatalf("expected no-expectation error, got %v", err)
}
if err := cm.ExpectationsWereMet(); err == nil {
t.Fatal("expected unmatched expectation error")
}
}

func runCases(t *testing.T, cm ClientMock, cases []func() error) {
t.Helper()
for i, c := range cases {
Expand Down