Skip to content

feat: in memory adapter for entity caching cache - #3137

Open
SkArchon wants to merge 7 commits into
mainfrom
milinda/eng-9922-in-memory-adapter
Open

feat: in memory adapter for entity caching cache#3137
SkArchon wants to merge 7 commits into
mainfrom
milinda/eng-9922-in-memory-adapter

Conversation

@SkArchon

@SkArchon SkArchon commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

This PR introduces the in memory adapter for entity caching cache based on the interface defined here in this PR wundergraph/graphql-go-tools#1618

Summary by CodeRabbit

  • New Features
    • Added an in-memory entity cache with batch reads and writes, ordered multi-key results, and per-entry expiration.
    • Rejects batches containing entries without a positive expiration time.
    • Protects cached values from unintended modification and automatically removes expired entries.
  • Bug Fixes
    • Ensures cache operations honor context cancellation and deadlines.
  • Tests
    • Added comprehensive coverage for expiration, eviction, ordering, overwrites, empty values, and cache compatibility.
  • Chores
    • Updated the GraphQL tools dependency.

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 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a mutex-protected in-memory entity cache with batch reads and writes, mandatory TTL expiration, context cancellation, defensive byte-slice copying, expiration cleanup, and comprehensive tests. Updates the GraphQL tools dependency.

Changes

Entity cache

Layer / File(s) Summary
Cache storage and batch operations
router/pkg/entitycaching/cache/in_memory.go, router/go.mod, router-tests/go.mod
Adds InMemoryCache with injectable time, ordered batch reads, batch writes, TTL validation, cancellation checks, defensive value copying, expired-entry cleanup, and missing-TTL reporting. Updates the GraphQL tools dependency.
Cache behavior validation
router/pkg/entitycaching/cache/in_memory_test.go
Tests initialization, batch storage and retrieval, duplicate keys, overwrites, TTL expiration, eviction, value copying, empty values, cache tags, partial writes, and expiration sweeping.
Context and interface validation
router/pkg/entitycaching/cache/in_memory_test.go
Tests canceled and expired contexts, failed writes, live contexts, cache-interface compliance, and deterministic time handling.

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

Possibly related PRs

🚥 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 an in-memory 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

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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

❌ Internal Query Planner CI checks failed

The Internal Query Planner CI checks failed in the celestial repository, and this is going to stop the merge of this PR.
If you are part of the WunderGraph organization, you can see the PR with more details.

@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

🤖 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/in_memory.go`:
- Around line 58-68: Update the in-memory cache implementation around the
GetMany expiration cleanup and c.entries to prevent unrequested expired entries
from accumulating indefinitely. Add a bounded capacity policy with periodic
expiration sweeping, or replace it with a bounded TTL cache that has an explicit
shutdown lifecycle; preserve concurrency safety and ensure cleanup does not
delete entries refreshed since observation.
🪄 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: 96515d7e-f2fd-44d4-ab66-c635979310e1

📥 Commits

Reviewing files that changed from the base of the PR and between bbf752b and aa38079.

⛔ Files ignored due to path filters (1)
  • router/go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • router/go.mod
  • router/pkg/entitycaching/cache/in_memory.go
  • router/pkg/entitycaching/cache/in_memory_test.go

Comment thread router/pkg/entitycaching/cache/in_memory.go Outdated

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
router/pkg/entitycaching/cache/in_memory.go (1)

50-63: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Recheck the context after mutex acquisition.

Both methods check ctx.Err() before a mutex operation that can block. If the context expires while waiting, GetMany can return data after cancellation and SetMany can store data after cancellation.

  • router/pkg/entitycaching/cache/in_memory.go#L50-L63: check ctx.Err() immediately after RLock succeeds and return nil, err before reading entries.
  • router/pkg/entitycaching/cache/in_memory.go#L92-L105: check ctx.Err() immediately after Lock succeeds and return before writing entries.
  • router/pkg/entitycaching/cache/in_memory_test.go#L542-L597: add contention tests that hold c.mu, cancel the context, then release the mutex; verify no result and no write.
🤖 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/in_memory.go` around lines 50 - 63, Recheck
the context immediately after `GetMany` acquires `c.mu` with `RLock`, returning
no results and the context error before reading entries; apply the same guard
after `SetMany` acquires `c.mu` with `Lock`, returning before writes. In
`router/pkg/entitycaching/cache/in_memory.go` lines 50-63 and 92-105, update the
corresponding methods; in `router/pkg/entitycaching/cache/in_memory_test.go`
lines 542-597, add contention tests that hold `c.mu`, cancel the context,
release the mutex, and verify `GetMany` returns no result and `SetMany` performs
no write.
🤖 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/in_memory.go`:
- Around line 57-61: Treat entries as expired when now is equal to or later than
expiresAt by updating the expiry check in GetMany within
router/pkg/entitycaching/cache/in_memory.go:57-61 to use
!now.Before(entry.expiresAt). Update the expiry-boundary test in
router/pkg/entitycaching/cache/in_memory_test.go:412-428 to expect a miss and
eviction when access occurs exactly at expiration.

---

Outside diff comments:
In `@router/pkg/entitycaching/cache/in_memory.go`:
- Around line 50-63: Recheck the context immediately after `GetMany` acquires
`c.mu` with `RLock`, returning no results and the context error before reading
entries; apply the same guard after `SetMany` acquires `c.mu` with `Lock`,
returning before writes. In `router/pkg/entitycaching/cache/in_memory.go` lines
50-63 and 92-105, update the corresponding methods; in
`router/pkg/entitycaching/cache/in_memory_test.go` lines 542-597, add contention
tests that hold `c.mu`, cancel the context, release the mutex, and verify
`GetMany` returns no result and `SetMany` performs no write.
🪄 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: 1d4c2590-eda1-4f6c-bc38-43045b58fa37

📥 Commits

Reviewing files that changed from the base of the PR and between aa38079 and 8af6fd3.

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

Comment thread router/pkg/entitycaching/cache/in_memory.go Outdated
@SkArchon
SkArchon marked this pull request as ready for review August 5, 2026 13:18
@SkArchon
SkArchon requested a review from a team as a code owner August 5, 2026 13:18

@claude claude Bot 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.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@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.39%. Comparing base (64eaf60) to head (d508d07).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3137      +/-   ##
==========================================
+ Coverage   62.37%   62.39%   +0.02%     
==========================================
  Files         262      263       +1     
  Lines       31003    31043      +40     
==========================================
+ Hits        19337    19370      +33     
- Misses      10158    10163       +5     
- Partials     1508     1510       +2     
Files with missing lines Coverage Δ
router/pkg/entitycaching/cache/in_memory.go 100.00% <100.00%> (ø)

... and 3 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.

@SkArchon
SkArchon force-pushed the milinda/eng-9922-in-memory-adapter branch from 3845440 to 689be8a Compare August 5, 2026 19:57
@SkArchon
SkArchon force-pushed the milinda/eng-9922-in-memory-adapter branch from ec32312 to d508d07 Compare August 5, 2026 20:01

@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

🤖 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/in_memory.go`:
- Around line 47-50: The mutex-protected paths in GetMany and SetMany must
re-check cancellation after acquiring c.mu; add a ctx.Err() check immediately
after locking and before the cache sweep or batch write, returning the error
without processing when canceled. Add contention tests covering cancellation
while each method waits for c.mu.
🪄 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: 48b65ce8-bac8-4458-941e-55fdde870cd9

📥 Commits

Reviewing files that changed from the base of the PR and between e6db77d and 689be8a.

📒 Files selected for processing (2)
  • router/pkg/entitycaching/cache/in_memory.go
  • router/pkg/entitycaching/cache/in_memory_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • router/pkg/entitycaching/cache/in_memory_test.go

Comment thread router/pkg/entitycaching/cache/in_memory.go Outdated

@dkorittki dkorittki 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.

