Skip to content

feat: redis adapter for entity caching cache - #3139

Open
SkArchon wants to merge 1 commit into
milinda/eng-9922-in-memory-adapterfrom
milinda/eng-9923-adapter-for-redis
Open

feat: redis adapter for entity caching cache#3139
SkArchon wants to merge 1 commit into
milinda/eng-9922-in-memory-adapterfrom
milinda/eng-9923-adapter-for-redis

Conversation

@SkArchon

@SkArchon SkArchon commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

This PR contains a redis adapter to be used for entity caching, currently it does not have support for managing cache tags.

Summary by CodeRabbit

  • New Features

    • Added Redis-backed caching with efficient batch reads and writes.
    • Supports configurable key prefixes, duplicate keys, missing entries, and item expiration.
    • Added validation for invalid cache clients and missing or invalid expiration times.
    • Preserves requested key order and reports connectivity or Redis errors.
  • Bug Fixes

    • Prevents partially applying batches that contain invalid expiration settings.
    • Correctly handles expired entries, empty values, and duplicate lookups.

Checklist

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Added RedisCache with prefixed, pipelined GetMany and SetMany operations. The implementation validates clients and TTLs, handles misses and Redis errors, and includes Miniredis tests for batching, expiry, failures, and context behavior.

Changes

Redis cache

Layer / File(s) Summary
Cache construction and interface
router/pkg/entitycaching/cache/redis.go, router/pkg/entitycaching/cache/redis_test.go
RedisCache stores the Redis client and prefix, rejects nil clients, and satisfies enginecache.Cache.
Pipelined batch reads
router/pkg/entitycaching/cache/redis.go, router/pkg/entitycaching/cache/redis_test.go
GetMany preserves order and duplicates, prefixes keys, handles misses, and returns Redis errors. Tests cover reads, expiry, failures, and context behavior.
Validated pipelined writes
router/pkg/entitycaching/cache/redis.go, router/pkg/entitycaching/cache/redis_test.go
SetMany validates positive TTLs before execution and writes prefixed keys through a pipeline. Tests cover overwrites, TTLs, invalid batches, connection failures, and context behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding a Redis adapter for entity caching.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the router label Aug 5, 2026
@SkArchon SkArchon changed the title feat: redis adapter feat: redis adapter for entity caching cache Aug 5, 2026
@SkArchon
SkArchon marked this pull request as ready for review August 5, 2026 19:59
@SkArchon
SkArchon requested a review from a team as a code owner August 5, 2026 19:59
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Router image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-ea81775d06bb8d379e4010e76b25f38a006349c7

@SkArchon
SkArchon force-pushed the milinda/eng-9923-adapter-for-redis branch from 3845440 to 3562724 Compare August 5, 2026 20:01
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.46%. Comparing base (d508d07) to head (3562724).

Additional details and impacted files
@@                          Coverage Diff                           @@
##           milinda/eng-9922-in-memory-adapter    #3139      +/-   ##
======================================================================
+ Coverage                               62.39%   62.46%   +0.06%     
======================================================================
  Files                                     263      264       +1     
  Lines                                   31043    31075      +32     
======================================================================
+ Hits                                    19370    19410      +40     
+ Misses                                  10163    10158       -5     
+ Partials                                 1510     1507       -3     
Files with missing lines Coverage Δ
router/pkg/entitycaching/cache/redis.go 100.00% <100.00%> (ø)

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dkorittki

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
router/pkg/entitycaching/cache/redis_test.go (1)

203-218: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test does not test what its name says.

The subtest is named "cache tags are accepted and ignored". The item carries no tag field, and no assertion refers to tags. The body is identical to "namespaces keys with the prefix" at Lines 108-119.

Either remove the subtest, or add a real tag field to the item once the cache item type exposes one.

♻️ Proposed removal
-		t.Run("cache tags are accepted and ignored", func(t *testing.T) {
-			t.Parallel()
-
-			c, mr := newTestRedisCache(t)
-
-			err := c.SetMany(ctx, []enginecache.Item{
-				{
-					Key:   "a",
-					Value: []byte("value"),
-					TTL:   time.Minute,
-				},
-			})
-			require.NoError(t, err)
-
-			require.Equal(t, []string{testPrefix + "a"}, mr.Keys())
-		})
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/pkg/entitycaching/cache/redis_test.go` around lines 203 - 218, Remove
the redundant “cache tags are accepted and ignored” subtest from the Redis cache
tests, since its item has no tags and it duplicates the existing
namespace-prefix coverage.
router/pkg/entitycaching/cache/redis.go (1)

47-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Error context is inconsistent between the two failure paths.

If Exec reports a command error other than redis.Nil, GetMany returns it unwrapped. The per-command loop below wraps the same class of error with the failing key. The result is that identical Redis failures produce different messages depending on whether a redis.Nil miss appears earlier in the batch. The test at redis_test.go Lines 374-388 depends on that ordering detail to get the key into the message.

Consider dropping the Exec error early return for command-level failures and letting the per-command loop classify every error, so the key is always present. Keep a check for transport-level failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/pkg/entitycaching/cache/redis.go` around lines 47 - 50, Update GetMany
so the pipe.Exec error handling does not return command-level failures before
the per-command loop can add the failing key; retain only the transport-level
failure check at the Exec stage. Let the existing per-command classification and
wrapping logic handle each command error consistently, including batches
containing redis.Nil misses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@router/pkg/entitycaching/cache/redis.go`:
- Around line 52-63: Update GetMany’s successful StringCmd.Bytes() result
handling to assign bytes.Clone(value) to enginecache.Result.Value, ensuring
returned values do not alias Redis command storage while preserving the existing
error and missing-key behavior.

---

Nitpick comments:
In `@router/pkg/entitycaching/cache/redis_test.go`:
- Around line 203-218: Remove the redundant “cache tags are accepted and
ignored” subtest from the Redis cache tests, since its item has no tags and it
duplicates the existing namespace-prefix coverage.

In `@router/pkg/entitycaching/cache/redis.go`:
- Around line 47-50: Update GetMany so the pipe.Exec error handling does not
return command-level failures before the per-command loop can add the failing
key; retain only the transport-level failure check at the Exec stage. Let the
existing per-command classification and wrapping logic handle each command error
consistently, including batches containing redis.Nil misses.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e4fbd543-f34b-46c0-92d8-b9fbf1d6e9e1

📥 Commits

Reviewing files that changed from the base of the PR and between d508d07 and 3562724.

📒 Files selected for processing (2)
  • router/pkg/entitycaching/cache/redis.go
  • router/pkg/entitycaching/cache/redis_test.go

Comment on lines +52 to +63
results := make([]enginecache.Result, len(keys))
for i, cmd := range cmds {
value, err := cmd.Bytes()
if errors.Is(err, redis.Nil) {
continue
}
if err != nil {
// There is no partial read to salvage, the whole batch fails.
return nil, fmt.Errorf("get %q: %w", keys[i], err)
}
results[i] = enginecache.Result{Value: value, Found: true}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect go-redis StringCmd.Bytes() implementation in the module cache / vendor dir.
set -euo pipefail

# Locate the go-redis version in use.
fd -t f 'go.mod' | xargs rg -n 'redis/go-redis/v9'

# Try vendored source first.
fd -t d 'go-redis' | head -20

# Search any available copy of the library for the Bytes implementation.
rg -nP -C 6 'func \(cmd \*StringCmd\) Bytes\(\)' $(go env GOMODCACHE 2>/dev/null || echo /root/go/pkg/mod) 2>/dev/null | head -40

Repository: wundergraph/cosmo

Length of output: 411


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- target implementation and test ---'
sed -n '1,100p' router/pkg/entitycaching/cache/redis.go
sed -n '300,355p' router/pkg/entitycaching/cache/redis_test.go

printf '%s\n' '--- dependency metadata ---'
sed -n '1,45p' router/go.mod

printf '%s\n' '--- local module source candidates ---'
modcache="$(go env GOMODCACHE 2>/dev/null || true)"
printf 'GOMODCACHE=%s\n' "$modcache"
if [ -n "$modcache" ] && [ -d "$modcache" ]; then
  find "$modcache" -path '*redis*go-redis*v9*' -type f -maxdepth 8 2>/dev/null | head -30
  rg -n -P -C 8 'func \(cmd \*StringCmd\) Bytes\(\)' "$modcache" 2>/dev/null | head -80 || true
fi

printf '%s\n' '--- upstream v9.7.3 source ---'
curl -fsSL https://raw.githubusercontent.com/redis/go-redis/v9.7.3/command.go \
  | rg -n -C 8 'func \(cmd \*StringCmd\) Bytes\(\)|StringToBytes|func \(cmd \*StringCmd\) Result' || true
curl -fsSL https://raw.githubusercontent.com/redis/go-redis/v9.7.3/internal/util/unsafe.go \
  | sed -n '1,120p' || true

Repository: wundergraph/cosmo

Length of output: 7786


🏁 Script executed:

cat <<'GO' | go run /dev/stdin
package main

import (
	"fmt"
	"unsafe"
)

func stringToBytes(s string) []byte {
	return *(*[]byte)(unsafe.Pointer(&struct {
		string
		Cap int
	}{s, len(s)}))
}

func main() {
	s := "value"
	b := stringToBytes(s)
	b[0] = 'V'
	fmt.Printf("string=%q bytes=%q\n", s, b)
}
GO

Repository: wundergraph/cosmo

Length of output: 273


Clone StringCmd.Bytes() before returning it. In go-redis v9.7.3, StringCmd.Bytes() uses an unsafe zero-copy conversion. GetMany therefore returns a slice that aliases the command's string storage. Use bytes.Clone(value) to match InMemoryCache.GetMany ownership semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/pkg/entitycaching/cache/redis.go` around lines 52 - 63, Update
GetMany’s successful StringCmd.Bytes() result handling to assign
bytes.Clone(value) to enginecache.Result.Value, ensuring returned values do not
alias Redis command storage while preserving the existing error and missing-key
behavior.

