Skip to content

Commit b0f1376

Browse files
committed
fix(cache-analytics): address review comments and CI build failures
Review comments addressed: - hasEntityAncestor stops at Interface/Union ancestors instead of scanning past, so Entity → Interface → ValueType correctly defers to the polymorphic runtime fallback. - Shadow-mode hashing recurses through ProvidesData per scalar leaf (with accumulated FieldPath) instead of emitting one whole-subobject hash; nested value-type leaves now line up with the fresh-side emission for staleness detection. __typename is skipped. - L2 entity-source records are emitted from the schema-shape cached value before alias denormalization, so aliased keys like userId: id no longer drop the entitySourceRecord. - Empty-slice canonicalization test passes []string{} (verifies the collector normalizes to nil); the redundant scratch append is gone. Other fixes: - HeaderImpactEvent dedup now keys on a Timestamp-less struct; the auto-stamped wall-clock made identical events look distinct under whole-struct equality. - Resolvable suppresses value-type-traversal hashes when the enclosing entity has no key fields selected (KeyRaw == "" / "{}" with zero KeyHash) — only affects value-type leaves; direct entity scalars always hash even with aliased keys. - Normalize helpers in v2 and execution/engine zero auto-stamped Timestamps and (in e2e) the DataSource / FieldPath dimensions on FieldHashes/MutationEvents (covered exhaustively by unit tests in cache_analytics_test.go). New tests: - TestHashShadowCachedLeaves covers nested per-leaf emission, the __typename skip, and array-of-objects path accumulation. - E2E fixtures updated for the new value-type traversal hashes (Product.body via Review.body) and new DataSource attribution.
1 parent a1b3edf commit b0f1376

9 files changed

Lines changed: 315 additions & 55 deletions

execution/engine/federation_caching_analytics_test.go

Lines changed: 56 additions & 19 deletions
Large diffs are not rendered by default.

execution/engine/federation_caching_helpers_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,8 +805,54 @@ func parseCacheAnalytics(t *testing.T, headers http.Header) resolve.CacheAnalyti
805805
}
806806

