Repository navigation
Release gate: confirm #1872's Azure replica attribution through the SHIPPED collector, plus the three axes the validation did not vary #1887
Description
Spun off from #1872 so the caveats recorded there are not buried when it closes.
#1872 flipped Query Store replica attribution ON for Azure SQL Database on live evidence from two tiers. The evidence is strong — but it was gathered by composing the collector's SQL and running it by hand, not by running the shipped collector. Two gaps follow from that, plus three configuration axes the validation deliberately did not vary.
Gap 1: the shipped code path has not hit a live Azure database
The Hyperscale run took BuildPayloadBody from origin/dev, composed it with hasReplicaAttribution = true, diffed the text against the already-verified 55-column Azure file to confirm only the three intended differences, and executed that. That is a very close proxy — but it is not Lite or the Darling service connecting to Azure, running the collector, and storing the result.
What the by-hand run therefore cannot speak for:
- the value surviving the reader at ordinal 53 and the positional bulk write into
query_store_statson both stores; - the grids actually rendering
Primarywhere they used to render blank; - the read-side dedup and rollup behaving on real Azure rows (analysis says this is a no-op — every key already partitions on
replica_role, so an Azure server moves from one NULL group to onePrimarygroup — but "analysis says no-op" is exactly the class of claim a smoke test exists to check).
Do this as part of the next release's Azure smoke, alongside whatever else that pass covers, rather than provisioning a database for it alone.
Gap 2 through 4: the axes #1872 did not vary
Carried forward verbatim from that issue's caveats, so they survive it closing:
- Hyperscale with 0 HA replicas. Provisioned with
--ha-replicas 0for minimum footprint. Binding is a schema property and the view returned its 4 role rows anyway, so this is very unlikely to matter — but a Hyperscale database that actually HAS HA or named replicas was not tested, and it is the one configuration axis that was not varied. - Serverless Hyperscale only. Provisioned Hyperscale (
HS_Gen5_2) untested. Same engine, byte-identical version banner, so no difference is expected. readonly_reason & 8(readable secondary) unexercised on either tier. Needs geo-replication or read scale-out to reach. Unchanged since Release gate: live Azure SQL DB smoke for the #1833 + #1836 collector fixes (includes two unverified edges) #1848 — this one is about the eligibility gate rather than the attribution, and it predates Query Store replica attribution on Azure SQL DB: General Purpose probe now passes; needs one Hyperscale probe to lift the gate #1872.
Items 1 and 2 would both be answered incidentally by any future Azure work that happens to run on a differently-shaped Hyperscale database; neither justifies a spin-up on its own.
What "done" looks like
One live Azure SQL Database collection cycle through the real app — GP is fine, Hyperscale better — showing replica_role = 'Primary' stored and displayed, plus a note on whichever of the three axes above the target happened to cover. If the attribution turns out NOT to bind on some configuration, the fix is to revert the gate in QueryStoreCollector.BuildPayloadBody to productVersion >= 16 && !context.Target.IsAzureSqlDb and record the finding in the comment — the test pin at Lite.Tests/QueryStoreCollectorDefinitionTests.cs (BuildQuery_Azure_AttributesReplicaRole_ProvenLiveOnBothServiceTiers) is written to be re-flipped, and says so.
Related: #1848 (the GP validation), #1872 (the Hyperscale validation and the flip).
🤖 Generated with Claude Code
Activity
erikdarlingdata commentedon Jul 31, 2026
Gap 1 closed: the shipped collector has now hit a live Azure database
One live Azure SQL Database, collected by the real Darling service (not composed SQL), stored into a
real PostgreSQL + TimescaleDB store, verified in the store. Zero product changes.
Target:
pm1887e2esrv.database.windows.net/pm1887db, East US 2, GP_S_Gen5 (General Purpose,Serverless, Gen5, 1 vCore max / 0.5 min, 32 GB, auto-pause 60 min, LRS backup, non zone-redundant).
Engine
Microsoft SQL Azure (RTM) - 12.0.2000.8, EngineEdition 5.Code under test:
origin/dev@4adf3236, built Release, 0 warnings / 0 errors.origin/devadvancedto
71438f3dduring the run; the diff is CI workflows,Darling.Tests, CHANGELOG and README only — noproduct code — so this run still speaks for current dev.
Store: throwaway PostgreSQL 18.4 + TimescaleDB (the repo's
pg-runtime.zip) on 127.0.0.1:5911,bring-your-own
connectionString, fresh database. Migrated to schema v41 on first start.Seed: 3 stored procedures + 1 table (20,000 rows); a 100-execution burst then one execution of each
procedure every 20s so
procedure_stats' 10-minute recency filter always had fresh rows.1. The service connected and ran the collectors
Per-cycle collector lines (3
query_storecycles at the shipped 5-minute cadence, 15procedure_statsat 1 minute):
The only
warn/error/failline in the entire service log was the expected plaintext-password notice(
--encrypt-passwordnot used for a scratch run). PostgreSQL's own log had zeroERROR/FATAL/PANIC.2.
collection_log— both collectors SUCCESS, nothing else anything but SUCCESSEvery non-SUCCESS row, whole store, all collectors:
3. THE #1872 FLIP THROUGH THE SHIPPED PATH —
replica_role = 'Primary', stored16 of 16 rows attributed, zero NULL placeholders. This is the value surviving
QueryStoreCollector'sreader and the positional bulk write into
collect.query_store_stats— the step #1848/#1872 couldnot speak for.
4.
plan_typeand the #1841 interval identity — populated5. No positional binding / order mismatch
ReadRowsAsyncreadscompatibility_levelat 49,query_plan_textat 50,replica_roleat 52,runtime_stats_interval_idat 53,interval_start_time_utcat 54. Checking all five together is astronger test than checking the flipped column alone — a shifted ordinal would corrupt its neighbours:
Cross-contamination guard — every count is the number of values landing in the wrong domain:
(The rows showing a NULL
query_plan_textare by design — the collector'sROW_NUMBER() ... = 1guard stores the plan XML once perplan_idper cycle.)6.
procedure_statsfor the seeded proceduresAll three seeded procedures collected on every cycle, with execution counts tracking the workload
(
+3per minute, one execution of each per 20s round).7. The read-side dedup and rollup — checked, not assumed
The issue flagged "analysis says no-op" as exactly the class of claim a smoke test exists to check. It
holds, and here is the actual shape rather than the assertion:
query_store_stats_hourly(and the corrected/daily tiers above it) do not group onreplica_roleatall, so the flip cannot change their grouping.
query_store_stats_interval_hourly— the dedup CAGG — does. After a refresh it carries the flippedvalue through, one group:
ViewerDataService.QueryStoreTopSql, load-bearing core run verbatim) rendersPrimarywhere it used to render blank:The attribution binds correctly on live Azure through the shipped path. No reason to revert the gate.
One defect found, unrelated to the flip: #1907
The
total_executionscolumn above is wrong on two of those five rows (8 where the truth is 94). That isnot the replica flip — it is
sys.query_store_runtime_statsreturning the flushed and still-in-memoryslices of one
runtime_stats_idas two rows that share the entire read-side dedup key includingcollection_time, soROW_NUMBER/last()keeps an arbitrary one. Confirmed additive three ways, includingthe product's own
procedure_statsindependently reporting exactly the slice sums (144/94/69).Filed as #1907 with full evidence.
sys.query_store_replicashas one row perreplica_group_id, sothe flip's
LEFT JOINcannot fan out and the collision is identical withreplica_roleNULL. Only asustained end-to-end run surfaces it, which is precisely the gap this issue existed to close.
What this run retires, and what it does not
Retired — Gap 1, in full:
query_store_stats— verified, plusits four neighbouring ordinals;
Primarywhere it used to render blank — verified through the viewer's own read SQLagainst the real store.
One caveat on the third: I ran
ViewerDataService.QueryStoreTopSql's load-bearing core against the storerather than launching the WPF viewer, so this is the read path proven, not pixels on screen.
Still open — Gaps 2, 3, 4, unchanged. This run varied none of them:
readonly_reason & 8(readable secondary) — needs geo-replication or read scale-out; this databasehad
readonly_reason = 0throughout. Not covered.Leaving this issue open for those three.
Cost and teardown
Database created
2026-07-31T04:16:49Z, resource group deleted~04:41Z— roughly 25 minutes ofdatabase lifetime. GP_S_Gen5_1 serverless at 1 vCore max bills about $0.12/vCore-hour, so compute is
well under $0.10; storage and LRS backup for under half an hour are negligible. Order-of-magnitude
estimate, not a metered figure.
Resources present immediately before deletion:
The local scratch rig was torn down too: service stopped, PostgreSQL stopped, cluster and extracted
runtime deleted, throwaway worktree removed. The generated SQL admin password lived only in the scratch
directory and was never committed.
Note for whoever reads the subscription: resource groups
pm1887hsandpm1886miwere present atteardown time and are not from this run (created 04:32Z, mid-run) — they look like concurrent
Hyperscale/MI validations. I deliberately left them alone.
🤖 Generated with Claude Code
erikdarlingdata commentedon Jul 31, 2026
Gaps 2, 3 and 4 retired: provisioned Hyperscale WITH an HA replica, and
readonly_reason & 8exercised on a live readable secondaryOne provisioned (non-serverless) Hyperscale database with an HA replica covers all three unvaried axes at
once, because the HA replica is the readable secondary. Zero product changes; no repo modifications;
no PR.
Target:
pm1887hs.database.windows.net/ databasepmhsdb, East US 2, HS_Gen5_2(Hyperscale, Provisioned, Gen5, 2 vCore, 1 HA replica, read scale-out Enabled, LRS backup,
non-zone-redundant). Firewall was a single rule for this box's IP only.
Byte-identical version banner to the GP database (#1848) and the serverless Hyperscale database (#1872).
Seed: the same shape as both prior runs — 3 stored procedures + 1 table (20,000 rows), 100 executions,
then
sys.sp_query_store_flush_db:0. How the SQL was obtained — no hand transcription this time
#1872's decisive test composed
BuildPayloadBodyby hand and diffed the text to prove it matched. This runremoves that step: a throwaway console harness (outside the repo, in a detached worktree at
origin/dev@
110dfca8) calls the shipped publicQueryStoreCollector.BuildQuery()withTarget.IsAzureSqlDb = trueand writes its.Textstraight to a.sqlfile. What ran on Azure is themethod's own output, character for character, with one line prepended —
DECLARE @cutoff_time datetime2(7),because the host binds that as a command parameter and sqlcmd needs it declared in-batch.
origin/devadvanced tob7d936a1during the run.PerformanceMonitor.Collectors/QueryStoreCollector.csis byte-identical between the two (
git diff 110dfca8 origin/dev -- ...is empty) and the only.cschanges at all are in
Darling.Tests, so this run still speaks for current dev.Three shapes were generated and all three came back with attribution ON:
The third is the probe-failure case (
EnumerationProbeResultnull, soDefaultProductVersion13), whichconfirms in generated text what
BuildQuery_Azure_AttributesReplicaRole_EvenWhenTheVersionProbeFailspins:the Azure arm of the gate is an OR, so attribution does not depend on the version probe succeeding.
1. Axis "provisioned (non-serverless) Hyperscale" — this is that database
{ "currentServiceObjective": "HS_Gen5_2", "requestedServiceObjective": "HS_Gen5_2", "sku": { "name": "HS_Gen5", "tier": "Hyperscale", "family": "Gen5", "capacity": 2 }, "haReplicas": 1, "readScale": "Enabled", "minCapacity": null, "autoPauseDelay": null, "zoneRedundant": false, "backupRedundancy": "Local", "status": "Online", "kind": "v12.0,user,vcore,hyperscale" }HS_Gen5_2, not #1872'sHS_S_Gen5_2— the missingSis the whole axis.minCapacityandautoPauseDelayare both null, which is what provisioned looks like: there is no auto-pause to fail totake (the thing #1872 had to warn about), it simply bills continuously by design.
2. Axis "Hyperscale WITH HA replicas" — the replica is real and live
sys.dm_database_replica_stateson the primary returns two rows, not one:3. The #1836 flip criterion, re-run on the HA-bearing database
Identical to GP (#1848) and serverless Hyperscale (#1872). Same object id, same column length.
4. The decisive question — does the view hold live per-replica rows now?
No. With a live HA replica attached,
sys.query_store_replicasholds exactly the same four static rolerows, and nothing else.
That settles #1872's caveat 1 in the direction it guessed: the view is a static enumeration of replica
roles, not instances, so replica count does not change it. Real runtime-stats rows still all carry
group 1:
5. The full shipped payload on the PRIMARY — 55/55 bind, both host shapes
Darling shape (
CapturePlanXml = true):The last five reader ordinals, from the returned header —
replica_rolelands where the reader expects it(53rd of 55 one-based, the reader's 0-based 52):
Sample rows (display width truncated so plan XML does not wrap; the SQL is unchanged):
Lite shape (
CapturePlanXml = false) — the shape #1872 did not run:…with the
nvarchar(1)placeholder where the plan text would be, and the attribution still populated:6. Axis "
readonly_reason & 8" — exercised at last, on a real readable secondaryConnected with
ApplicationIntent=ReadOnly(sqlcmd -K ReadOnly), which read scale-out routes to the HAreplica.
Updateabilityproves which replica the session landed on.On the secondary:
Bit 8 is set, and it is the only term declining the database. Note
actual_state = 4(
READ_CAPTURE_SECONDARY) — which is in the gate's admitted set(1, 2, 4), soactual_state_in_1_2_4 = 1. The state clause passes. Onlyreadonly_reason & 8 = 0fails. The comment inQueryStoreCollectorreasons about(state 4, reason 0)— a 2025 secondary capturing real local workload,which still collects. This run adds the live
(state 4, reason 8)case, and the gate handles it correctly.The shipped query text itself, run on that same secondary connection:
Zero bytes. Not an error, not an empty grid — the gate's
RETURNfired and the batch produced no resultset at all, which is exactly what the reader is designed to see as zero rows.
The same predicate on the primary, for contrast:
SKIP on the secondary, PASS on the primary, from the same file over the same credentials — the only
difference is the application intent.
7. Counterfactual: the bit-8 term is load-bearing here, not decorative
Since
actual_state = 4passes the state clause on its own, it is worth showing what dropping the bit-8term would actually cost. Run on the secondary (a probe, not a proposal):
Same five plan ids, the primary's execution counts, all attributed
Primary. That is #1558's rationalereproduced live on Azure: without the bit-8 term the secondary hands back duplicate, lagged primary data
under a second server identity.
8. New finding: on Hyperscale read scale-out, secondary workload never reaches the Query Store
Worth recording because it bounds what the flipped column can ever say on Azure. I ran a marked workload on
the readable secondary, then flushed and inspected the primary's Query Store:
The secondary's work is not captured anywhere: not locally (the replica cannot persist, and
sp_query_store_flush_dbon it produced nothing), and not streamed into the primary. So on Azure SQLDatabase Hyperscale,
replica_rolewill always readPrimary— the docs' "Query Store for secondaryreplicas is unavailable on Hyperscale" is confirmed from the data side.
That is not a defect in the flip and does not argue for reverting it. It means the recovered column is
honest but constant on Azure: it says "the server attributed this row to the primary", which is true, and
it replaces a bare
NULLthat said nothing. Nobody should expectSecondaryrows from an Azure SQL DBtarget.
What this run retires
HS_Gen5_2; identical banner and identical resultsreadonly_reason & 8unexercisedreadonly_reason = 8; gate SKIPs there and PASSes on the primary, proven with the shipped query textNothing regressed and nothing argues for the revert path in the issue body:
hasReplicaAttributionstaysas #1884 shipped it, and the test pin
BuildQuery_Azure_AttributesReplicaRole_ProvenLiveOnBothServiceTiersstays as written. The only claim Iwould now add to it is the one in section 8 — Azure will only ever return
Primary.Not in this run's scope: the shipped-collector end-to-end smoke (Gap 1), which is the companion
General Purpose run posted above.
Cost and teardown
Resource group created
2026-07-31T04:31:12Z; database created04:39:57Z, resource group deletionreturned
04:47:59Z— about 8 minutes of database lifetime, about 17 minutes of resource-grouplifetime. Provisioned Hyperscale has no auto-pause, so it billed continuously for all 8 minutes, and the HA
replica bills as its own compute.
East US 2 retail rate, pulled live from the Azure retail prices API this time rather than estimated:
SQL Database SingleDB/Elastic Pool Hyperscale - Compute Gen5= $0.18266 per vCore-hour. Two vCores forthe primary plus two for the HA replica is ~$0.731/hour, so ~8 minutes of compute is about $0.10;
Hyperscale storage and LRS backup for eight minutes are rounding error. That is computed from the published
rate, not read off an invoice, but it is a metered rate rather than a guess.
Resources present immediately before deletion:
Compute,NetworkWatcherRGandNetworkingare pre-existing and untouched.pm1886miis not fromthis run — it belongs to the concurrent #1886 Managed Instance validation, and I left it alone. The
groups from both prior runs were re-verified gone (
az group exists→falseforpm-1872-hyperscaleand
pm1872hs). The generated SQL admin password lived only in the scratch directory, was deleted atteardown, and was never committed; the throwaway worktree and harness were removed.
🤖 Generated with Claude Code
erikdarlingdata commentedon Jul 31, 2026
Closing: all four items covered by two live Azure runs, both on 2026-07-31
pm1887e2esrv/pm1887db, East US 2replica_role = 'Primary', 16 of 16 rows, 0 NULLs, into PostgreSQL + TimescaleDB. Reader ordinals 49-54 all verified together, so a shifted ordinal could not hide.collection_logall SUCCESS, zero non-SUCCESS rows store-wide.pm1887hs/pmhsdb, East US 2sys.dm_database_replica_statesreturns primary SYNCHRONIZED + secondary SYNCHRONIZING). Probe, view contents and the 55-column payload are all identical to the 0-replica run.HS_Gen5_2,minCapacityandautoPauseDelayboth null. Same version banner, same results as serverless.readonly_reason & 8on a readable secondaryApplicationIntent=ReadOnlyactual_state = 4 (READ_CAPTURE_SECONDARY),readonly_reason = 8. The shipped query text returns zero bytes there (the gate'sRETURNfires) and PASSes on the primary.Nothing failed, and nothing pointed at the revert path in the issue body.
hasReplicaAttributionstays as#1884 shipped it and the
BuildQuery_Azure_AttributesReplicaRole_ProvenLiveOnBothServiceTierspin stays aswritten.
Three things worth carrying forward, none of them blocking:
replica_rolewill always readPrimaryon Azure SQL Database. A marked workload run on theHyperscale readable secondary reached neither its own Query Store nor the primary's (0 marker queries,
still only
replica_group_id = 1). The column is honest but constant there — it now says "the serverattributed this to the primary" instead of a bare NULL, and no blending occurs to disambiguate.
sys.query_store_replicasis a static enumeration of four replica roles, not instances. Adding alive HA replica did not change its contents. That is why the 0-HA-replica evidence in Query Store replica attribution on Azure SQL DB: General Purpose probe now passes; needs one Hyperscale probe to lift the gate #1872 held.
ViewerDataService.QueryStoreTopSql's load-bearing core against the real store rather than launchingthe WPF viewer. The read path is verified; the visual is not, and it rides along with any normal
viewer pass.
Both resource groups were torn down and verified gone (
az group exists->false), with nopm1887resource left anywhere in the subscription. Combined cost of the two runs is roughly $0.20, dominated by
~8 minutes of provisioned Hyperscale compute at the live retail rate of $0.18266/vCore-hour x 2 vCores x 2
(primary + HA replica).
Closing. Related: #1848 (GP probe), #1872 (Hyperscale probe and the evidence for the flip), #1884 (the
flip), #1886 (the MI basis, validated separately).
🤖 Generated with Claude Code
Merge pull request #1910 from erikdarlingdata/fix/1887-azure-pin-comment
Record that Azure attributes a constant replica_role (erikdarlingdata…