Comment on lines +12 to +13
// RedisCache stores entries in Redis. Redis owns expiry, so unlike
// InMemoryCache there is nothing to sweep here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wouldn't mention the in memory store here. Has nothing to do with this implementation + I always like to mention the interface this intends to implement.

Suggested change
// RedisCache stores entries in Redis. Redis owns expiry, so unlike
// InMemoryCache there is nothing to sweep here.
// RedisCache stores entries in Redis. It implements enginecache.Cache

// InMemoryCache there is nothing to sweep here.
type RedisCache struct {
client redis.UniversalClient
prefix string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add godoc to fields as well? I.e. whats prefix good for?

}

// A miss surfaces as redis.Nil, which is not a failure of the batch.
if _, err := pipe.Exec(ctx); err != nil && !errors.Is(err, redis.Nil) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: too much happening in one line. Can you split it up?

Suggested change
if _, err := pipe.Exec(ctx); err != nil && !errors.Is(err, redis.Nil) {
_, err := pipe.Exec(ctx)
if err != nil && !errors.Is(err, redis.Nil) {

// There is no partial read to salvage, the whole batch fails.
return nil, fmt.Errorf("get %q: %w", keys[i], err)
}
results[i] = enginecache.Result{Value: value, Found: true}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cmd.Bytes() does not copy the value but instead returns the value as an unsafe.Pointer. As you return this value its possible someone outside this package will modify the value under the hood. Better copy it.

Suggested change
results[i] = enginecache.Result{Value: value, Found: true}
results[i] = enginecache.Result{Value: bytes.Clone(value), Found: true}

func TestRedisCache(t *testing.T) {
t.Parallel()

ctx := context.Background()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use t.Context() instead

require.Empty(t, mr.Keys())
})

t.Run("one bad item rejects the whole batch", func(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test contradicts the contract defined in the godoc of SetMany where it states "An error means an unspecified subset of the items may already have been stored"

require.Empty(t, mr.Keys())
})

t.Run("cache tags are accepted and ignored", func(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where exactly are cache tags used in this test? I don't know much about these tags but it does not look obvious from looking at the test

err := c.SetMany(ctx, []enginecache.Item{
{Key: "a", Value: []byte("value"), TTL: time.Minute},
})
require.Error(t, err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use assert.Contains to broadly verify the error is about failure to reach redis

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants