Repository navigation
Recommendations skip databases that are a secondary copy on this node - #5564
Merged
Merged
Conversation
…: shared rule, fact table, Lite engine (#5558)
…xt resolves the set (#5558)
…Lite tab, Darling viewer and web, both get_analysis_findings tools, Darling twins of the seeded tests (#5558)
…is node: Lite, Darling viewer, web and MCP, with the skipped-databases note (#5558)
…lved inside the pass try, per-site context scan, notes on analyze_server and compare_analysis, one secondary_replica_note field, drill-down filter before LIMIT, freshness lower bound (#5558)
…d yields to an abandoned budget (#5558)
…, always present and null when nothing was skipped (#5558)
…abase names match exactly, scan bounded to its member, both drill-down variants pinned (#5558)
erikdarlingdata
marked this pull request as ready for review
October 8, 2026 15:44
erikdarlingdata
added a commit
that referenced
this pull request
Oct 8, 2026
…#5564) Recommendations skip databases that are a secondary copy on this node. On an Availability Group, the Recommendations tab reported the same per-database finding once per node, secondary copies included. A database's settings and Query Store content replicate from the primary, so every node said the same thing about it. Lite and Darling now skip those findings for a database this node holds only as a secondary copy. These are database settings, percent file growth, Query Store plan regressions and table growth. On the FinOps tab they are idle databases, dev and test names, TDE blockers and compression candidates. The node that holds the primary copy reports them. Findings about work on the node, such as queries, missing indexes, blocking and I/O, stay on every node. The rule reads the replica and database snapshots the monitor already stores. It adds no query on the monitored server, no collector change and no schema change. A database is skipped only when three things hold. Both snapshots exist and are within an hour of the window end. The database has a local row. This node's role for that database's group is SECONDARY. Missing, stale, NULL, RESOLVING, standalone and Azure SQL Database cases skip nothing. A point-in-time pass and each compare window use the role at that time. Database names match exactly, since both sides come from the same server catalog. What it shows. The tab says how many databases it skipped. The analyze_server, compare_analysis and get_analysis_facts tools carry one secondary_replica_note field, built from the same set the pass used. The regressed-queries drill-downs filter in SQL before their row limit. Finding alerts follow the same rule. Fixes #5558
erikdarlingdata
added a commit
that referenced
this pull request
Oct 8, 2026
Adds the CHANGELOG entries for the PRs merged after the second splice. They cover the Viewer query tab loads (#5556), role provisioning under a lock (#5566) and the Query Store interval purge (#5572). They also cover Recommendations skipping secondary copies (#5564) and the release-walk fixes (#5554). The release-walk fixes get seven new entries, one per topic, written from the squash body. Each full entry goes into docs/changelog/3.10.md and its title-only line into CHANGELOG.md, with the archive census updated. Changelog entries for the 3.10.0 gate PRs merged after the second splice.
4 of 5 tasks
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.
Why
On an Availability Group, the Recommendations tab reports the same per-database finding once per node, secondary copies included. A database's settings and Query Store content replicate from the primary, so every node says the same thing about it. This change makes a node skip those findings for databases it holds only as a secondary copy. The primary reports them once. Findings that come from work done on the node itself (queries, blocking, waits, I/O, disks) stay on every node, and so do server-level findings.
How a secondary is decided
The rule uses data the monitor already stores (
ag_replica_statesandag_database_replica_states). It adds no query on the monitored server, no collector change and no schema change. The engine's own definitions are in these pages:The stored snapshots are used instead of a live
sys.fn_hadr_is_primary_replicacall for three reasons. There is no new query. An AsOf pass and eachcompare_analysiswindow use the role at that time rather than now. And it fails open on Azure SQL Database, which has no such data.A database is skipped only when all of these hold. Both snapshots exist and are within an hour of the window end. The database has a local row. The local replica's role for that database's group is explicitly SECONDARY. The role is read per group, so two groups with different primaries are handled. Missing, stale, NULL, RESOLVING, standalone and Azure SQL Database cases skip nothing.
What changes (done in this PR)
AgReplicaScopeinPerformanceMonitor.Common, next toAgAlertPolicy. It is pure, case-insensitive and fail-open.FactReplicaScopeinPerformanceMonitor.Analysisis the one table that says which fact keys are replicated and which are node-local. Replicated keys are filtered on a secondary:DB_CONFIGand its sub-keys,FILE_AUTOGROWTH_PERCENT,PLAN_REGRESSIONandANOMALY_OBJECT_GROWTH. Everything else is kept, and an unknown key is kept.AnalysisContext.SecondaryReplicaDatabasesis resolved at the pass door.AnalysisService. Each compare window resolves its own set. The predicate is applied in the DB config, autogrowth and plan regression facts and drill-downs, and in the object growth anomaly.PgSecondaryReplicaScope(resolver and predicate) and the same predicates in the Pg fact, drill-down and anomaly twins. The audit-config and same-statement-pileup contexts set an explicit empty set because they read only node-local facts. Lite's same-statement-pileup context now does the same throughSecondaryReplicaScope.NoneSkipped.Display note on the findings surfaces
When this node holds a secondary copy of any database, the same one-sentence note (
AgReplicaScope.SkippedNote) appears on every engine Recommendations surface.secondary_replica_notefield, shown with the list and with the empty list.get_analysis_findingsin both Lite and Darling: asecondary_replica_notefield on the payload (null when nothing is skipped). The empty answer carries the note in its message. Anas_ofread names the role at that time.Finding alerts
Checked: no alert path re-reads facts. Both alert senders build their alerts from stored findings. The only code that calls a fact collector is the analysis service, which already runs behind the filter. So the alerts follow the filtered facts with no further change.
Test plan
AgReplicaScopeTests,FactReplicaScopeCoverageTestsandSecondaryReplicaFactTests(seeded DuckDB store), plus the newget_analysis_findingsnote test inMcpAnalysisFindingsCommandTests. With the Census, Ratchet and Recommendations classes: 142 total, 0 failed.SecondaryReplicaScopeLiveTestsis the seeded twin of Lite's fact tests. It covers the DB config fact and drill-down counts, the autogrowth fact and drill-down, plan regression and object growth. It also covers the primary, no-rows and stale-rows cases, the AsOf role and theget_analysis_findingsnote.PgSecondaryReplicaScopeTestscoversApply(marker removed, array parameter numbering, WHERE keyword, node-local key, no set) and the fail-openReadAsyncandEnsureAsync.AnalysisContextSecondaryScopeSourceScanTestsfails when anew AnalysisContextin Lite or Darling product code does not resolve the set. The web page has a Node harness scenario inAnalysisFindingsTabTests. Those classes: 30 total, 0 failed, 0 skipped.dotnet buildof Lite, Lite.Tests, Darling and Darling.Tests: succeeded, 0 warnings.Stage=Guardclass (CI decides).Left undone
For the coordinator to double-check
AgReplicaScope.SnapshotFreshness). A custom AG collector cadence slower than that fails open.LIMIT 5, when the fact did not run (round 1).Lite.Tests/LatestValueLookbackTests.csonly had two seed helpers madepublicso the new tests can reuse them.FinOps Recommendations
The FinOps Recommendations sub-tab now leaves out databases this server holds only as a secondary copy in an availability group. The primary reports them.
What is filtered, and what is kept
Instance-level role check
GetAgReplicaRoleAsyncin Lite,server_properties.ag_replica_rolein Darling). An edition is one decision for the whole group, so it is not a per-database finding.Where the note shows
AgReplicaScope.SkippedNote. Lite shows it on the Recommendations sub-tab, the Darling viewer on its FinOps Recommendations sub-tab, and the web on the same page.get_finops_recommendationspayload carries it assecondary_databases_note. The key is written only when the node holds a secondary copy, so every other server's payload is unchanged.Tests
FinOpsSecondaryReplicaTests(7 pass). A secondary copy drops from the idle list. A primary, stale data and no data change nothing. The low IO list is unchanged. The shared filter helper is covered.FinOpsRecommendationsSecondaryReplicaLiveTests(5 pass on a scratch PostgreSQL). The cases are dropped from each filtered list, kept on the primary, kept on stale data, low IO unchanged, and all-TDE-secondary. They read through the storage reader, viewer and MCP tool. A new envelope test pins the payload with and without the note.GetRecommendationsWithNoteAsync, andAgReplicaScope.IsFreshjoins the known truncated ranges.Review round 1
v_ag_replica_statesandv_ag_database_replica_states. These cover the hot tables and the Parquet archive, like every other Lite analysis read. An AsOf pass or compare window older than 7 days now resolves the role, and so does a store right after the 512 MB reset. A new test puts the AG rows only in Parquet and asserts the set. It fails with the bare tables.AnalyzeAsyncresolves the set inside its try, afterIsAnalyzing = true. A cancelled or timed-out pass takes the existing abandon path, and the guard window is no wider. Darling's wrapper reads the set in its context initializer. There, an abandoned budget now yields an empty set instead of escapingAnalyzeAsync(DarlingAnalysisBudgetTestscaught this). A new test checks that a cancelled pass returns an empty list without throwing.new AnalysisContexton its own member, so a second context cannot borrow another'sEnsureAsync. Lite's audit_config context (CollectConfigAuditFactsAsync) now setsNoneSkipped, like Darling's. Red plant: removing that one assignment made the scan fail on exactly that site. I restored it.analyze_serverandcompare_analysis, on Lite and Darling, carrysecondary_replica_note.analyze_servercarries the sentence for the set the pass used, from a newLastSecondaryReplicaNoteon both analysis services. It is in the findings, empty and unavailable payloads and in the empty message.compare_analysiscarries one note per window:baseline.secondary_replica_noteandcomparison.secondary_replica_note. In the both-windows-empty answer they arebaseline_secondary_replica_noteandcomparison_secondary_replica_noteunder hints. Each comes from the role at that window's end. Both sides have payload tests.LIMIT 5, matching Lite. The C# filter is gone. In the live test the secondary database holds the five worst regressions and the primary's two still come back. It fails without the filter.secondary_replica_note, on every MCP tool, always present and null when nothing is skipped (the convention oftruncation_note,persistence_noteand the FinOpscost_reason). The FinOps tool'ssecondary_databases_noteis renamed and now always written. Its tool text, web page and tests follow.GetRecommendationsAsyncis kept. The review said it had no callers, butLite.Testscalls it inFinOpsTests,ArchivedHistoryReadersTestsandTestDataSeeder.AgReplicaScoperemarks now say a distributed AG forwarder skips nothing, by design. Its database is the local primary of its own group, so it is kept (fail open).Readstraits: three in Darling.Tests (Reads=Lite) and three in Lite.Tests (Reads=Darling). I added them.get_analysis_facts(Lite and Darling) now carriessecondary_replica_notetoo, in the same shape as the other tools. It is read the waycompare_analysisandget_analysis_findingsread it, a separate read as of the window end, so two concurrent reads cannot swap notes. The field is in the facts payload, in the "facts but the window was never observed" answer and in the "no facts" answer, and the "no facts" message also names the note. That last answer used to be a bare status and message when no collection caveat applied. It now always carries a hints block with the field. New tests: two on each side (secondary node in all three envelopes, primary node null). Run, all passing: the new Darling class on a PostgreSQL rig (11). Guard stage (441 Lite, 1409 Darling). The Census, Ratchet, McpPayload, DocCommentHygiene and analysis classes (135 Lite, 600 Darling). The existing get_analysis_facts live classes (9).Round 1 tests (Lite.Tests and Darling.Tests built with 0 warnings): Guard stage 441 Lite and 1409 Darling, all passing. Census, Ratchet, McpPayload and DocCommentHygiene classes: 576 Darling and 60 Lite, all passing. Source-scan classes that read touched files: 2021 Darling (72 skipped, live) and 1106 Lite, all passing. Live Darling classes against a local PostgreSQL 55520 rig (SecondaryReplica*, AnalysisAsOfAnchor, FinOpsRecommendationsReadsLive, NewestSnapshotReadsLive): 35 passing. I did not run the full suite. CI decides.
Review round 2
All four Low findings are fixed in 5e86494.
ComparePeriodsAsyncreturns both windows' secondary sets after the coverages (Lite/Analysis/AnalysisService.cs:671,Darling/PerformanceMonitor.Darling.Analysis/DarlingAnalysisService.cs:914), andcompare_analysisbuilds both notes from them (Lite/Mcp/McpAnalysisTools.cs:458,Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpTools.cs:492). The second role read, and Darling's read with no logger, are gone.get_analysis_factsfilters at read time, so it had the same gap. Its set now rides on the returnedCollectionCaveatState.SecondaryReplicaDatabases(PerformanceMonitor.Analysis/AnalysisContext.cs:397) and both tools build the note from it (McpAnalysisTools.cs:313,DarlingMcpTools.cs:347).get_analysis_findingsstays as it was: it reads findings the pass already stored, so there is no filter at read time to match. New tests:CompareAndFactsReads_CarryTheSetTheirContextFilteredWith_SoTheNoteCannotDisagreeon Lite (DuckDB) and Darling (live PostgreSQL).database_nameissys.databases.name(PerformanceMonitor.Collectors/AgDatabaseReplicaStatesCollector.cs:133,d.name), and the replicated facts' names ared.nameorDB_NAME()(DatabaseConfigCollector.cs:82,QueryStoreCollector.cs:180,IndexObjectStatsCollector.cs:344,FileIoStatsCollector.cs:80). Changed: the set builder andIsSkipped(PerformanceMonitor.Common/AgReplicaScope.cs), the DuckDB filter (PerformanceMonitor.Analysis/FactReplicaScope.cs:114,124) and the PostgreSQL filter (PgSecondaryReplicaScope.cs:147), which no longer wrap the column inlower(). The FinOps Recommendations path usesIsSkipped, so it matches exactly too. Two tests that pinned case-insensitive matching were updated on purpose (FinOpsSecondaryReplicaTests,FinOpsRecommendationsSecondaryReplicaLiveTests). The one limit (a case-only rename between a stored row and the snapshot shows a finding, never hides one) is in theAgReplicaScoperemarks. New tests: a secondarySalesDbnext to a separatesalesdbon the pure rule, on the PostgreSQL predicate, and on a seeded DuckDB store.Darling/Darling.Tests/SecondaryReplicaScopeTests.cs, thelimitclamp before the};search). Red-planted once: a temporary file withFoo(new AnalysisContext { ServerId = 1 });in one method and a context that sets the property in the next. The old scan passed it, and the new scan failed it at the first method. The plant is removed.LIMIT 5(EachRegressedQueriesVariant_CarriesTheMarkerExactlyOnce_BeforeItsLimit, a two-case theory). I took the brief's second option rather than seeding the interval table for a live theory.Round 2 tests (built with 0 warnings on both projects): Guard stage 441 Lite and 1409 Darling, all passing. McpPayload, Census, Ratchet, source-scan, analysis and FinOps classes on both sides pass. Two live classes ran on a throwaway PostgreSQL on port 55531 and passed. The rig is stopped. No full suite was run.
Fixes #5558