807807
// normalizeSnapshot makes a CacheAnalyticsSnapshot deterministically comparable by
808-
// sorting EntityTypes, L1Reads, L2Reads, L1Writes, L2Writes, and FieldHashes.
808+
// sorting EntityTypes, L1Reads, L2Reads, L1Writes, L2Writes, and FieldHashes,
809+
// zeroing the auto-stamped Timestamp on every event slice, and stripping the
810+
// DataSource and FieldPath dimensions from FieldHashes / MutationEvents.
811+
//
812+
// DataSource and FieldPath attribution is exhaustively covered by the unit
813+
// tests in v2/pkg/engine/resolve/cache_analytics_test.go. The e2e test
814+
// fixtures in this package focus on request-flow and cache-hit-pattern
815+
// invariants, so the per-fixture DataSource and FieldPath values are
816+
// intentionally not asserted here — keeping fixtures focused on the
817+
// causation-by-comment pattern documented in CLAUDE.md.
809818
func normalizeSnapshot(snap resolve.CacheAnalyticsSnapshot) resolve.CacheAnalyticsSnapshot {
819+
// Zero auto-stamped Timestamps on every event slice. Each Record* method
820+
// stamps time.Now() when the caller passes a zero value; the wall-clock
821+
// nanos make literal struct equality flake without this normalization.
822+
for i := range snap.L1Reads {
823+
snap.L1Reads[i].Timestamp = time.Time{}
824+
}
825+
for i := range snap.L2Reads {
826+
snap.L2Reads[i].Timestamp = time.Time{}
827+
}
828+
for i := range snap.L1Writes {
829+
snap.L1Writes[i].Timestamp = time.Time{}
830+
}
831+
for i := range snap.L2Writes {
832+
snap.L2Writes[i].Timestamp = time.Time{}
833+
}
834+
for i := range snap.ErrorEvents {
835+
snap.ErrorEvents[i].Timestamp = time.Time{}
836+
}
837+
for i := range snap.FieldHashes {
838+
snap.FieldHashes[i].Timestamp = time.Time{}
839+
snap.FieldHashes[i].DataSource = ""
840+
snap.FieldHashes[i].FieldPath = nil
841+
}
842+
for i := range snap.ShadowComparisons {
843+
snap.ShadowComparisons[i].Timestamp = time.Time{}
844+
}
845+
for i := range snap.MutationEvents {
846+
snap.MutationEvents[i].Timestamp = time.Time{}
847+
snap.MutationEvents[i].DataSource = ""
848+
}
849+
for i := range snap.HeaderImpactEvents {
850+
snap.HeaderImpactEvents[i].Timestamp = time.Time{}
851+
}
852+
for i := range snap.CacheOpErrors {
853+
snap.CacheOpErrors[i].Timestamp = time.Time{}
854+
}
855+
810856
// Sort EntityTypes by TypeName
811857
if snap.EntityTypes != nil {
812858
sorted := make([]resolve.EntityTypeInfo, len(snap.EntityTypes))

execution/engine/federation_caching_source_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestCacheWriteEventSource_MutationL2Write(t *testing.T) {
7676
},
7777
},
7878
FieldHashes: []resolve.EntityFieldHash{
79-
{EntityType: "User", FieldName: "username", FieldHash: 4957449860898447395, KeyRaw: `{"id":"1234"}`}, // xxhash("Me")
79+
{EntityType: "User", FieldName: "username", FieldHash: 4957449860898447395, KeyRaw: `{"id":"1234"}`, DataSource: "accounts"}, // xxhash("Me"); accounts owns User
8080
},
8181
EntityTypes: []resolve.EntityTypeInfo{
8282
{TypeName: "User", Count: 1, UniqueKeys: 1}, // Mutation triggered resolution of 1 User entity

v2/pkg/engine/resolve/cache_analytics.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -593,18 +593,35 @@ func deduplicateShadowComparisons(events []ShadowComparisonEvent) []ShadowCompar
593593
}
594594

595595
// deduplicateHeaderImpactEvents removes duplicate header impact events,
596-
// keeping the first occurrence for each unique event identity.
596+
// keeping the first occurrence for each unique event identity. Identity
597+
// excludes Timestamp — auto-stamping populates a different time.Now() per
598+
// Record* call, so two semantically identical events would otherwise look
599+
// distinct under whole-struct equality.
597600
func deduplicateHeaderImpactEvents(events []HeaderImpactEvent) []HeaderImpactEvent {
598601
if len(events) == 0 {
599602
return events
600603
}
601-
seen := make(map[HeaderImpactEvent]struct{}, len(events))
604+
type dedupKey struct {
605+
BaseKey string
606+
HeaderHash uint64
607+
ResponseHash uint64
608+
EntityType string
609+
DataSource string
610+
}
611+
seen := make(map[dedupKey]struct{}, len(events))
602612
out := make([]HeaderImpactEvent, 0, len(events))
603613
for _, ev := range events {
604-
if _, ok := seen[ev]; ok {
614+
k := dedupKey{
615+
BaseKey: ev.BaseKey,
616+
HeaderHash: ev.HeaderHash,
617+
ResponseHash: ev.ResponseHash,
618+
EntityType: ev.EntityType,
619+
DataSource: ev.DataSource,
620+
}
621+
if _, ok := seen[k]; ok {
605622
continue
606623
}
607-
seen[ev] = struct{}{}
624+
seen[k] = struct{}{}
608625
out = append(out, ev)
609626
}
610627
return out

v2/pkg/engine/resolve/cache_analytics_test.go

Lines changed: 113 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,7 +1953,7 @@ func TestCacheAnalyticsCollector_HeaderImpactEvents(t *testing.T) {
19531953
c.RecordHeaderImpactEvent(base)
19541954
c.RecordHeaderImpactEvent(base)
19551955
c.RecordHeaderImpactEvent(base)
1956-
snap := c.Snapshot()
1956+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
19571957
assert.Equal(t, []HeaderImpactEvent{base}, snap.HeaderImpactEvents)
19581958
})
19591959

@@ -1963,7 +1963,7 @@ func TestCacheAnalyticsCollector_HeaderImpactEvents(t *testing.T) {
19631963
other.BaseKey = "key2"
19641964
c.RecordHeaderImpactEvent(base)
19651965
c.RecordHeaderImpactEvent(other)
1966-
snap := c.Snapshot()
1966+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
19671967
assert.Equal(t, []HeaderImpactEvent{base, other}, snap.HeaderImpactEvents)
19681968
})
19691969

@@ -1973,7 +1973,7 @@ func TestCacheAnalyticsCollector_HeaderImpactEvents(t *testing.T) {
19731973
other.HeaderHash = 222
19741974
c.RecordHeaderImpactEvent(base)
19751975
c.RecordHeaderImpactEvent(other)
1976-
snap := c.Snapshot()
1976+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
19771977
assert.Equal(t, []HeaderImpactEvent{base, other}, snap.HeaderImpactEvents)
19781978
})
19791979

@@ -1983,7 +1983,7 @@ func TestCacheAnalyticsCollector_HeaderImpactEvents(t *testing.T) {
19831983
other.ResponseHash = 888
19841984
c.RecordHeaderImpactEvent(base)
19851985
c.RecordHeaderImpactEvent(other)
1986-
snap := c.Snapshot()
1986+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
19871987
assert.Equal(t, []HeaderImpactEvent{base, other}, snap.HeaderImpactEvents)
19881988
})
19891989

@@ -1993,7 +1993,7 @@ func TestCacheAnalyticsCollector_HeaderImpactEvents(t *testing.T) {
19931993
other.EntityType = "Product"
19941994
c.RecordHeaderImpactEvent(base)
19951995
c.RecordHeaderImpactEvent(other)
1996-
snap := c.Snapshot()
1996+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
19971997
assert.Equal(t, []HeaderImpactEvent{base, other}, snap.HeaderImpactEvents)
19981998
})
19991999

@@ -2003,14 +2003,14 @@ func TestCacheAnalyticsCollector_HeaderImpactEvents(t *testing.T) {
20032003
other.DataSource = "reviews"
20042004
c.RecordHeaderImpactEvent(base)
20052005
c.RecordHeaderImpactEvent(other)
2006-
snap := c.Snapshot()
2006+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
20072007
assert.Equal(t, []HeaderImpactEvent{base, other}, snap.HeaderImpactEvents)
20082008
})
20092009

20102010
t.Run("single event is preserved", func(t *testing.T) {
20112011
c := NewCacheAnalyticsCollector()
20122012
c.RecordHeaderImpactEvent(base)
2013-
snap := c.Snapshot()
2013+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
20142014
assert.Equal(t, []HeaderImpactEvent{base}, snap.HeaderImpactEvents)
20152015
})
20162016

