Skip to content

test(resolve): add race detector test for parallel entity fetches#1436

Merged
jensneuse merged 3 commits intofeat/add-caching-supportfrom
jensneuse/fix-arena-data-race
Mar 7, 2026
Merged

test(resolve): add race detector test for parallel entity fetches#1436
jensneuse merged 3 commits intofeat/add-caching-supportfrom
jensneuse/fix-arena-data-race

Conversation

@jensneuse
Copy link
Copy Markdown
Member

@jensneuse jensneuse commented Mar 7, 2026

Summary

Add TestResolveParallel_NoConcurrentArenaRace to verify that parallel entity fetches with L2 caching do not race on arena memory. This test exercises goroutine code paths in resolveParallel Phase 2 that allocate from per-goroutine arenas, catching regressions if someone accidentally uses the shared l.jsonArena from a goroutine.

Why

The data race described in the arena-data-race bug report was already mitigated by per-goroutine arenas (commit 1ad5a75). This test ensures no regression.

Test Plan

  • Run with -race flag: go test -race -run TestResolveParallel_NoConcurrentArenaRace ./v2/pkg/engine/resolve/... -v
  • All resolve tests pass with -race: go test -race ./v2/pkg/engine/resolve/... -count=1

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests
    • Added robust concurrency tests that validate parallel data fetches with layered caching (L1/L2) to prevent race conditions across cache-hit and cache-miss scenarios.
    • Introduced a deterministic, thread-safe test data provider and repeated iterations to amplify concurrency exposure and ensure consistent results under concurrent loads.

…h L2 caching

Add TestResolveParallel_NoConcurrentArenaRace to verify that parallel entity
fetches with L2 caching do not race on arena memory. This test exercises goroutine
code paths in resolveParallel Phase 2 that allocate from per-goroutine arenas,
catching regressions if someone accidentally uses the shared l.jsonArena from
a goroutine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f5797cbe-41ad-4e8a-b3ca-3e784e16a2c5

📥 Commits

Reviewing files that changed from the base of the PR and between 49f1313 and b1ab091.

📒 Files selected for processing (1)
  • v2/pkg/engine/resolve/loader_parallel_race_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • v2/pkg/engine/resolve/loader_parallel_race_test.go

📝 Walkthrough

Walkthrough

Adds a new Go test that stresses concurrent loader Phase 2 operations (extractCacheKeysStrings, populateFromCache, denormalizeFromCache) with shared L1/L2 caches and a reused arena. It runs two scenarios (L2 miss and partial L2 hit) across repeated iterations and provides a thread-safe staticDataSource for deterministic loads.

Changes

Cohort / File(s) Summary
Parallel Race Tests
v2/pkg/engine/resolve/loader_parallel_race_test.go
New ~364-line test file that: defines staticDataSource (thread-safe Load/LoadWithFiles), constructs GraphQL responses and Resolvable contexts, configures a Loader with shared L1/L2 caches and a reused arena, and runs 100-iteration L2-miss and partial-L2-hit scenarios to reproduce parallel arena/cache races across BatchEntityFetch paths.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: adding a race detector test for parallel entity fetches with L2 caching, which aligns with the changeset that adds a new test file (loader_parallel_race_test.go) validating concurrent behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jensneuse/fix-arena-data-race

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (1)
v2/pkg/engine/resolve/loader_parallel_race_test.go (1)

191-211: Consider clarifying the test scenario description.

The comment states "pre-populated L2 cache" but only product entity data is pre-populated (lines 197-198). The inventory fetch will miss the cache (no matching inventory data cached) and will fetch from inventoryDS.

This tests a hybrid scenario (products cache hit + inventory cache miss), which is valid for race detection, but the description could be clearer to avoid confusion.

