mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-12 05:32:36 +00:00
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:
Vendored
+7
-1
@@ -158,7 +158,12 @@ func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
||||
entry.MemorySize = int64(len(key) + len(entry.Value) + approxEntryOverhead)
|
||||
}
|
||||
|
||||
// Check if this is a new entry or an update
|
||||
// Check if this is a new entry or an update. The exists-check, counter
|
||||
// update and store must be atomic together: without the lock, two
|
||||
// concurrent Sets of the same brand-new key both observe it as absent and
|
||||
// both increment entryCount/memoryUsage, leaving the counters inflated
|
||||
// for a single stored entry.
|
||||
c.Lock()
|
||||
oldEntry, exists := c.entries.Load(key)
|
||||
if exists {
|
||||
// Update memory usage: subtract old entry size, add new entry size
|
||||
@@ -172,6 +177,7 @@ func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
||||
// Add new entry's memory size to total
|
||||
atomic.AddInt64(&c.memoryUsage, entry.MemorySize)
|
||||
c.entries.Store(key, entry)
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
|
||||
+28
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user