@@ -2034,7 +2034,7 @@ func TestCacheAnalyticsCollector_WriteEventSource(t *testing.T) {
20342034
c.RecordWrite(CacheWriteEvent{CacheKey: "key2", EntityType: "Product", ByteSize: 256, DataSource: "products", CacheLevel: CacheLevelL2, TTL: 60 * time.Second, Source: CacheSourceMutation})
20352035
c.RecordWrite(CacheWriteEvent{CacheKey: "key3", EntityType: "Review", ByteSize: 512, DataSource: "reviews", CacheLevel: CacheLevelL2, TTL: 90 * time.Second, Source: CacheSourceSubscription})
20362036

2037-
snap := c.Snapshot()
2037+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
20382038
// Assert entire L2Writes slice — each event preserves its Source from the recording call
20392039
assert.Equal(t, []CacheWriteEvent{
20402040
{CacheKey: "key1", EntityType: "User", ByteSize: 128, DataSource: "accounts", CacheLevel: CacheLevelL2, TTL: 30 * time.Second, Source: CacheSourceQuery}, // Recorded with CacheSourceQuery
@@ -2061,7 +2061,7 @@ func TestCacheAnalyticsCollector_WriteEventSource(t *testing.T) {
20612061
}
20622062
c.RecordMutationEvent(event)
20632063

2064-
snap := c.Snapshot()
2064+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
20652065
// Assert entire MutationEvents slice — Source field preserved through record→snapshot
20662066
assert.Equal(t, []MutationEvent{event}, snap.MutationEvents)
20672067
})
@@ -2073,7 +2073,7 @@ func TestCacheAnalyticsCollector_WriteEventSource(t *testing.T) {
20732073
c.RecordWrite(CacheWriteEvent{CacheKey: "query-key-1", EntityType: "User", ByteSize: 128, DataSource: "accounts", CacheLevel: CacheLevelL2, TTL: 30 * time.Second, Source: CacheSourceQuery}) // Write from query resolution
20742074
c.RecordWrite(CacheWriteEvent{CacheKey: "mutation-key-2", EntityType: "User", ByteSize: 256, DataSource: "accounts", CacheLevel: CacheLevelL2, TTL: 30 * time.Second, Source: CacheSourceMutation}) // Write from mutation resolution
20752075

2076-
snap := c.Snapshot()
2076+
snap := normalizeCacheAnalyticsSnapshot(c.Snapshot())
20772077
// Assert entire L2Writes — different keys prevent deduplication, each retains its Source
20782078
assert.Equal(t, []CacheWriteEvent{
20792079
{CacheKey: "query-key-1", EntityType: "User", ByteSize: 128, DataSource: "accounts", CacheLevel: CacheLevelL2, TTL: 30 * time.Second, Source: CacheSourceQuery}, // Query-triggered write
@@ -2345,3 +2345,106 @@ func TestWalkCachedResponseForSources_DataSource(t *testing.T) {
23452345
assert.Equal(t, "accounts", sources[i].dataSource, "every entity record must carry the dataSource")
23462346
}
23472347
}
2348+
2349+
// TestHashShadowCachedLeaves verifies that compareShadowValues' recursive
2350+
// per-leaf walk emits one EntityFieldHash per scalar leaf with the accumulated
2351+
// FieldPath, mirroring the fresh-resolution emission in renderFieldValue so
2352+
// shadow-mode comparisons line up by (entityType, fieldName, FieldPath).
2353+
//
2354+
// Before this fix, the shadow loop only iterated info.ProvidesData.Fields
2355+
// at the top level and emitted one whole-subobject hash for nested objects,
2356+
// so nested value-type leaves like User.address.street had no shadow-side
2357+
// counterpart for staleness detection.
2358+
func TestHashShadowCachedLeaves(t *testing.T) {
2359+
a := arena.NewMonotonicArena(arena.WithMinBufferSize(1024))
2360+
parser := &astjson.Parser{}
2361+
2362+
t.Run("nested value-type leaves emit per-leaf hashes", func(t *testing.T) {
2363+
// User { address { street, city } } — Address is a value type under User.
2364+
providesData := &Object{
2365+
Fields: []*Field{
2366+
{Name: []byte("address"), Value: &Object{
2367+
Fields: []*Field{
2368+
{Name: []byte("street"), Value: &String{}},
2369+
{Name: []byte("city"), Value: &String{}},
2370+
},
2371+
}},
2372+
},
2373+
}
2374+
cached, err := parser.ParseBytesWithArena(a, []byte(`{"address":{"street":"1 Main","city":"NYC"}}`))
2375+
require.NoError(t, err)
2376+
2377+
ctx := NewContext(context.Background())
2378+
ctx.cacheAnalytics = NewCacheAnalyticsCollector()
2379+
l := &Loader{ctx: ctx}
2380+
l.hashShadowCachedLeaves(cached, providesData, "User", `{"id":"1"}`, "accounts", nil)
2381+
2382+
snap := normalizeCacheAnalyticsSnapshot(ctx.cacheAnalytics.Snapshot())
2383+
require.Equal(t, 2, len(snap.FieldHashes))
2384+
// snapshot is already deduped/ordered by collector internals;
2385+
// sort manually by FieldName for deterministic assertions
2386+
got := make(map[string]EntityFieldHash, len(snap.FieldHashes))
2387+
for _, fh := range snap.FieldHashes {
2388+
got[fh.FieldName] = fh
2389+
}
2390+
street := got["street"]
2391+
assert.Equal(t, "User", street.EntityType)
2392+
assert.Equal(t, []string{"address"}, street.FieldPath)
2393+
assert.Equal(t, FieldSourceShadowCached, street.Source)
2394+
assert.Equal(t, "accounts", street.DataSource)
2395+
city := got["city"]
2396+
assert.Equal(t, "User", city.EntityType)
2397+
assert.Equal(t, []string{"address"}, city.FieldPath)
2398+
})
2399+
2400+
t.Run("__typename is never hashed", func(t *testing.T) {
2401+
// __typename is a meta field; the fresh-side resolvable also skips it.
2402+
providesData := &Object{
2403+
Fields: []*Field{
2404+
{Name: []byte("__typename"), Value: &String{}},
2405+
{Name: []byte("username"), Value: &String{}},
2406+
},
2407+
}
2408+
cached, err := parser.ParseBytesWithArena(a, []byte(`{"__typename":"User","username":"Alice"}`))
2409+
require.NoError(t, err)
2410+
2411+
ctx := NewContext(context.Background())
2412+
ctx.cacheAnalytics = NewCacheAnalyticsCollector()
2413+
l := &Loader{ctx: ctx}
2414+
l.hashShadowCachedLeaves(cached, providesData, "User", `{"id":"1"}`, "accounts", nil)
2415+
2416+
snap := normalizeCacheAnalyticsSnapshot(ctx.cacheAnalytics.Snapshot())
2417+
require.Equal(t, 1, len(snap.FieldHashes))
2418+
assert.Equal(t, "username", snap.FieldHashes[0].FieldName, "only username is hashed; __typename is skipped")
2419+
})
2420+
2421+
t.Run("array of objects walks each item under the same path", func(t *testing.T) {
2422+
// Product { reviews: [Review { body }] } — array of objects.
2423+
// Each review's body should produce one hash with FieldPath ["reviews"].
2424+
providesData := &Object{
2425+
Fields: []*Field{
2426+
{Name: []byte("reviews"), Value: &Array{Item: &Object{
2427+
Fields: []*Field{
2428+
{Name: []byte("body"), Value: &String{}},
2429+
},
2430+
}}},
2431+
},
2432+
}
2433+
cached, err := parser.ParseBytesWithArena(a, []byte(`{"reviews":[{"body":"first"},{"body":"second"}]}`))
2434+
require.NoError(t, err)
2435+
2436+
ctx := NewContext(context.Background())
2437+
ctx.cacheAnalytics = NewCacheAnalyticsCollector()
2438+
l := &Loader{ctx: ctx}
2439+
l.hashShadowCachedLeaves(cached, providesData, "Product", `{"upc":"top-1"}`, "reviews", nil)
2440+
2441+
snap := normalizeCacheAnalyticsSnapshot(ctx.cacheAnalytics.Snapshot())
2442+
require.Equal(t, 2, len(snap.FieldHashes))
2443+
for _, fh := range snap.FieldHashes {
2444+
assert.Equal(t, "Product", fh.EntityType)
2445+
assert.Equal(t, "body", fh.FieldName)
2446+
assert.Equal(t, []string{"reviews"}, fh.FieldPath)
2447+
assert.Equal(t, FieldSourceShadowCached, fh.Source)
2448+
}
2449+
})
2450+
}

v2/pkg/engine/resolve/cache_load_test.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,13 +1318,48 @@ func (f *FakeLoaderCache) SetRawData(key string, value []byte, ttl time.Duration
13181318
// Shadow Mode Integration Tests
13191319
// =============================================================================
13201320

1321-
// normalizeCacheAnalyticsSnapshot zeroes out non-deterministic fields (FetchTimings.DurationMs)
1322-
// and normalizes empty slices to nil for consistent assert.Equal comparison.
1321+
// normalizeCacheAnalyticsSnapshot zeroes out non-deterministic fields
1322+
// (FetchTimings.DurationMs and every event's auto-stamped Timestamp) and
1323+
// normalizes empty slices to nil for consistent assert.Equal comparison.
13231324
// CacheAgeMs is deterministic when tests run inside synctest.Test (fake clock).
13241325
func normalizeCacheAnalyticsSnapshot(snap CacheAnalyticsSnapshot) CacheAnalyticsSnapshot {
13251326
// Zero out non-deterministic FetchTimings (DurationMs varies between runs)
13261327
snap.FetchTimings = nil
13271328

1329+
// Zero auto-stamped Timestamps on every event slice. Each Record* method
1330+
// stamps time.Now() when the caller passes a zero value; the wall-clock
1331+
// nanos make literal struct equality flake without this normalization.
1332+
for i := range snap.L1Reads {
1333+
snap.L1Reads[i].Timestamp = time.Time{}
1334+
}
1335+
for i := range snap.L2Reads {
1336+
snap.L2Reads[i].Timestamp = time.Time{}
1337+
}
1338+
for i := range snap.L1Writes {
1339+
snap.L1Writes[i].Timestamp = time.Time{}
1340+
}
1341+
for i := range snap.L2Writes {
1342+
snap.L2Writes[i].Timestamp = time.Time{}
1343+
}
1344+
for i := range snap.ErrorEvents {
1345+
snap.ErrorEvents[i].Timestamp = time.Time{}
1346+
}
1347+
for i := range snap.FieldHashes {
1348+
snap.FieldHashes[i].Timestamp = time.Time{}
1349+
}
1350+
for i := range snap.ShadowComparisons {
1351+
snap.ShadowComparisons[i].Timestamp = time.Time{}
1352+
}
1353+
for i := range snap.MutationEvents {
1354+
snap.MutationEvents[i].Timestamp = time.Time{}
1355+
}
1356+
for i := range snap.HeaderImpactEvents {
1357+
snap.HeaderImpactEvents[i].Timestamp = time.Time{}
1358+
}
1359+
for i := range snap.CacheOpErrors {
1360+
snap.CacheOpErrors[i].Timestamp = time.Time{}
1361+
}
1362+
13281363
// Normalize empty slices to nil
13291364
if len(snap.L1Reads) == 0 {
13301365
snap.L1Reads = nil
@@ -1534,8 +1569,8 @@ func TestShadowMode_L2_AlwaysFetches(t *testing.T) {
15341569
{CacheKey: shadowTestKeyProduct, EntityType: "Product", IsFresh: true, CachedHash: 16331343294028781429, FreshHash: 16331343294028781429, CachedBytes: 36, FreshBytes: 36, DataSource: "products", ConfiguredTTL: 30 * time.Second, CacheAgeMs: 5000}, // Cached data matches subgraph (same hash), no staleness; entry was 5s old
15351570
},
15361571
FieldHashes: []EntityFieldHash{
1537-
{EntityType: "Product", FieldName: "id", FieldHash: 4016270444951293489, KeyRaw: `{"id":"prod-1"}`, Source: FieldSourceShadowCached}, // Cached "id" field from shadow comparison
1538-
{EntityType: "Product", FieldName: "name", FieldHash: 8385814294091472045, KeyRaw: `{"id":"prod-1"}`, Source: FieldSourceShadowCached}, // Cached "name" field from shadow comparison
1572+
{EntityType: "Product", FieldName: "id", FieldHash: 4016270444951293489, KeyRaw: `{"id":"prod-1"}`, Source: FieldSourceShadowCached, DataSource: "products"}, // Cached "id" field from shadow comparison
1573+
{EntityType: "Product", FieldName: "name", FieldHash: 8385814294091472045, KeyRaw: `{"id":"prod-1"}`, Source: FieldSourceShadowCached, DataSource: "products"}, // Cached "name" field from shadow comparison
15391574
},
15401575
}), normalizeCacheAnalyticsSnapshot(ctx2.GetCacheStats()))
15411576
})
@@ -1719,8 +1754,8 @@ func TestShadowMode_StalenessDetection(t *testing.T) {
17191754
{CacheKey: shadowTestKeyUser, EntityType: "User", IsFresh: false, CachedHash: 272931794584083561, FreshHash: 4550742678894771079, CachedBytes: 30, FreshBytes: 37, DataSource: "accounts", ConfiguredTTL: 30 * time.Second, CacheAgeMs: 5000}, // Cached "Alice" differs from fresh "AliceUpdated" (different hashes); entry was 5s old
17201755
},
17211756
FieldHashes: []EntityFieldHash{
1722-
{EntityType: "User", FieldName: "id", FieldHash: 13311642224980425257, KeyRaw: `{"id":"u1"}`, Source: FieldSourceShadowCached}, // Cached "id" field from "Alice" entity
1723-
{EntityType: "User", FieldName: "username", FieldHash: 5631231822564450273, KeyRaw: `{"id":"u1"}`, Source: FieldSourceShadowCached}, // Cached "username"="Alice" (stale value)
1757+
{EntityType: "User", FieldName: "id", FieldHash: 13311642224980425257, KeyRaw: `{"id":"u1"}`, Source: FieldSourceShadowCached, DataSource: "accounts"}, // Cached "id" field from "Alice" entity
1758+
{EntityType: "User", FieldName: "username", FieldHash: 5631231822564450273, KeyRaw: `{"id":"u1"}`, Source: FieldSourceShadowCached, DataSource: "accounts"}, // Cached "username"="Alice" (stale value)
17241759
},
17251760
}), normalizeCacheAnalyticsSnapshot(ctx2.GetCacheStats()))
17261761
})

v2/pkg/engine/resolve/extensions_cache_invalidation_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func TestExtensionsCacheInvalidationAnalytics(t *testing.T) {
216216
env.ctx.ExecutionOptions.Caching.EnableCacheAnalytics = true
217217

218218
env.run()
219-
stats := env.ctx.GetCacheStats()
219+
stats := normalizeCacheAnalyticsSnapshot(env.ctx.GetCacheStats())
220220

221221
assert.Equal(t, []MutationEvent{
222222
{

v2/pkg/engine/resolve/loader_cache.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2594,6 +2594,11 @@ func (l *Loader) hashShadowCachedLeaves(
25942594
}
25952595
for _, field := range providesData.Fields {
25962596
fieldName := string(field.Name)
2597+
// Skip __typename — meta field, not part of the entity's data identity
2598+
// (the fresh-side resolvable also doesn't hash it).
2599+
if fieldName == "__typename" {
2600+
continue
2601+
}
25972602
fieldVal := cachedValue.Get(fieldName)
25982603
if fieldVal == nil {
25992604
continue

0 commit comments

Comments
 (0)