Repository navigation
Collect tempdb on Azure SQL Database: the gate's stated reason was false (#2512) - #2516
Conversation
TempDbStatsCollector.AppliesTo was !target.IsAzureSqlDb, so the whole tier - General
Purpose and Hyperscale - collected no tempdb data at all. No get_tempdb_trend, no
tempdb space alerting, nothing in the viewer's tempdb surfaces, on a platform where
the customer cannot walk over and look at the box.
The doc comment said the first result set's three-part
tempdb.sys.dm_db_file_space_usage reference could not be served there and the
collector "could only ever fail". Running the collector's SQL verbatim against live
Azure SQL Database, both result sets, over an Entra token (2026-08-22):
GP_S_Gen5_2 (EngineEdition 5, 12.0.2000.8): user 5.44 MB, internal 1.81 MB,
version store 0.00 MB, unallocated 54.19 MB; session 74, 0.13 MB, 1 over threshold
HS_S_Gen5_2: user 1.88 MB, unallocated 60.69 MB
SELECT COUNT(*) FROM tempdb.sys.dm_db_file_space_usage returns 4 on both
And the figures describe something rather than merely returning: allocating ~57 MB
into a #temp table moved user_mb 1.88 -> 59.75 and unallocated_mb 60.69 -> 2.69,
with sys.dm_db_session_space_usage attributing 59.25 MB to the session that did it.
Worth MORE here than on a box: the tempdb ceiling on Azure SQL Database is governed
by the service tier. You cannot add files and cannot grow past the cap, so "tempdb is
filling" means "change service objective", not "go look at the disk".
Managed Instance is unchanged - never gated, real tempdb, full DMV access - and the
pin still asserts both directions so this cannot be re-narrowed later on the strength
of a stale comment.
What the #2150 field report was actually about is PERMISSIONS, and that is a property
of the LOGIN, not of the tier, so a gate is the wrong instrument: it denies every
properly-permissioned Azure target to spare the one that is not. Error 262
("permission denied in database 'tempdb'") was classified Unclassified, which means
ERROR on every cycle forever - the exact collection-health pollution PERMISSIONS
exists to absorb. It is now a permission denial in both SKUs, carrying an Azure
explanation alongside the one error 300 already gets.
Those numbers lived in THREE hand-maintained places and had already drifted: 916 was
in SqlServerTargetProvider.Classify and in neither runner's catch, so the engine seam
whose stated purpose is that the runner and the provider cannot disagree about what
an error means, disagreed. All three now read one shared
SqlServerPermissionErrors.IsPermissionDenied - which is also the only way to pin
them, since SqlException cannot be constructed in a test.
Payload composition verified end to end against the real build, not assumed: the
composed CollectorCatalog.AppliesTo gate (both halves), the by-name surface Lite
dispatches through, RunsPerDatabase=false (plain single-connection path, no Azure
per-database fan-out), a byte-identical query on Azure and on-prem, ReadAsync over
the measured values with session_id as the SMALLINT the DMV actually returns,
WritePayload emitting 8 values in PayloadColumns order with decimal/bigint/integer
CLR types matching the generated tempdb_stats DDL.
One consequence named rather than shipped quietly: unallocated_mb is free space in
the tempdb files AS CURRENTLY ALLOCATED, and Azure creates them small (4 files,
~62 MB total on both 2-vCore tiers) and autogrows toward the cap. So the
reserved/(reserved+unallocated) ratio the tempdb Space alert fires on measures
distance to the next autogrow, not to the ceiling - the measurement above takes it
from 3% to 96% with one #temp table, against an 80% default. Filed as #2515 with the
mechanism, the options and the measurement that should decide, rather than retuning a
threshold every existing on-prem target depends on.
Closes #2512
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| return sqlErrorNumber switch | ||
| { | ||
| 300 => ServerScopedHint, | ||
| 262 => DatabaseScopedHint, |
There was a problem hiding this comment.
Correctness: DatabaseScopedHint assumes every 262 is about tempdb, but the check has no way to know that.
For(int sqlErrorNumber, bool isAzureSqlDb) dispatches on the bare error number alone — it never inspects ex.Message to see which database the denial actually named. Since SqlServerPermissionErrors.IsPermissionDenied now includes 262 for every collector (both Darling's worker catch and Lite's RemoteCollectorService catch call AzureDmvPermissionHint.For(ex.Number, isAzureSqlDb) unconditionally — see DarlingWorker.cs:4500 and RemoteCollectorService.cs:639), any collector that raises SQL error 262 on Azure SQL DB gets this exact tempdb-specific text appended, e.g. "Reach tempdb's space DMVs through server-level state access instead", even when the denial named a completely different database.
Today tempdb_stats is the only cross-database read that can plausibly raise 262, so this is silent for now. But the per-database loop collectors (index_object_stats, file_io_stats, etc.) run against arbitrary user databases on Azure SQL DB and could plausibly raise a genuine "permission denied in database '<userdb>'" 262 for an unrelated reason — and they'd get the same wrong, tempdb-only guidance appended to their error message.
Worth either checking the database name embedded in ex.Message before applying DatabaseScopedHint, or scoping this hint to only fire for the tempdb_stats collector specifically, rather than keying purely off the SQL error number.
|
Reviewed the tempdb/Azure SQL DB gate removal and the permission-error consolidation. The core change is solid — the composed gate derivation ( Two more observations that don't have a changed line to anchor to:
Consolidation is incomplete — two more hand-rolled copies of the same permission-number pattern remain, un-migrated:
These two are consistent with each other (Lite/Darling parity is fine), but both are now out of step with the new There's also None of these are blocking — the core fix (gate removal + permission classification) is well-verified against live measurements per the PR description. |
Review catch. AzureDmvPermissionHint.For dispatched on the error number alone, so
every 262 on an Azure SQL Database target got the tempdb sentence appended - including
a denial that named somebody's user database. That is worse than appending nothing:
"reach tempdb's space DMVs through server-level state access" sends the reader after a
##MS_ServerStateReader## membership that would not have helped, for a denial where the
raw error already names the database and the grant IS the fix.
This is the whole difference between the 262 arm and the 300 one, and it is now stated
where the arms are. 300 is server-scoped: on Azure SQL Database it can only ever be the
service objective, so the number settles it. 262 names a DATABASE and the advice
inverts on which one, so the number cannot.
Both call sites now pass ex.Message, and the arm matches the QUOTED identifier error
262's format string interpolates ("permission denied in database '%.*ls'"). Quoted
rather than bare because tempdb is a reserved name and no user database can collide
with 'tempdb' in that position, while a bare match would read a database called
reporting_tempdb_stage as tempdb. Case-insensitive, because the quoted name follows the
server's collation and not this code's.
A null or absent message stays silent rather than guessing. That direction is
deliberate: a call site that forgets the message loses a helpful sentence instead of
gaining a wrong one.
No collector raises a user-database 262 today - tempdb_stats is the only cross-database
read that plausibly can - but the per-database loop collectors (index_object_stats,
file_io_stats) run against arbitrary user databases on Azure SQL DB and are one
permission change away from it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review finding fixed —
|
| /// </list> | ||
| /// </summary> | ||
| public static bool IsPermissionDenied(int sqlErrorNumber) | ||
| => sqlErrorNumber is 229 or 262 or 297 or 300 or 916 or 8189; |
There was a problem hiding this comment.
The doc comment above frames this as fixing exactly this kind of drift ("it already WAS three literals, and they had already drifted... deriving all three from this set is how it cannot"), but the consolidation is incomplete. Three more sites still hardcode their own copy of the permission-number list instead of calling IsPermissionDenied:
Darling/PerformanceMonitor.Darling.Service/DarlingWorker.cs:3508—catch (SqlException ex) when (ex.Number is 229 or 297 or 300 or 916)(inGetRecentlyFailedJobsAsync)Lite/Services/RemoteCollectorService.RunningJobs.cs:64— the same literal set, in Lite's equivalent methodLite/Services/RemoteCollectorService.cs:616—status = (sqlError.Number == 229 || sqlError.Number == 297 || sqlError.Number == 300)in the XE-session-missing catch
None of these picked up 262 or 8189, and none of them will pick up whatever number gets added to SqlServerPermissionErrors next — they're still hand-maintained lists that can drift, which is the exact failure mode this PR set out to close. Worth routing them through SqlServerPermissionErrors.IsPermissionDenied too (or filing a fast-follow), otherwise the "one shared predicate" claim in the PR description isn't quite accurate.
Review summaryReviewed the diff (Azure SQL DB gate removal on Overall: the core change is well-supported (live measurements cited in the doc comments/PR body) and Lite/Darling parity is maintained for everything actually touched — both catch sites ( One gap worth a look: left an inline comment on
These aren't broken by this PR and Lite/Darling stay in parity with each other at those sites, but they're the same drift risk this PR calls out, just left unconsolidated. Not a blocker, but worth a fast-follow or a note in the PR description that it's deliberately out of scope. No correctness, security, or missing-index concerns found beyond that. |
Second review catch, and a fair one - the doc comment claimed the drift was closed while three more hand-written copies of the list were still sitting there, which made the claim less true than before rather than more. The full census, and it had drifted in three different directions at once: SqlServerTargetProvider.Classify 229 297 300 916 8189 DarlingWorker collector catch 229 297 300 8189 Lite RunCollectorAsync collector catch 229 297 300 8189 DarlingWorker failed-jobs msdb read 229 297 300 916 Lite failed-jobs msdb read 229 297 300 916 Lite XE-session-ensure catch 229 297 300 Six lists, four distinct sets, one question. All six now ask SqlServerPermissionErrors.IsPermissionDenied. One set is right for all of them because they all answer the same question - did the login get told no - even though they act on it differently: the collector catches record PERMISSIONS instead of ERROR, the failed-jobs reads return an empty list instead of failing the alert cycle, and the XE catch marks the capture unavailable. What each does with the answer is its own business; what counts as a denial is not. Widening is correct rather than merely tolerable at each site: - failed-jobs msdb reads gain 262 and 8189. A 262 naming msdb is precisely the least-privilege case these exist for and used to fall through to the warning arm; 8189 is sys.traces and cannot arise from an msdb read at all. - the XE catch gains 262, 916 and 8189. 8189 is inert there for the same reason; 262 and 916 both mean the login was denied where it asked, which is the PERMISSIONS this arm already records for 229/297/300. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#2512 measured the tempdb DMVs returning real data on Azure SQL Database, and #2516 opens TempDbStatsCollector's gate accordingly -- so tempdb_stats stops being a permanent gap and get_tempdb_trend correctly reverts to unavailable there. The behaviour tests used it as their third example, which tied them to a gate that is moving. Swapped for get_default_trace_events: Azure SQL Database has no default trace at all, so that gate is one of the durable ones (as are sys.configurations and DBCC TRACESTATUS, both already asserted). No coverage lost -- the mechanism is what is under test, not any one read. get_tempdb_trend keeps its capability call, which self-disables the moment the gate opens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second review finding fixed —
|
| site | 229 | 262 | 297 | 300 | 916 | 8189 |
|---|---|---|---|---|---|---|
SqlServerTargetProvider.Classify |
✓ | ✓ | ✓ | ✓ | ✓ | |
DarlingWorker collector catch |
✓ | ✓ | ✓ | ✓ | ||
Lite RunCollectorAsync collector catch |
✓ | ✓ | ✓ | ✓ | ||
DarlingWorker failed-jobs msdb read |
✓ | ✓ | ✓ | ✓ | ||
| Lite failed-jobs msdb read | ✓ | ✓ | ✓ | ✓ | ||
| Lite XE-session-ensure catch | ✓ | ✓ | ✓ |
Six lists, four distinct sets, one question. Not one of them had 262. All six now ask SqlServerPermissionErrors.IsPermissionDenied.
One set is right for all of them because they all answer the same question — did the login get told no? — even though they act on it differently: the collector catches record PERMISSIONS instead of ERROR, the failed-jobs reads return an empty list instead of failing the alert cycle, the XE catch marks the capture unavailable. What each one does with the answer is its own business; what counts as a denial is not. That is now stated on the type.
Widening is correct at each site, not merely tolerable, and I checked rather than assumed:
- failed-jobs msdb reads gain 262 and 8189. A 262 naming msdb is precisely the least-privilege case those catches exist for, and it used to fall through to the warning arm. 8189 is
sys.tracesand cannot arise from an msdb read at all. - the XE-session catch gains 262, 916 and 8189. 8189 is inert there for the same reason; 262 and 916 both mean the login was denied where it asked, which is exactly the
PERMISSIONSthat arm already records for 229/297/300.
This is the second instance of one category in this PR — a rule stated in one place and transcribed into others — so it was worth taking the whole census rather than fixing the three the review named and leaving a seventh to be found later.
|
Reviewed. This is a clean, well-tested fix — a few notes, no blockers found. What it does: Removes the Parity: Solid.
One pre-existing thing adjacent to this change, not a defect in it: No SQL files changed, no correctness issues found in the widened permission set (each catch site's own comments correctly reason through why the widening is safe there), and the new tests exercise the real edge cases (short→int widening for |
|
Reviewed and ready — deliberately not merging yet. This PR is correct and I want it. The hold is the consequence it documents: enabling Azure tempdb collection while Since then the question got settled rather than deferred. Probing the same #2515 now has that evidence and is being fixed as a denominator correction ( Two things in here I want to note separately, because they are worth more than the gate:
And thank you for checking #2511's map rather than assuming: confirming it is genuinely derived from |
|
#2517 merged first (
if (rows.Count == 0)
- return await DarlingEngineCapability.NotCollectedStatusAsync(postgres, resolved.ServerId, resolved.ServerName, "tempdb_stats")
- ?? McpHelpers.Status("unavailable", "No TempDB data available.");
+ return McpHelpers.Status("unavailable", "No TempDB data available.");
{
- return await McpEngineCapability.NotCollectedStatusAsync(dataService, resolved.ServerId, resolved.ServerName, "tempdb_stats")
- ?? McpHelpers.Status("unavailable", "No TempDB data available.");
+ return McpHelpers.Status("unavailable", "No TempDB data available.");
}Nothing else of #2511's needs touching: the derivation is by-sweep, so - ["tempdb_stats"] = "the tempdb space-usage snapshot",Three lines total. My test files no longer reference Why in your PR rather than a follow-up from me, having offered the other way round earlier: mine merged first, so splitting it would leave And to close the loop on my earlier note: you were right that the gate's stated reason was false, and #2511 was about to convert that into a message telling Azure callers a working DMV "is not collected for this server, and never will". Good catch, and it is a better illustration of #2511's own thesis than anything in my PR body. |
#2511 landed first and made the reads answer "not collected, and permanently so on this engine" wherever a collector is gated off for the target's engine. Correct machinery, and it derived that answer for tempdb from a gate this branch exists to remove -- so an Azure caller was about to be told a DMV that demonstrably works "is not collected and never will be". That is worse than the message it replaced. "Unavailable, go check" leaves someone investigating and possibly discovering it works; a permanent verdict closes the question. The chain is wrong-gate to correct-derivation to confident-wrong-answer, and the derivation is the only honest link. So the un-wiring lands with the gate change, in three places, not the two this branch's description claimed: both SKUs' reads stop asking the capability question, and the prose entry goes too -- there is a pin asserting every capture-path entry describes a collector still gated somewhere, which is the pin doing exactly its job. The CHANGELOG conflict was two ADDITIVE entries. Kept both; taking either side would have silently reverted the other's release note while looking like a resolution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed the diff (14 files, tempdb_stats gate removal + shared permission-error classification). Verdict: looks solid, no correctness or parity issues found. What I checked:
No T-SQL was touched in this PR (all changes are C#), so the CONTRIBUTING T-SQL style rules don't apply here. C# changes are well-commented per the "comments explain WHY" convention and don't need further nitpicking. Nothing to flag for correctness, security, or parity. |
|
#2515 is implemented in #2521, so the caveat this PR carries is now resolved rather than deferred: the ratio did not need a size floor, it needed a different denominator. This PR is safe to ship, and it should go after #2521 — or at least not before it, since until #2521 lands, enabling Azure collection arms the alert this PR's own doc comment warns about. Three things to expect when the two meet, from a trial merge of this branch into
(Plus the usual CHANGELOG link-definition block — both branches add |
The class-summary conflict had this branch saying the Azure gate is gone and dev adding #2515's ceiling explanation. Both are true and both are needed, so both stay -- taking either side would have shipped a doc that is half right and reads as settled. The two hazards flagged on this PR were real and neither was a conflict, so git would have merged them silently: AppliesTo's doc still argued the ratio "needs a size floor before it can safely carry the tempdb Space alert -- filed as #2515". That was true when written and is now the opposite of what shipped: #2515 changed the denominator instead, precisely because a floor was the wrong instrument. A stale forward-reference to an issue that has since been settled the other way is worse than no comment, because it reads as current design intent. And the Azure payload test passed eight arguments to a Row that grew to nine. It failed to compile -- in Lite.Tests, not Darling.Tests, which is why my first build said clean and I nearly believed it. Fixed by feeding and asserting the ceiling rather than by adding an argument to satisfy the compiler: the fixture now carries 65,536 MB beside the real GP_S_Gen5_2 allocation figures, so the test pins the thing the merge was about instead of merely surviving it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| catch (SqlException ex) when (ex.Number is 229 or 297 or 300 or 916) | ||
| { | ||
| /* Expected for read-only monitoring accounts; hit every alert cycle, so Info. The named | ||
| catch (SqlException ex) when (SqlServerPermissionErrors.IsPermissionDenied(ex.Number)) |
There was a problem hiding this comment.
Nice fix widening this catch to the shared predicate — but the XML doc summary directly above FetchFailedJobsAsync (around line 3465) is now stale: it still says "a login that cannot SELECT the msdb job tables raises SqlException 229/297/300/916". After this change the set is the full SqlServerPermissionErrors list (229/262/297/300/916/8189). Same drift in PerformanceMonitor.Alerting/FailedJobsQuery.cs's class doc, which says Darling's runner degrades "the same exception" as the Dashboard's literal 229/297/300/916 check — no longer true now that Darling routes through the wider shared set. Given this whole PR is about a doc comment's premise going unchecked, worth tidying these two along with it.
| /* #2512: the numbers live in SqlServerPermissionErrors now, so this and the two runner catch | ||
| filters cannot disagree about what a denial is — they already had (916 was here and in | ||
| neither of them). */ | ||
| if (SqlServerPermissionErrors.IsPermissionDenied(sql.Number)) |
There was a problem hiding this comment.
CollectorTargetFault.cs's doc comment on the Permissions member still says "SQL Server: 229, 297, 300, 8189" — it was never updated to match the 6-number SqlServerPermissionErrors set this Classify method (and the two runner catches) now use, so it's missing 262 and 916. Small thing, but it's the canonical description of what this fault class means and is now inaccurate.
|
Reviewed. This is a well-evidenced fix and the parity/derivation story checks out — verified:
Left two inline nits — both doc-comment drift, not runtime bugs: No correctness, security, or Lite/Darling parity issues found beyond that. |
Fixed the Row comparison and missed the positional payload array three lines below it, which asserts what the writer emits. The writer emits nine values now; the expectation still listed eight. Both assertions exist for different reasons -- the Row proves the collector READ the ceiling, the payload array proves it WRITES it in the position the column expects -- so patching only the first would have left the half that guards the store write untested while the suite went green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review: #2516 (tempdb on Azure SQL Database)Went through the diff against CONTRIBUTING.md's style rules and the Lite/Darling parity requirement. Overall this is a well-verified, well-tested change - the gate removal is backed by live measurements, the permission-number consolidation removes a real drift (916 was in the classifier but neither runner catch; the two failed-jobs reads and Lite's XE-session catch didn't have 8189/916/262 in various combinations), and Lite/Darling stay in lockstep across DarlingWorker.cs and RemoteCollectorService, SqlServerTargetProvider.Classify and Lite's catches, and both GetTempDbTrend MCP tools dropping the now-always-null NotCollectedStatusAsync fallback together. One class of finding, not blocking: this PR's whole point is eliminating hand-maintained copies of the permission-error-number set (SqlServerPermissionErrors.cs's own doc says the set "already WAS six literals... drifted in three different directions"). Three doc comments elsewhere still spell out the old, narrower literal lists and are now stale relative to the new shared IsPermissionDenied set (229, 262, 297, 300, 916, 8189). None are on lines this PR touched, so I can't leave these inline, but worth a follow-up touch-up:
Nothing else stood out as a correctness, security, or parity problem - AzureDmvPermissionHint.For's new errorMessage parameter is threaded through both of its call sites (Darling and Lite), the 262/tempdb-name matching is quote-anchored so it can't false-positive on a user database merely containing "tempdb" in its name, and the new max_size_mb subquery's three-part tempdb.sys.database_files reference falls under the same permission-classification catch as the rest of the batch even though it wasn't separately called out in the live-verification measurements. |
Closes #2512.
What the gate cost
Every Azure SQL Database target — General Purpose and Hyperscale — collected no tempdb data at all. No
get_tempdb_trend, no tempdb space alerting, nothing in the viewer's tempdb surfaces, nothing for the analysis pass'sTEMPDB_USAGEfact to score. On a platform where tempdb contention is common and the customer cannot go and look at the box themselves.Its doc comment said the first result set's three-part
tempdb.sys.dm_db_file_space_usagereference could not be served there, so the collector "could only ever fail". That premise was checkable.The live evidence
The collector's SQL, verbatim, both result sets, over an Entra token (2026-08-22):
GP_S_Gen5_2(EngineEdition 5, 12.0.2000.8)HS_S_Gen5_2SELECT COUNT(*) FROM tempdb.sys.dm_db_file_space_usagereturns 4 on both. No Azure-specific query variant is needed, and both result sets bind — so the one row this collector promises is a whole row on Azure SQL DB, which is what the old comment's "half a row is a different contract" objection was about.And the figures describe something rather than merely returning, which was the open question. Allocating ~57 MB into a
#temptable onGP_S_Gen5_2:user_mbunallocated_mbwith
sys.dm_db_session_space_usageattributing 59.25 MB to the session that did it. The counters track actual allocation; the session view attributes it correctly.Worth more here than on a box. The tempdb ceiling on Azure SQL Database is governed by the service tier: you cannot add files and cannot grow past the cap. On a box "tempdb is filling" means "go look at the disk". Here it means "you are approaching a limit you cannot raise without changing service objective".
What each red test was really pinning
CollectorGateSurfacePinTests.TempDbStats_AppliesTo_SkipsOnlyAzureSqlDb— the only test that goes red, and it was pinning two different claims in oneAssert.False:The other half of that test —
Assert.True(...AppliesTo(AzureMi))— never changed, and it is why the test survives rather than being deleted. Its original comment says outright that asserting both directions is what stops an "anything Azure" gate creeping in. That risk now runs the other way: this must not be re-narrowed later on the strength of the stale comment, so both directions are still asserted, and the test now also asserts the composedCollectorCatalog.AppliesToand the by-name surface Lite dispatches through — neither of whichAppliesToalone can see.Nothing else was pinning the gate.
AzureSweepScopeTests,CollectorRunnerConnectionEngineTestsandDarlingWorkerTestsdon't reference this collector at all;DarlingCliCommandsTests.ToTargetInfo_RoundTripsTheFactsTheGateReadsasserts all SQL Server collectors are off at a PostgreSQL target, which is the engine half of the composed gate and is unaffected — verified by asserting the same thing directly against the real build.Coordination with #2511
The composer's capability map is derived, not transcribed —
DarlingWebEndpoints.BuildAppliesToNodecallscollector.AppliesTo(s_azureSqlDbTarget)per measure — so the threetempdb_*measures flip toazureSqlDb: truewith no edit here, andDarlingComposeTests.Catalog_Measures_CarryAppliesTo_ReflectingTheCollectorGatedoesn't name tempdb. If #2511'snot_collectedmapping is derived the same way it will flow through automatically; if it goes red on this branch, that is the signal the map was transcribed.What I verified about payload composition
The issue listed what it had not checked. All of it, executed against the real build rather than reasoned about:
CollectorCatalog.AppliesTo(def, azureSqlDb)→ true,(def, azureMi)→ true,(def, postgres)→ false (the engine half still holds). PlusCollectorCatalog.AppliesTo("tempdb_stats", azure), the by-name surface Lite dispatches through.RunsPerDatabase(azure)→ false, andBuildEnumerationQuery→ null. So on Azure this takes the plain single-connection path, not the per-database loopfile_io_statsandindex_object_statstake. Per [BUG] Blocking/Deadlock alerts cross-talk between Azure SQL databases sharing the same logical server (Darling) #2220 a registration naming a database is scoped to that database, so this is one tempdb snapshot per registration, not N.sys.dm_db_session_space_usage.session_idissmallint, so the driver hands back ashort; the definition reads it throughConvert.ToInt32(GetValue(0))precisely for that. Fed a realshort,ReadAsyncwidens it andWritePayloademitsintfortop_session_id INTEGER. The existing parity pin feeds anintand can never see this.total_sessionsisCOUNT_BIG→long→bigint.PayloadColumnsorder, CLR typesdecimal ×5, long, int, decimal, matching the generated DDL (numeric(18,2)×5,bigint,integer,numeric(18,2)).It composes cleanly. Nothing had to be forced.
The permissions half — what actually makes this safe
The #2150 field report was 11x consecutive with error 262, "VIEW DATABASE PERFORMANCE STATE permission denied in database 'tempdb'". That is real, and removing the gate without handling it would have regressed a fixed bug — 262 was classified
Unclassified, which means ERROR, every cycle, forever, which is exactly the collection-health pollution the gate was created to stop.262 is now a permission denial in both SKUs, so such a login degrades to a non-fatal PERMISSIONS skip with an explanatory Azure sentence, the same treatment error 300 already gets (#1631) — while every properly-permissioned Azure target collects.
Those numbers lived in three hand-maintained places and had already drifted: 916 was in
SqlServerTargetProvider.Classifyand in neither runner's catch, so the engine seam whose stated purpose is that "the runner and the provider cannot disagree about what an error means" disagreed. All three now read one sharedSqlServerPermissionErrors.IsPermissionDenied. That is also the only way to pin them —SqlExceptioncannot be constructed in a test, so a shared predicate is the difference between a tested rule and three unverifiable literals. Note the behaviour change: Lite and Darling's runners gain 916 → PERMISSIONS. That is the correct classification perCollectorTargetFault's own definition, and it is called out here rather than smuggled in.Thresholds that need attention — filed as #2515
unallocated_mbis free space in the tempdb files AS CURRENTLY ALLOCATED, not headroom to the tier ceiling. Azure creates them small — 4 files, ~62 MB total on both 2-vCore tiers measured — and autogrows toward the cap. Sototal_reserved / (total_reserved + unallocated), which is exactly whatTempDbSpaceInfo.UsedPercentcomputes and what thetempdb Spacealert fires on at a default of 80%, measures distance to the next autogrow, not to the ceiling.Run the alert's own arithmetic on the measured numbers:
UsedPercent#temptableOne ordinary temp table puts an Azure SQL DB target over the default with 60 MB at stake. That would fire on the first busy minute of every Azure target this PR enables, which is the "worse than the gap" case. The alert needs a size floor — the shape
ScorePlanCacheBloatalready uses ("a high % on a trivially small footprint is noise").I did not retune it here. The floor value is a policy call that suppresses alerts existing on-prem and RDS targets get today, and I cannot measure what those customers rely on from here — note that
AlertEngineTests.TempDb_FiresAtThreshold_AndResolutionCarriesTheCurrentPercentuses a fixture totalling exactly 1000 MB, so a 1 GB floor flips an existing pin. #2515 carries the mechanism, the three options, and the measurement that should decide (the distribution of total tempdb size at the momentUsedPercent >= 80across collected history). If you want the floor in this PR instead, say so and I will add it.Second threshold, failing the other way and needing no change:
FactScorer.ScoreTempDbVersionStorebars at 1000/2000/5000 MB are unreachable while tempdb sits at ~62 MB, so that arm scores 0. It is self-consistent — a >1 GB version store implies a >1 GB tempdb — and produces no false alerts. Worth knowing because RCSI is on by default on Azure SQL Database, which makes the version store the tempdb consumer most likely to matter there; the space-fraction arm is the only cover until tempdb grows past a gigabyte. Noted in #2515, not acted on.get_tempdb_trend(both the MCP read and the viewer's),TempDbSpaceSql, and the analysis facts needed no change: they are column reads with no absolute-MB assumptions, and thenumeric(18,2)→double precision/bigint/integertyping is already what an Azure row produces.Testing
CollectorGateSurfacePinTests.TempDbStats_AppliesTo_EveryTarget_IncludingAzureSqlDb— flipped Azure, kept MI, added the composed and by-name surfaces.TempDbStatsCollectorDefinitionTests.AzureSqlDb_MeasuredValues_ComposeThroughToThePayload— the composition run above, using the valuesGP_S_Gen5_2actually returned and a realshortsession id.SqlServerPermissionErrorsTests— the six permission numbers, six non-permission numbers that must stay loud, and the 262 hint on/off Azure.Test projects are
net10.0-windowsand cannot run on macOS, so every assertion above was additionally executed against the real build through a throwawaynet10.0harness first. CI is the arbiter for the suites themselves.🤖 Generated with Claude Code