I'd change package structure a bit. We know we will have at least two implementations of entitycache.Cache. I'd prefer to have a seperate packages for each

  • router/pkg/entitycaching/in_memory/*.go
  • router/pkg/entitycaching/redis/*.go

Gives us better namespacing and allows for better type names like redisEntity.Cache and inMemoryEntity.Cache by aliasing import names on call sites.

Another question: Have you considered using a ristretto cache as the backing cache technology for this implementation? It comes with all the evict logic we need to deal with TTLs.


// ErrMissingTTL reports items that were rejected because they carried no
// positive TTL. Every cached entry expires, so a TTL is mandatory.
var ErrMissingTTL = errors.New("cache item requires a positive TTL")

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 error isn't specific to the in memory cache, right? I would define it in the corresponding graphql-go-tools package then and mention in the methods godoc when its returned

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

moved to graphqlgotools

// This cache should only be used for tests OR development and never production
type InMemoryCache struct {
mu sync.Mutex
entries map[string]inMemoryEntry

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.

Why not use map[string]enginecache.Item ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

switched to ristretto

Comment on lines +24 to +26
// InMemoryCache sweeps every expired entry on each call, so both methods take
// the mutex exclusively, hence a plain Mutex rather than an RWMutex.
// This cache should only be used for tests OR development and never production

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 rather belongs to the mu field as a godoc instead of InMemoryCache.

Also this could become a major bottleneck. Using a normal mutex instead of an RWMutex forces syncronous reads to the cache for all concurrent requests on the router. You can't use an RWMutex because of sweepExipred on GetMany. Maybe worth checking if you can use Ristretto like mentioned above so you don't need to care about this kind of problem.

I mean yeah it's only meant to be used in tests but if Ristretto can solve it for free and have it production ready for when the situation calls for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

switched to ristretto

if !ok {
continue
}
results[i] = enginecache.Result{Value: bytes.Clone(entry.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.

You're not really making use of Result.Found because its always true for returned items. Do we really need the Found field?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cleaned up

Comment on lines +48 to +50
if err := ctx.Err(); err != nil {
return nil, 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.

I'd put this on the top like in SetMany

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

type InMemoryCache struct {
mu sync.Mutex
entries map[string]inMemoryEntry
now func() time.Time

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 is only for test purposes right? Have you considered using testing/synctest? It lets you mock time inside the runtime, so you don't need to introduce code, which is only there for testing purposes.

https://go.dev/blog/testing-time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cleaned up

@SkArchon
SkArchon force-pushed the milinda/eng-9922-in-memory-adapter branch from 8fa4312 to f80628a Compare August 6, 2026 20:27
)

const entryCost = 1
const maxSize = math.MaxInt32

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'd probably go for something much smaller, like a million or so. This blows OOM way before we reach maxInt32

Comment on lines +81 to +83
// SetMany stores every item, all of which must carry a positive TTL. A single
// item without one fails the whole batch with an ErrMissingTTL and nothing is
// stored. It otherwise only fails on a cancelled context.

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.

Since it's an implementation of the engines interface method I'd simply write

Suggested change
// SetMany stores every item, all of which must carry a positive TTL. A single
// item without one fails the whole batch with an ErrMissingTTL and nothing is
// stored. It otherwise only fails on a cancelled context.
// SetMany implements enginecache.SetMany.

Any behaviour, guard logic and rules should be documented on the interface method godic as a contract for any implementor (imo)

return &InMemoryCache{cache: cache}, nil
}

// GetMany returns the entries it found, keyed by the key they were asked for.

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.

Since it's an implementation of the engines interface method I'd simply write

Suggested change
// GetMany returns the entries it found, keyed by the key they were asked for.
// GetMany implements enginecache.GetMany.

Any behaviour, guard logic and rules should be documented on the interface method godic as a contract for any implementor (imo)


for _, item := range items {
if item.TTL <= 0 {
return fmt.Errorf("%w: key %q", enginecache.ErrMissingTTL, item.Key)

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.

On the lib version currently referred to by the routers go.mod there is no ErrMissingTTL. I think you need to bumb the engine version

Comment on lines +112 to +113
// Close stops the goroutines ristretto runs behind the cache. It is safe to
// call more than once, but not while a GetMany or a SetMany is in flight.

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.

but not while a GetMany or a SetMany is in flight

We can't control this from here right? I checked and Ristretto will actually panic if you close the cache during a concurrent write. Can you secure it with an RWMutex?

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