diff --git a/router/core/executor.go b/router/core/executor.go index 69e4b02cf7..c7d287be86 100644 --- a/router/core/executor.go +++ b/router/core/executor.go @@ -8,10 +8,10 @@ import ( "go.uber.org/zap" - "github.com/wundergraph/cosmo/router/pkg/mondaytweaks" nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/cosmo/router/pkg/grpcconnector" + "github.com/wundergraph/cosmo/router/pkg/mondaytweaks" pubsub_datasource "github.com/wundergraph/cosmo/router/pkg/pubsub/datasource" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" @@ -52,19 +52,6 @@ type Executor struct { TrackUsageInfo bool } -// Close releases schema and planner references held by the executor so a replaced -// graph mux can be garbage-collected after config reload. -func (e *Executor) Close() { - if e == nil { - return - } - e.ClientSchema = nil - e.RouterSchema = nil - e.PlanConfig = plan.Configuration{} - e.RenameTypeNames = nil - e.Resolver = nil -} - type ExecutorBuildOptions struct { EngineConfig *nodev1.EngineConfiguration Subgraphs []*nodev1.Subgraph diff --git a/router/core/executor_test.go b/router/core/executor_test.go deleted file mode 100644 index a157b09a49..0000000000 --- a/router/core/executor_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package core - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" -) - -func TestExecutorCloseReleasesSchemaReferences(t *testing.T) { - t.Parallel() - - executor := &Executor{ - ClientSchema: &ast.Document{}, - RouterSchema: &ast.Document{}, - PlanConfig: plan.Configuration{DataSources: []plan.DataSource{nil}}, - RenameTypeNames: nil, - } - - executor.Close() - - require.Nil(t, executor.ClientSchema) - require.Nil(t, executor.RouterSchema) - require.Empty(t, executor.PlanConfig.DataSources) - require.Nil(t, executor.RenameTypeNames) - require.Nil(t, executor.Resolver) -} - -func TestExecutorCloseNilSafe(t *testing.T) { - t.Parallel() - - var executor *Executor - require.NotPanics(t, func() { - executor.Close() - }) -} diff --git a/router/core/graph_server.go b/router/core/graph_server.go index f83aacc246..475f877f6d 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -701,10 +701,6 @@ type graphMux struct { reused atomic.Bool finalized atomic.Bool - wsHandler *WebsocketHandler - executor *Executor - planCacheOnEvictEnabled atomic.Bool - planCache *ristretto.Cache[uint64, *planWithMetaData] planFallbackCache *slowplancache.Cache[*planWithMetaData] persistedOperationCache *ristretto.Cache[uint64, NormalizationCacheEntry] @@ -755,21 +751,8 @@ func (s *graphMux) buildOperationCaches(srv *graphServer) (computeSha256 bool, e BufferItems: 64, } if srv.cacheWarmup != nil && srv.cacheWarmup.Enabled && srv.cacheWarmup.InMemoryFallback { - if mondaytweaks.SkipPlanCacheOnEvictDuringMuxShutdown { - s.planCacheOnEvictEnabled.Store(true) - planCacheConfig.OnEvict = func(item *ristretto.Item[*planWithMetaData]) { - // This could be called before planFallbackCache is set, but it's not a problem - // because there is a nil guard inside, as well as items should not really be evicted - // on startup - if !s.planCacheOnEvictEnabled.Load() { - return - } - s.planFallbackCache.Set(item.Key, item.Value, item.Value.planningDuration) - } - } else { - planCacheConfig.OnEvict = func(item *ristretto.Item[*planWithMetaData]) { - s.planFallbackCache.Set(item.Key, item.Value, item.Value.planningDuration) - } + planCacheConfig.OnEvict = func(item *ristretto.Item[*planWithMetaData]) { + s.planFallbackCache.Set(item.Key, item.Value, item.Value.planningDuration) } } s.planCache, err = ristretto.NewCache(planCacheConfig) @@ -1020,81 +1003,41 @@ func (s *graphMux) stopPubsubProviders(ctx context.Context) error { }, providerTimeout, "pubsub provider shutdown timed out") } -func closeRistrettoCacheUint64[V any](cache **ristretto.Cache[uint64, V]) { - if *cache != nil { - (*cache).Close() - *cache = nil - } -} - -// releaseOperationCaches drops references to closed Ristretto caches so the old -// graphMux can be collected after shutdown (Close clears entries but retains structs). -func (s *graphMux) releaseOperationCaches() { - closeRistrettoCacheUint64(&s.planCache) - if s.planFallbackCache != nil { - s.planFallbackCache.Close() - s.planFallbackCache = nil - } - closeRistrettoCacheUint64(&s.persistedOperationCache) - closeRistrettoCacheUint64(&s.normalizationCache) - closeRistrettoCacheUint64(&s.variablesNormalizationCache) - closeRistrettoCacheUint64(&s.remapVariablesCache) - closeRistrettoCacheUint64(&s.complexityCalculationCache) - closeRistrettoCacheUint64(&s.validationCache) - closeRistrettoCacheUint64(&s.operationHashCache) -} - -func (s *graphMux) closeOperationCachesLegacy() { - s.planCache.Close() - s.planFallbackCache.Close() - s.persistedOperationCache.Close() - s.normalizationCache.Close() - s.variablesNormalizationCache.Close() - s.remapVariablesCache.Close() - s.complexityCalculationCache.Close() - s.validationCache.Close() - s.operationHashCache.Close() -} - func (s *graphMux) Shutdown(ctx context.Context) error { // Make sure we do not shutdown the mux multiple times if !s.finalized.CompareAndSwap(false, true) { return nil } - if mondaytweaks.DrainWebsocketSubscriptionsBeforeCacheClose { - // Close websocket subscriptions synchronously before tearing down plan caches so - // active preparedPlan and executor references are released first. - if s.wsHandler != nil { - s.wsHandler.ShutdownConnections() - } - } - // cancel the graph muxes context to close its resources like websocket connections, resolvers, etc. s.cancel() - if mondaytweaks.SkipPlanCacheOnEvictDuringMuxShutdown { - // ristretto Close() clears all entries and invokes OnEvict for each one. Disable - // migration into the slow-plan fallback cache during intentional mux shutdown. - s.planCacheOnEvictEnabled.Store(false) - if s.planFallbackCache != nil { - s.planFallbackCache.Wait() - } + if s.planCache != nil { + s.planCache.Close() } - - if mondaytweaks.CloseExecutorOnGraphMuxShutdown { - if s.executor != nil { - s.executor.Close() - s.executor = nil - } + if s.planFallbackCache != nil { + s.planFallbackCache.Close() } - - if mondaytweaks.NilGraphMuxCachesOnShutdown { - s.releaseOperationCaches() - s.wsHandler = nil - s.mux = nil - } else { - s.closeOperationCachesLegacy() + if s.persistedOperationCache != nil { + s.persistedOperationCache.Close() + } + if s.normalizationCache != nil { + s.normalizationCache.Close() + } + if s.variablesNormalizationCache != nil { + s.variablesNormalizationCache.Close() + } + if s.remapVariablesCache != nil { + s.remapVariablesCache.Close() + } + if s.complexityCalculationCache != nil { + s.complexityCalculationCache.Close() + } + if s.validationCache != nil { + s.validationCache.Close() + } + if s.operationHashCache != nil { + s.operationHashCache.Close() } var err error @@ -1605,13 +1548,13 @@ func (s *graphServer) buildGraphMux( ecb := &ExecutorConfigurationBuilder{ introspection: s.introspection, - baseURL: s.baseURL, - baseTripper: s.baseTransport, - subgraphTrippers: subgraphTippers, - pluginHost: s.connector, - logger: s.logger, - trackUsageInfo: s.graphqlMetricsConfig.Enabled || s.metricConfig.Prometheus.PromSchemaFieldUsage.Enabled, - subscriptionClientOptions: subscriptionClientOptions, + baseURL: s.baseURL, + baseTripper: s.baseTransport, + subgraphTrippers: subgraphTippers, + pluginHost: s.connector, + logger: s.logger, + trackUsageInfo: s.graphqlMetricsConfig.Enabled || s.metricConfig.Prometheus.PromSchemaFieldUsage.Enabled, + subscriptionClientOptions: subscriptionClientOptions, transportOptions: &TransportOptions{ SubgraphTransportOptions: s.subgraphTransportOptions, PreHandlers: s.preOriginHandlers, @@ -1648,9 +1591,6 @@ func (s *graphServer) buildGraphMux( if err != nil { return nil, fmt.Errorf("failed to build plan configuration: %w", err) } - if mondaytweaks.CloseExecutorOnGraphMuxShutdown { - gm.executor = executor - } if s.engineStats != nil && executor.Resolver != nil { s.engineStats.RegisterResolver(executor.Resolver) @@ -1994,7 +1934,7 @@ func (s *graphServer) buildGraphMux( }) if s.webSocketConfiguration != nil && s.webSocketConfiguration.Enabled { - wsMiddleware, wsHandler := NewWebsocketMiddleware(graphMuxCtx, WebsocketMiddlewareOptions{ + wsMiddleware, _ := NewWebsocketMiddleware(graphMuxCtx, WebsocketMiddlewareOptions{ OperationProcessor: operationProcessor, OperationBlocker: operationBlocker, Planner: operationPlanner, @@ -2014,9 +1954,6 @@ func (s *graphServer) buildGraphMux( DisableVariablesRemapping: s.engineExecutionConfiguration.DisableVariablesRemapping, ApolloCompatibilityFlags: s.apolloCompatibilityFlags, }) - if mondaytweaks.DrainWebsocketSubscriptionsBeforeCacheClose { - gm.wsHandler = wsHandler - } // When the playground path is equal to the graphql path, we need to handle // ws upgrades and html requests on the same route. @@ -2381,9 +2318,6 @@ func (s *graphServer) Shutdown(ctx context.Context) error { if err := mux.Shutdown(ctx); err != nil { finalErr = errors.Join(finalErr, err) } - if mondaytweaks.NilGraphMuxCachesOnShutdown { - delete(s.graphMuxList, name) - } } // Close idle connections on base and subgraph transports diff --git a/router/core/router.go b/router/core/router.go index 81a297f6e2..f7a0f49bdd 100644 --- a/router/core/router.go +++ b/router/core/router.go @@ -19,7 +19,6 @@ import ( "connectrpc.com/connect" "github.com/mitchellh/mapstructure" "github.com/nats-io/nuid" - "github.com/wundergraph/cosmo/router/pkg/mondaytweaks" "github.com/wundergraph/cosmo/router/pkg/routerconfig" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -28,7 +27,6 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.uber.org/zap" "google.golang.org/grpc" - "google.golang.org/protobuf/proto" "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/graphqlmetrics/v1/graphqlmetricsv1connect" nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" @@ -629,12 +627,6 @@ func (r *Router) serverTLSConfig() (*tls.Config, error) { // newGraphServer creates a new server. func (r *Router) newServer(ctx context.Context, response *routerconfig.Response) error { - // Extract slow-plan cache entries before building the new graph server, which - // overwrites ReloadPersistentState cache references and before the old graphMux shuts down. - if mondaytweaks.CallOnRouterConfigReloadOnHotReload { - r.reloadPersistentState.OnRouterConfigReload() - } - server, err := newGraphServer(ctx, r, response, r.proxy) if err != nil { r.logger.Error("Failed to create graph server. Keeping the old server", zap.Error(err)) @@ -1114,11 +1106,6 @@ func (r *Router) bootstrap(ctx context.Context) error { } r.staticExecutionConfig = executionConfig - - if hash, hashErr := routerconfig.ManifestMapperSHA256(r.manifestConfig.Path); hashErr == nil && mondaytweaks.SkipManifestReloadWhenMapperUnchanged { - r.lastManifestMapperHash = hash - r.manifestMapperHashSeen = true - } } if err := r.buildClients(ctx); err != nil { @@ -1738,20 +1725,6 @@ func (r *Router) buildManifestConfigWatcher(ctx context.Context, ll *zap.Logger) return } - if mondaytweaks.SkipManifestReloadWhenMapperUnchanged { - mapperHash, err := routerconfig.ManifestMapperSHA256(r.manifestConfig.Path) - if err != nil { - ll.Error("Failed to hash manifest mapper", zap.Error(err)) - return - } - - if r.manifestMapperHashSeen && mapperHash == r.lastManifestMapperHash { - ll.Debug("Manifest mapper unchanged, skipping reload", - zap.String("path", r.manifestConfig.Path)) - return - } - } - cfg, err := routerconfig.AssembleStaticExecutionConfigFromManifest( r.manifestConfig.Path, routerconfig.AssembleConfigRules{ SkipMissingFeatureFlags: r.manifestConfig.SkipMissingFeatureFlags, @@ -1769,22 +1742,6 @@ func (r *Router) buildManifestConfigWatcher(ctx context.Context, ll *zap.Logger) ll.Error("Failed to update server with new config", zap.Error(err)) return } - - if mondaytweaks.SkipManifestReloadWhenMapperUnchanged { - mapperHash, err := routerconfig.ManifestMapperSHA256(r.manifestConfig.Path) - if err != nil { - ll.Error("Failed to hash manifest mapper", zap.Error(err)) - return - } - r.lastManifestMapperHash = mapperHash - r.manifestMapperHashSeen = true - } - - if mondaytweaks.ResetExecutionConfigProtoOnReload { - if old := r.staticExecutionConfig; old != nil && old != cfg { - proto.Reset(old) - } - } r.staticExecutionConfig = cfg r.trackExecutionConfigUsage(cfg, true) }, diff --git a/router/core/router_config.go b/router/core/router_config.go index 4931e5346a..239fe721b3 100644 --- a/router/core/router_config.go +++ b/router/core/router_config.go @@ -158,9 +158,6 @@ type Config struct { grpcPluginDialOptions []grpc.DialOption tracingAttributes []config.CustomAttribute subscriptionHooks subscriptionHooks - // lastManifestMapperHash skips manifest reload when mapper.json content is unchanged. - lastManifestMapperHash [32]byte - manifestMapperHashSeen bool } // Usage returns an anonymized version of the config for usage tracking diff --git a/router/core/websocket.go b/router/core/websocket.go index a57a483b40..c7ab8fe7a0 100644 --- a/router/core/websocket.go +++ b/router/core/websocket.go @@ -152,18 +152,6 @@ func NewWebsocketMiddleware(ctx context.Context, opts WebsocketMiddlewareOptions }, handler } -// ShutdownConnections closes all active websocket connections and unsubscribes -// any live GraphQL subscriptions before graph mux caches are torn down. It blocks -// until every sync connection-handler goroutine has returned so executor.Resolver -// is not read concurrently with executor.Close during graphMux shutdown. -func (h *WebsocketHandler) ShutdownConnections() { - if h == nil { - return - } - h.closeAllConnections() - h.closeSyncConnectionsAndWait() -} - // wsConnectionWrapper is a wrapper around websocket.Conn that allows // writing from multiple goroutines type wsConnectionWrapper struct { @@ -265,14 +253,6 @@ type WebsocketHandler struct { connections map[int]*WebSocketConnectionHandler connectionsMu sync.RWMutex - // syncHandlers tracks connections handled by handleConnectionSync goroutines (used - // when netpoll is unavailable). ShutdownConnections closes them and waits on - // syncHandlersWg so every handler goroutine returns before graphMux tears down - // executor.Resolver. - syncHandlers map[*WebSocketConnectionHandler]struct{} - syncHandlersMu sync.Mutex - syncHandlersWg sync.WaitGroup - stats statistics.EngineStatistics readTimeout time.Duration @@ -464,24 +444,7 @@ func (h *WebsocketHandler) handleUpgradeRequest(w http.ResponseWriter, r *http.R } // Handle messages sync when net poller implementation is not available - - h.syncHandlersMu.Lock() - if h.syncHandlers == nil { - h.syncHandlers = make(map[*WebSocketConnectionHandler]struct{}) - } - h.syncHandlers[handler] = struct{}{} - h.syncHandlersMu.Unlock() - - h.syncHandlersWg.Add(1) - go func() { - defer h.syncHandlersWg.Done() - defer func() { - h.syncHandlersMu.Lock() - delete(h.syncHandlers, handler) - h.syncHandlersMu.Unlock() - }() - h.handleConnectionSync(handler) - }() + go h.handleConnectionSync(handler) } func (h *WebsocketHandler) handleConnectionSync(handler *WebSocketConnectionHandler) { @@ -649,21 +612,6 @@ func (h *WebsocketHandler) closeAllConnections() { } } -func (h *WebsocketHandler) closeSyncConnectionsAndWait() { - h.syncHandlersMu.Lock() - handlers := make([]*WebSocketConnectionHandler, 0, len(h.syncHandlers)) - for handler := range h.syncHandlers { - handlers = append(handlers, handler) - } - h.syncHandlersMu.Unlock() - - for _, handler := range handlers { - handler.Close(true, wsproto.CloseKindGoingAway) - } - - h.syncHandlersWg.Wait() -} - type websocketResponseWriter struct { id string protocol wsproto.Proto diff --git a/router/pkg/mondaytweaks/mondaytweaks.go b/router/pkg/mondaytweaks/mondaytweaks.go index ccfa086100..6ca9e3d638 100644 --- a/router/pkg/mondaytweaks/mondaytweaks.go +++ b/router/pkg/mondaytweaks/mondaytweaks.go @@ -1,49 +1,10 @@ // Package mondaytweaks defines compile-time feature flags for monday.com-specific -// behavioural overrides in the cosmo router. All monday-specific toggles live in -// one place so they are easy to audit and remove when upstreamed. +// behavioural overrides in the cosmo router. Keep only non-memory-leak behavior +// and performance toggles here; memory-reload cleanup notes live in +// `wiki/reference/cosmo-router-reload-memory-benchmark-tooling`. package mondaytweaks const ( - // ClearSlowPlanCacheOnClose makes slowplancache.Close() clear all entries from - // the sync.Map immediately, releasing references to cached values (including - // *ast.Document schema pointers). Without this, entries survive until the Cache - // struct itself is GC'd — which may be delayed by goroutines still referencing - // the owning graphMux — causing ~200-300 MB of retained memory per config reload. - ClearSlowPlanCacheOnClose = true - - // OmitSchemaDocumentFromCachedPlans removes the unused schemaDocument field from - // planWithMetaData (compile-time structural change in operation_planner.go). - OmitSchemaDocumentFromCachedPlans = true - - // CallOnRouterConfigReloadOnHotReload invokes ReloadPersistentState.OnRouterConfigReload - // at the start of Router.newServer(), matching the supervisor restart path. - CallOnRouterConfigReloadOnHotReload = true - - // SkipPlanCacheOnEvictDuringMuxShutdown disables ristretto OnEvict migration into - // slowplancache while a graphMux is shutting down intentionally. - SkipPlanCacheOnEvictDuringMuxShutdown = true - - // DrainWebsocketSubscriptionsBeforeCacheClose closes client websocket subscriptions - // synchronously before plan caches are torn down on graphMux shutdown. - DrainWebsocketSubscriptionsBeforeCacheClose = true - - // CloseExecutorOnGraphMuxShutdown nils federation schema refs held by Executor after - // graphMux drain, allowing the old graph generation to be garbage-collected. - CloseExecutorOnGraphMuxShutdown = true - - // NilGraphMuxCachesOnShutdown closes and nils Ristretto caches on shut-down graphMux, - // drops wsHandler/mux references, and removes the mux from graphMuxList. - NilGraphMuxCachesOnShutdown = true - - // ResetExecutionConfigProtoOnReload proto.Resets the previous staticExecutionConfig - // after a successful manifest reload so decoded protojson strings can be collected. - ResetExecutionConfigProtoOnReload = true - - // SkipManifestReloadWhenMapperUnchanged skips manifest watcher reload when mapper.json - // bytes are unchanged. Disabled: latest.json / feature-flag files can change without - // mapper.json changing, which would serve stale config. - SkipManifestReloadWhenMapperUnchanged = false - // ShareUpstreamSubscriptionClient uses one upstream GraphQLSubscriptionClient per // DefaultFactoryResolver instead of one per subgraph factory (behavior-altering). ShareUpstreamSubscriptionClient = true @@ -62,31 +23,10 @@ const ( // PlanCacheSizeAwareBudgetPerSlotBytes is the per-configured-slot byte budget used to // derive the size-aware execution-plan-cache MaxCost when SizeAwarePlanCache is enabled. - // The Ristretto MaxCost becomes ExecutionPlanCacheSize * this value (bytes), and each - // entry is charged its estimated retained heap (see estimatePlanCacheCost). With the - // historical count-based config a single giant aliased-batch plan occupied one of N - // slots regardless of its true size, so a burst of structurally-unique giant plans (US - // cluster group 02) could pin far more heap than the operator budgeted for. 8 KiB/slot - // keeps normal-traffic capacity roughly unchanged (typical plans estimate well under - // this) while charging a 200 KB+ giant plan tens of slots, and — crucially — bounds the - // total plan-cache heap to a predictable ceiling instead of (entry count x worst case). PlanCacheSizeAwareBudgetPerSlotBytes int64 = 8 * 1024 ) var ( - // SizeAwarePlanCache switches the execution-plan Ristretto cache from count-based - // eviction (every entry costs 1, MaxCost = ExecutionPlanCacheSize) to size-aware - // eviction (each entry costs its estimated retained heap, MaxCost = - // ExecutionPlanCacheSize * PlanCacheSizeAwareBudgetPerSlotBytes). This targets the RSS - // gap on US cluster group 02, where structurally-unique aliased-batch mutation plans are - // far larger than typical plans yet, under count-based eviction, could evict thousands of - // small hot plans while collectively pinning most of the heap. - // - // Unlike the behaviour-preserving fixes above, this materially changes cache eviction - // semantics and the plan-cache heap ceiling for every request. It defaults ON so the - // size-aware heap ceiling applies fleet-wide; an individual instance can opt back out to - // the original count-based eviction via EngineExecutionConfiguration.DisableSizeAwarePlanCache - // (used by tests that rely on count-based single-entry eviction). It is a var so tests can - // exercise both cache configurations. When false the original count-based path runs unchanged. + // SizeAwarePlanCache — monday perf tweak (#7 OPEN); not an upstream memory-leak fix. SizeAwarePlanCache = true ) diff --git a/router/pkg/slowplancache/slow_plan_cache.go b/router/pkg/slowplancache/slow_plan_cache.go index e14380309a..17fba9aa5f 100644 --- a/router/pkg/slowplancache/slow_plan_cache.go +++ b/router/pkg/slowplancache/slow_plan_cache.go @@ -6,8 +6,6 @@ import ( "sync" "sync/atomic" "time" - - "github.com/wundergraph/cosmo/router/pkg/mondaytweaks" ) // Entry holds a cached value and the duration it took to produce. @@ -224,13 +222,5 @@ func (c *Cache[V]) Close() { // This downside is also there in ristretto (if set is called concurrently) // it is even documented in the ristretto code as a comment close(c.writeCh) - - if mondaytweaks.ClearSlowPlanCacheOnClose { - c.entries.Range(func(key, _ any) bool { - c.entries.Delete(key) - return true - }) - c.size = 0 - } }) } diff --git a/router/pkg/slowplancache/slow_plan_cache_test.go b/router/pkg/slowplancache/slow_plan_cache_test.go index f8d5e1313c..69734bf771 100644 --- a/router/pkg/slowplancache/slow_plan_cache_test.go +++ b/router/pkg/slowplancache/slow_plan_cache_test.go @@ -411,29 +411,6 @@ func TestCache_DoubleClose(t *testing.T) { }) } -func TestCache_CloseReleasesEntries(t *testing.T) { - t.Parallel() - c, err := New[*testPlan](10, 0) - require.NoError(t, err) - - c.Set(1, &testPlan{content: "q1"}, 10*time.Millisecond) - c.Set(2, &testPlan{content: "q2"}, 20*time.Millisecond) - c.Set(3, &testPlan{content: "q3"}, 30*time.Millisecond) - c.Wait() - - c.Close() - - // Verify the underlying sync.Map is empty — entries must not pin - // referenced objects (e.g. schema AST documents) after Close. - count := 0 - c.entries.Range(func(_, _ any) bool { - count++ - return true - }) - require.Equal(t, 0, count, "entries sync.Map must be empty after Close") - require.Equal(t, int64(0), c.size) -} - func BenchmarkCache_Set(b *testing.B) { c, err := New[*testPlan](1000, 0) require.NoError(b, err)