Repository navigation
Scope an Azure SQL DB sweep to the database it registered (#2220) - #2265
Conversation
Reported symptom: one real deadlock in one Azure SQL Database produced
near-identical "Deadlocks Detected" alerts on every OTHER monitored database on
the same logical server. Azure SQL DB engines are isolated per database, so one
database's sessions cannot block another's -- and the stored data bore that out:
byte-identical deadlock graphs and the same top query, counters within ~1%, under
six unrelated server_ids. Not cross-talk. The same rows, collected six times.
Confirmed from source, and the reporter found it before I did:
GetAzureDatabaseListAsync hops to master and runs
SELECT name FROM sys.databases WHERE state_desc = N'ONLINE' AND database_id > 0
returning EVERY online database on the logical server, narrowed only by that
registration's ExcludedDatabases -- a denylist, not an allowlist. The caller then
sweeps all of them and stores every row under the one server_id of whichever
registration ran the sweep. The single-database behaviour existed only as the
fallback for a master-ACCESS ERROR, so the safe path was the exceptional one.
On a logical server holding N separately-registered databases that is N
registrations x N databases: N-squared collection, with every registration's
stored history contaminated by its siblings'.
Not an oversight -- two parts of the product hold incompatible ideas of what an
Azure SQL DB registration IS, and both are deliberate. The enumeration assumes one
registration = one LOGICAL SERVER, which is the shape #857 was written for.
Identity assumes one registration = one DATABASE: server_id hashes
host[:database][:RO], so registering two databases on one server is the supported
way to get two identities, and the Azure query_store path needs a per-database
connection anyway (#1836). Nothing reconciled them, so the second shape silently
behaved like the first.
The rule, now in one shared place: a registration that NAMES a database is a
registration OF that database and sweeps exactly that one. Only a registration
naming none -- or naming master, where a catalog-less Azure connection lands -- is
a registration of the SERVER, and only that one enumerates.
This also subsumes #857's motivating case and improves on it: a login granted
access to one user database but not to master HAS a named database, so it now
returns without probing master at all rather than probing, failing, forming a
verdict and falling back.
Extracted to shared AzureSweepScope rather than fixed twice: both runners carried
their own private copy of this predicate, and a sweep-scoping rule that disagrees
between Lite and Darling is the same class of defect as the one being fixed. Both
suites pin it.
NOT done here, deliberately: the master-probe throttle
(_azureMasterInaccessibleSince / IsMasterProbeThrottled / OnServerReconnected) is
now unreachable for any registration that names a database, which is #857's whole
population. Removing that machinery is a separate change from a correctness fix a
reporter is waiting on, and its guard is left stating the precondition this path
now satisfies by construction.
Verified the rule against compiled code (12/12 in a scratch harness), including the
over-match cases -- mastermind, paymaster, master_archive are real databases and
are still swept -- and that each call returns its own list, since both runners hand
it straight to their per-database loop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reviewed. The core fix in One correctness regression found in both Also flagged: the docstrings above both No security, SQL injection, or missing-index concerns — parametrized queries are unchanged, and the exclusion filter logic wasn't touched. |
… back to Review catch, and it is a regression I introduced rather than a nit. The old guard read `hasFallback && IsMasterProbeThrottled(...)`. I read `hasFallback &&` as a redundant condition and dropped it. It was the opposite: in this branch hasFallback is FALSE, so the guard existed to DISABLE the throttle, exactly as the comment I deleted said -- "honouring the throttle would guarantee 15 minutes of failure without ever attempting to recover." With the check left unconditional, and the branch now reachable only when the registration names no database, one master failure would send every subsequent cycle straight into FallbackDatabaseList -- which throws immediately without probing -- for up to the whole recheck interval, where it used to retry every cycle. Recovery detection delayed by 15 minutes. Removed the check from this path in both hosts, restoring the old effective behaviour, with the reasoning written where the old comment was so the next reader does not re-add it. Left the throttle machinery alone rather than deleting it as newly-dead: Lite's IsMasterProbeThrottled is directly tested by AzureMasterFallbackTests (six assertions from #857/#1506), so retiring it is its own change with those tests. Worth recording WHY it is now obsolete though, since it is a different reason than this fix: its entire purpose was to stop re-probing master for a registration that HAS a fallback, and after #2220 such a registration never probes master at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both confirmed and fixed in I read Left unconditional, and with the branch now reachable only when the registration names no database, one master failure would send every subsequent cycle straight into Removed the check from that path in both, restoring the old effective behaviour, with the reasoning written where the old comment was so it does not get re-added by the next person who reads the condition the way I did. One thing I did not do, deliberately. The obvious follow-through is to delete the throttle as newly-dead code, and I checked before reaching for it: Lite's Worth recording why it is obsolete now, since it is a different reason than this fix: the throttle's entire purpose was to stop re-probing master for a registration that has a fallback, and after this PR such a registration never probes master at all. So it is unreachable in production either way — but that is a deletion to argue on its own merits. |
Review summary (high effort)This is a well-scoped, well-justified fix, and the PR already caught and corrected its own regression in the second commit (the Correctness / parity: No functional bugs found. The new early-return ( What I did flag (inline, all low/medium severity, no functional-bug findings): a few spots where the fix's code changed but adjacent documentation or a newly-added helper didn't keep up, which is a little ironic given the PR's own postmortem is about a doc/code mental-model mismatch:
No SQL injection, secrets, or missing-index concerns; not raising folklore missing-index feedback per instructions. |
Review, three findings, two of them mine to fix. IsScopedToOneDatabase was dead surface. I wrote a named predicate arguing a call site should read as the rule rather than as a length check, then wrote both call sites as `ownDatabase.Count > 0` anyway -- because they need the LIST to return, so the boolean twin would mean calling twice. Dropped it rather than contorting the callers to justify it; the tests that only existed for it went with it. Both methods' XML summaries still taught the pre-#2220 strategy -- "try master enumeration first ... and on a master-access error fall back ... throttling re-probes per server" -- which is now the exceptional path, not the default. Both rewritten to lead with the rule that actually governs: a registration naming a database sweeps that database and never touches master; only a registration naming none enumerates. Lite's also records that this covers #857's own case better than #857 did, since a login with access to one database but not master now never probes at all. Left as flagged follow-up, agreeing with the review: FallbackDatabaseList's `quiet` parameter is now dead in both hosts (one caller, always the default), and it is the same now-unreachable machinery as IsMasterProbeThrottled. Both retire together, with the AzureMasterFallbackTests that pin the throttle, rather than as riders here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All five addressed in
Both stale summaries rewritten. You're right that they re-teach the pre-fix model: "try master enumeration first … and on a master-access error fall back … throttling re-probes per server" is now the exceptional path, not the default. Both now lead with the rule that governs, and Lite's also records that this covers #857's own case better than #857 did — a login with access to one database but not master no longer probes, fails and falls back; it simply never probes.
Full solution still builds clean, 0 warnings. |
|
Reviewed the diff ( Correctness: the fix itself checks out. Lite/Darling parity: good — the predicate was genuinely de-duplicated into the shared Two minor nits, left as inline comments where GitHub would let me anchor them:
No security, missing-index, or performance concerns beyond what's already discussed above (and the change is a net performance win — it removes a |
Review nit, and an earned one: the sed that customized the Darling test file's header for Lite ate two words, leaving "A rule that disagrees and Darling is the same class of defect". In a PR whose whole point is that Lite and Darling must not disagree, that is the sentence to get right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed in |
|
Reviewed the diff ( Correctness / scope check (passed) — Throttle removal reasoning checks out — I traced the old Parity — One gap: no No functional bugs, security issues, or performance regressions found. The deliberate deferral of retiring the throttle machinery is reasonable — it's inert now, not actively wrong, and bundling its removal here would mix an orthogonal cleanup into a correctness fix. |
Fixes #2220.
The report was right, and the reporter found the mechanism before I did
One real deadlock in one Azure SQL Database produced near-identical alerts on every other monitored database on the same logical server. Azure SQL DB engines are isolated per database, so one database's sessions cannot block another's — and the stored data bore that out: byte-identical deadlock graphs and the same top query, counters within ~1%, under six unrelated
server_ids. Not cross-talk. The same rows, collected six times.Confirmed from source.
GetAzureDatabaseListAsync→BuildDatabaseListPlanhops to master:That returns every online database on the logical server, narrowed only by that registration's
ExcludedDatabases— a denylist, not an allowlist. The caller sweeps all of them and stores every row under the oneserver_idof whichever registration ran the sweep. The single-database behaviour existed only as the fallback for a master-access error, so the safe path was the exceptional one.On a logical server holding N separately-registered databases: N registrations × N databases = N² collection, with every registration's history contaminated by its siblings'. This is a data-correctness bug, not alert noise.
Why it happened, which is the part worth keeping
Two parts of the product hold incompatible ideas of what an Azure SQL DB registration is, and both are deliberate:
server_idhasheshost[:database][:RO], so registering two databases on one server is the supported way to get two identities, and the Azure query_store path needs a per-database connection anyway (Query Store collector never collects on Azure SQL Database #1836).Nothing reconciled them, so the second shape silently behaved like the first.
The rule
A registration that names a database is a registration of that database, and sweeps exactly that one. Only a registration naming none — or naming
master, where a catalog-less Azure connection lands — is a registration of the server, and only that one enumerates.This also subsumes #857's motivating case and improves on it: a login granted access to one user database but not to master has a named database, so it now returns without probing master at all, rather than probing, failing, forming a verdict and falling back.
Extracted to shared code rather than fixed twice
Both runners carried their own private copy of this predicate. A sweep-scoping rule that disagrees between Lite and Darling is the same class of defect as the one being fixed, so it now lives in
AzureSweepScopeand both suites pin it.Deliberately not done here
The master-probe throttle (
_azureMasterInaccessibleSince/IsMasterProbeThrottled/OnServerReconnected) is now unreachable for any registration that names a database — which is #857's whole population. Removing that machinery is a separate change from a correctness fix a reporter is waiting on; its guard is left stating the precondition this path now satisfies by construction. Happy to do the cleanup as a follow-up.Verification
Rule checked against compiled code (12/12 in a scratch harness, since
net10.0-windowsxunit will not run on this machine), including the over-match cases —mastermind,paymaster,master_archiveare real databases and are still swept — and that each call returns its own list, since both runners hand it straight to their per-database loop. Full solution builds clean, 0 warnings.What this does not fix: existing stored data. Any store that has been collecting Azure SQL DB siblings already holds rows under the wrong
server_id, and this change stops the contamination rather than unwinding it. Worth deciding separately whether that needs a cleanup path or just a note.🤖 Generated with Claude Code