📝 Suggested comment clarification
 	t.Run("parallel batch entity fetches with L2 cache hit", func(t *testing.T) {
-		// Scenario: Same as above but with pre-populated L2 cache.
-		// Goroutines exercise populateFromCache (parsing cached JSON on goroutine arena).
+		// Scenario: Root fetch → Parallel(
+		//     BatchEntityFetch (products subgraph, L2 hit → populateFromCache),
+		//     BatchEntityFetch (inventory subgraph, L2 miss → subgraph fetch),
+		// )
+		// Products fetch exercises populateFromCache (parsing cached JSON on goroutine arena).
+		// Inventory fetch exercises concurrent subgraph fetch alongside cache path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v2/pkg/engine/resolve/loader_parallel_race_test.go` around lines 191 - 211,
Update the test description and inline comment to reflect that only product
entities are pre-populated in the L2 cache while inventory entities will miss
and be fetched from inventoryDS; change the t.Run name string (currently
"parallel batch entity fetches with L2 cache hit") to something like "parallel
batch entity fetches with partial L2 cache hit (products hit, inventory miss)"
and update the preceding comment above the cache.SetRawData calls to explain
this hybrid scenario; reference the test block containing cache.SetRawData,
productCacheKeyTemplate, productsDS and inventoryDS to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@v2/pkg/engine/resolve/loader_parallel_race_test.go`:
- Around line 3-15: The import block in loader_parallel_race_test.go is
mis-ordered for the gci linter; reorder imports into three groups separated by
blank lines: first standard library packages (context, net/http, sync, testing),
then external packages (github.com/stretchr/testify/assert,
github.com/stretchr/testify/require, github.com/wundergraph/go-arena), and
finally internal project packages
(github.com/wundergraph/graphql-go-tools/v2/pkg/ast,
github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient,
github.com/wundergraph/graphql-go-tools/v2/pkg/fastjsonext); adjust the import
grouping in the file so gci passes.

---

Nitpick comments:
In `@v2/pkg/engine/resolve/loader_parallel_race_test.go`:
- Around line 191-211: Update the test description and inline comment to reflect
that only product entities are pre-populated in the L2 cache while inventory
entities will miss and be fetched from inventoryDS; change the t.Run name string
(currently "parallel batch entity fetches with L2 cache hit") to something like
"parallel batch entity fetches with partial L2 cache hit (products hit,
inventory miss)" and update the preceding comment above the cache.SetRawData
calls to explain this hybrid scenario; reference the test block containing
cache.SetRawData, productCacheKeyTemplate, productsDS and inventoryDS to locate
the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a91109a3-35a4-44ed-ae63-7d505d8c4485

📥 Commits

Reviewing files that changed from the base of the PR and between 1ad5a75 and 17b9661.

📒 Files selected for processing (1)
  • v2/pkg/engine/resolve/loader_parallel_race_test.go

Comment thread v2/pkg/engine/resolve/loader_parallel_race_test.go
jensneuse and others added 2 commits March 7, 2026 17:17
- Fix gci import grouping: separate external and internal packages
- Clarify L2 cache hit test: only products are pre-populated, inventory misses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Separate testify (default) from go-arena (wundergraph prefix) per .golangci.yml
gci config: standard / default / prefix(github.com/wundergraph) / prefix(github.com/wundergraph/graphql-go-tools).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@v2/pkg/engine/resolve/loader_parallel_race_test.go`:
- Around line 200-203: The test currently reuses a single FakeLoaderCache across
all loop iterations and sets product entries once, causing later iterations to
see stale hits; change the subtest to allocate fresh caches per iteration (e.g.,
call NewFakeLoaderCache() inside the loop) and preload only the product cache
entries into a product-specific cache instance via SetRawData, while creating a
separate inventory-specific cache instance (or namespace) for the inventory
loader so inventory misses remain isolated; update the inventory loader setup so
it uses the inventory cache instance instead of the shared `default` cache.
- Around line 183-185: The test currently only asserts
`"id":"prod-1"`/`"id":"prod-2"` which can be satisfied by the root fetch; update
the assertions in loader_parallel_race_test.go (the block using
fastjsonext.PrintGraphQLResponse(resolvable.data, resolvable.errors) and
variable resolvable) to also assert fields that only the parallel entity fetches
produce (e.g. assert the output contains `"name":"..."` and
`"inStock":true/false` for both prod-1 and prod-2), and make the same assertion
upgrade for the other occurrence around the 343-345 region so the test proves
the Phase 2 goroutine branches actually ran.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8ac400b5-9ad9-40c2-bf60-a0ec76c88451

📥 Commits

Reviewing files that changed from the base of the PR and between 17b9661 and 49f1313.

📒 Files selected for processing (1)
  • v2/pkg/engine/resolve/loader_parallel_race_test.go

Comment on lines +183 to +185
out := fastjsonext.PrintGraphQLResponse(resolvable.data, resolvable.errors)
assert.Contains(t, out, `"id":"prod-1"`)
assert.Contains(t, out, `"id":"prod-2"`)
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.

⚠️ Potential issue | 🟠 Major

Assert fields that only the parallel entity fetches can produce.

These checks only look for id, but id is already present in the root fetch payload. The test will still pass if the Phase 2 goroutine paths are skipped or misconfigured, which weakens it as a race regression test. Please assert name and inStock too, or otherwise prove those parallel branches ran.

🔧 Minimal assertion upgrade
- assert.Contains(t, out, `"id":"prod-1"`)
- assert.Contains(t, out, `"id":"prod-2"`)
+ assert.Contains(t, out, `"name":"Widget"`)
+ assert.Contains(t, out, `"name":"Gadget"`)
+ assert.Contains(t, out, `"inStock":true`)
+ assert.Contains(t, out, `"inStock":false`)

Also applies to: 343-345

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v2/pkg/engine/resolve/loader_parallel_race_test.go` around lines 183 - 185,
The test currently only asserts `"id":"prod-1"`/`"id":"prod-2"` which can be
satisfied by the root fetch; update the assertions in
loader_parallel_race_test.go (the block using
fastjsonext.PrintGraphQLResponse(resolvable.data, resolvable.errors) and
variable resolvable) to also assert fields that only the parallel entity fetches
produce (e.g. assert the output contains `"name":"..."` and
`"inStock":true/false` for both prod-1 and prod-2), and make the same assertion
upgrade for the other occurrence around the 343-345 region so the test proves
the Phase 2 goroutine branches actually ran.

Comment on lines +200 to +203
cache := NewFakeLoaderCache()
// Pre-populate L2 cache with product entities only; inventory entities are NOT cached
cache.SetRawData(`{"__typename":"Product","key":{"id":"prod-1"}}`, []byte(`{"__typename":"Product","id":"prod-1","name":"Widget"}`), 60_000_000_000)
cache.SetRawData(`{"__typename":"Product","key":{"id":"prod-2"}}`, []byte(`{"__typename":"Product","id":"prod-2","name":"Gadget"}`), 60_000_000_000)
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.

⚠️ Potential issue | 🟠 Major

Keep the partial-hit scenario isolated per iteration.

This subtest does not reliably stay in the advertised “products hit / inventory miss” state. The cache is preloaded once outside the loop and then reused for all 100 runs, and the inventory fetch also points at default. That means the inventory branch can observe the product cache entries immediately, and after the first iteration the remaining runs will trend toward hit+hit instead of hit+miss. Use a fresh product/inventory cache setup per iteration and give inventory its own cache namespace/object.

Also applies to: 217-217, 295-300, 327-327

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v2/pkg/engine/resolve/loader_parallel_race_test.go` around lines 200 - 203,
The test currently reuses a single FakeLoaderCache across all loop iterations
and sets product entries once, causing later iterations to see stale hits;
change the subtest to allocate fresh caches per iteration (e.g., call
NewFakeLoaderCache() inside the loop) and preload only the product cache entries
into a product-specific cache instance via SetRawData, while creating a separate
inventory-specific cache instance (or namespace) for the inventory loader so
inventory misses remain isolated; update the inventory loader setup so it uses
the inventory cache instance instead of the shared `default` cache.

@jensneuse jensneuse merged commit f6cafa7 into feat/add-caching-support Mar 7, 2026
11 checks passed
@jensneuse jensneuse deleted the jensneuse/fix-arena-data-race branch March 7, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant