Repository navigation
Analysis passes share each 30-day baseline once per analysis hour instead of recomputing it every pass (#3941) - #4011
Merged
Conversation
…tead of recomputing it every pass (#3941) Every scheduled pass built a fresh DarlingAnalysisService, whose providers' bucket cache died with the pass, so every pass recomputed every 30-day baseline and the MCP and web hosts paid for them again. Measured on a 31-day rig: 12 baseline reads were 1,017 of a PostgreSQL-target pass's 1,252 ms, 9 were 159 of a SQL Server pass's 209 ms. BaselineCache is a process-wide second tier the worker's per-pass services, the MCP host and the web host share (a DI singleton; the three read as different roles, so they share computed buckets, not providers). Only successful computes are shared; the provider type keys the engine; entries die after CacheTtl and are swept. The compute's window now ends on the analysis hour the cache has always keyed on, so every caller in the hour asks for the same rows and a shared entry is the fresh answer. Lite twinned: one tier per DuckDB store (BaselineCache.For), handed to the scheduler's, MCP host's, Recommendations tab's and overview lanes' providers, with the same window snap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…ny more (#3941) The shared tier sweeps its dead entries, but each provider's own cache never did. A per-pass provider dies before that matters; the MCP and web hosts' singletons kept every keyed series they were ever asked for (the keys follow each server's top statements), and KeyedEntryCount counted those dead series toward the cardinality note's bar, so a bounded consumer would have tripped it after enough days. The provider now runs the tier's sweep over its own cache, through the same gate and removal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
This was referenced Sep 23, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…ntries in their sections (#4080) Adds 42 entries and 42 link refs (#3992, #3995, #3996, #3998, #4001, #4002, #4003, #4007, #4010, #4011, #4013, #4015, #4020, #4022, #4025, #4029, #4030, #4031, #4036, #4038, #4039, #4040, #4044, #4047, #4048, #4049, #4050, #4051, #4055, #4061, #4063, #4064, #4065, #4066, #4067, #4068, #4069, #4070, #4071, #4073, #4074, #4078). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran. #3989 left 26 entries under bare 'Changed' and 'Fixed' lines above '### Added'. They move into '### Changed' and '### Fixed', below the new entries, and one blank line stays under [Unreleased]. Claude-Session: https://claude.ai/code/session_01Ua31ugERL5DmhFVRtf6keQ Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3941.
Why
Every scheduled analysis pass was cold. The worker builds a fresh
DarlingAnalysisServiceper pass, and the service built its ownPgBaselineProvider/PgTargetBaselineProvider, so the providers' 1-hour bucket cache never outlived a pass. Every pass recomputed every 30-day baseline, and the MCP host's singleton recomputed them again foranalyze_serverandcompare_analysis.Measured on a rig first (PostgreSQL 18.6 + TimescaleDB 2.30.1; a store built the way the service builds it, with 71 hypertables and 23 continuous aggregates; 6 PostgreSQL targets and 6 SQL Server servers, each with 31 days of one-minute and five-minute collections, about 2.9M rows). The field has 43+ servers. A pass's cost is per server, so the share of the pass holds at that size, and store load scales with servers x passes. Averages over 6 servers, warm buffers:
The seeded SQL Server servers only had wait and CPU history, so the other SQL Server arms read empty aggregates. On real data the SQL Server share will be higher. The issue's diagnosis holds. The keyed statement arms were only part of the cost: every unkeyed arm ran again on every pass, plus a clock read and supply probes per compute (the 36 and 20 statements that drop out above).
What changes
A process-wide second tier,
BaselineCache. The three hosts read the store as three roles (worker = owner, MCP =mcp, web =viewer, #3914). Sharing providers would have run the MCP and web reads as the worker's role, so they share the computed buckets instead. The tier is a DI singleton. The worker hands it to every per-pass service, the MCP host to its singleton, and the web host toMapAll's service. Each provider keeps its own data source and its own cache as the first tier, and consults the shared tier only on a miss.When a shared entry answers:
CacheTtl(1 h) after its compute, which bounds late rows, purges and a target changing zone. Dead entries are swept at most every quarter-TTL, so the tier holds about one TTL of computes. Measured on the rig: 150 entries for 12 servers took 1.74 MB, about 145 KB per server. Across an hour boundary the old hour lives until the sweep, so the peak is at most double that.remove_serverkeeps the history, and the hash id is stable.Effect. The scheduled passes' baseline reads halve, from servers x 2 per hour to servers x 1. At the default 30-minute interval, each clock hour's two passes share one analysis hour.
analyze_serverandcompare_analysisrun inside an hour a pass already computed read no baseline. A persisted table was not needed, so there is no migration.A long-lived provider's own cache is swept too. A per-pass provider dies before its cache matters. The MCP and web hosts' singletons, though, kept every keyed series they were ever asked for, because the keys follow each server's top statements.
KeyedEntryCountcounted those dead series toward the cardinality note's 500 bar, so a consumer that did bound itself would still have tripped the warning after enough days. The provider now runs the tier's sweep, through the same gate and removal, over its own cache. Lite's provider has no keyed series, so its cache is already bounded by servers x metrics.Lite parity. Lite had the same shape: a fresh
AnalysisServiceper scheduled pass and per Recommendations-tab run, an MCP singleton, and per-tabLocalDataServiceproviders. It gets the same tier, one per DuckDB store (BaselineCache.For(duckDb), never a process static), handed to all four production sites, with the same window snap.Test plan
pg_tps: 245 samples vs 290).DarlingAnalysisServices in one analysis hour. The first reads ≥ 10 baselines, the second 0, and a private one reads them all again. The anomaly detector's facts from the warm shared provider equal those from a cold private one. Without the snap they differ (baseline_samples=50vs58).InvalidateCacheclears the shared tier.KeyedEntryCountonce they are a TTL old, and stay while they are live.PgTargetClockLiveTestsand bothLocalClockBucketKeylive tests (Darling and Lite): lookups moved one week later to the same local hour-of-week, so the planted rows are wholly inside the window. The DST smear is still demonstrated (UTC twin: median 10, mean 32.5).Store(...)andwindowEnd:PgBaselineProviderKeyedTests,PgTargetClockTests.FleetSweepWebFeedTests:MapAllnow takes_baselineCache.Darling.Tests(withDARLING_TEST_PG): 12,968 total, 0 failed, 29 skipped (environment-gated), after mergingorigin/devand after the sweep commit.Lite.Tests: 5,167 total, 0 failed.🤖 Generated with Claude Code
https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv