fix: recover introspection block across named fragment spreads; dedup cache hash; fix cache Set counter race

- graphql.go: parseGraphQLQuery now indexes top-level FragmentDefinition nodes
  and checkSelections resolves FragmentSpread by name, closing an
  introspection-blocking bypass (`{ ...F }` where F spreads __schema was
  allowed because the spread carried no selection set and definitions loop
  skipped fragments).
- proxy.go/server.go: handleCaching stores the computed cache hash on the
  request context; performProxyRequest reuses it instead of recomputing a
  full MD5 of the request body per proxied request (fallback preserved for
  direct callers).
- cache/memory: Set serializes the exists-check + counter update + store
  with the embedded mutex so concurrent Sets of the same new key can't
  double-increment entryCount/memoryUsage; regression test added.

Verification: go build, go vet, gofmt clean; full go test ./... and
go test -race ./... green (9 packages); golangci-lint 0 issues; trufflehog clean.
This commit is contained in:
2026-08-13 09:07:52 +01:00
parent 14be2398e8
commit 56eb0af213
6 changed files with 117 additions and 14 deletions
+28
View File
@@ -118,3 +118,31 @@ func TestMemoryCacheConcurrentGetOnExpiredEntry_CountersConsistent(t *testing.T)
t.Fatalf("memoryUsage = %d after concurrent expired gets, want 0", got)
}
}
func TestMemoryCacheConcurrentSetSameNewKey_CounterNotDoubled(t *testing.T) {
// Regression: two (or more) concurrent Sets of the same brand-new key must
// not both take the "new entry" branch. Previously the exists-check and
// counter increment were separate atomic ops, so concurrent Sets of a new
// key each incremented entryCount, inflating it for a single stored entry.
cache := NewWithSize(DefaultTestExpiration, DefaultMaxMemorySize, DefaultMaxCacheSize)
const workers = 32
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
cache.Set("same-key", []byte("value"), DefaultTestExpiration)
}()
}
wg.Wait()
if got := cache.CountQueries(); got != 1 {
t.Fatalf("entryCount = %d after %d concurrent Sets of one new key, want 1", got, workers)
}
// Sanity: the entry is actually stored and retrievable.
if _, ok := cache.Get("same-key"); !ok {
t.Fatal("key should be retrievable after Set")
}
}