Skip to content

The Azure per-database branch emits no phase split at all #2855

Description

@erikdarlingdata

The Azure SQL DB per-database branch in DarlingCollectorRunner.RunAsync (the OpenDatabaseConnectionAsync loop) emits no phase split at all. It is uninstrumented, not mis-instrumented — distinct from #2854, which fixed stamps that existed but were skipped on throw.

Today it measures only a blended per-database total:

using (var dbConnection = await OpenDatabaseConnectionAsync(perDbProvider, server, databaseName, dbToken))
using (var dbCommand = CreateCollectorCommand(perDbProvider, dbPlan, dbConnection, perDbTimeout))
using (var dbReader = await dbCommand.ExecuteReaderAsync(dbToken))
{
    batch = await definition.ReadAsync(dbReader, context, dbToken);
    ...
}
var dbSqlMs = sqlSlice.ElapsedMilliseconds;

So an Azure per-database run has exactly the attribution problem #2851 fixed for server-scoped collectors and #2164/#2312 fixed for the enumerated path: one number covering everything, which cannot say whether a slow database is bound by connect, by server-side work before the first row, or by streaming.

Why it is its own issue rather than part of #2854

The shape differs. This branch opens a connection per database, which neither other path does. Its split is therefore connect: + open: + drain:, not open: + drain: — a third decomposition, with OpenDatabaseConnectionAsync as a phase in its own right. On Azure SQL DB that connect is a real cost (a fresh login per database, per cycle), and it is exactly the kind of term that currently hides inside a blended number.

It needs new fields and a new log line, not a stamp move: new CollectorContext members, a gating flag mirroring PerItemPhasesMeasured / ServerPhasesMeasured, and a per-database line this branch does not currently emit at all.

Worth doing because the pattern keeps paying

Two independent ~20-40x in-process gaps have been found this week and both were only tractable once phases were split:

Neither was visible in a blended number. The Azure branch is the last collection path still reporting one.

When it lands, extend the pin

ServerScopePhaseSplitTests now derives its IL reachability set from CollectorContext (PerItem*Ms / ServerScope*Ms, long, settable) and asserts a minimum count. New phase stamps named to that pattern are covered automatically; a stamp named outside it would not be, so either follow the convention or widen the derivation and raise MinimumPhaseStamps.

Activity

  1. erikdarlingdata commented on Sep 4, 2026

    @erikdarlingdata
    OwnerAuthor

    Fixed by #2893, merged to dev.

    The Azure SQL DB per-database branch now emits connect: + open: + drain: + other: instead of one blended per-database total, with OpenDatabaseConnectionAsync timed as a phase in its own right — the third decomposition this issue called for, since that branch opens a connection per database and neither other path does.

    Naming took option (b), a new PerDatabase*Ms prefix, and widened the pin's derivation in the same change. Reusing PerItem*Ms was rejected on the precedent already recorded in ServerScopeOpenMs's own doc comment: the enumerated log site gates on PerItemPhasesMeasured and prints an item name taken from the enumeration, so sharing those fields makes an Azure run print as an enumerated one. That is the same collision that gave the server-scoped path its own prefix. ServerScopePhaseSplitTests' derivation now admits the third prefix and MinimumPhaseStamps went 12 → 16.

    Two latent holes turned up on the way, both now pinned rather than just noted:

    1. MinimumPhaseStamps = 12 was already one behind the true 13. An abandoned collection cycle records that it stopped, not what it was doing #2864's ServerScopeLastReadMs matched the derivation pattern the day it landed and was covered automatically — exactly as designed — but nobody raised the floor, so a deletion could have passed unnoticed. 16 is now the exact count, not a round number.
    2. The measured flags were guarded by nothing at all. PhaseSetters selects on long, so no bool could ever appear in it. A flag assigned after the await would leave a faulting phase declaring itself unmeasured and printing nothing, while the number stamped correctly and the arithmetic pin stayed green. MeasuredFlags_AreReachableFromExceptionHandlers_... closes that, derived on bool/*PhasesMeasured through the same IL walk, and it covers the pre-existing PerItemPhasesMeasured too.

    Every stamp and the flag are set from a finally, verified by IL walk rather than inspection (inHandler=1 on all three setters, against a control setter at inHandler=0). The per-iteration reset matters and is deliberate: the loop reuses one CollectorContext across databases, so without it a faulting connect would print the previous database's split as its own.

    Scope — what this deliberately does not do:

    Not verified: no Azure SQL DB target was available, so the branch was never exercised end to end. What the per-database connect actually costs is now answerable, not answered — and the same applies to the PostgreSQL per-database collector that shares this branch. The Windows suites ran in CI only.

  2. added a commit that references this issue on Sep 4, 2026
    ed811c9
  3. added 2 commits that reference this issue on Sep 4, 2026
  4. erikdarlingdata commented on Sep 4, 2026

    @erikdarlingdata
    OwnerAuthor

    Correction: this split is live in production, and my closing note said otherwise

    I closed this saying "no Azure SQL DB target was available, so the branch was never exercised end to end… What the per-database connect actually costs is now answerable, not answered." The first half is true and the conclusion is wrong.

    The branch is not Azure-only. RunsPerDatabase is engine-unconditional — => true; — for seven PostgreSQL collectors: pg_index_bloat, pg_index_usage_stats, pg_table_bloat_stats, pg_autovacuum_stats, pg_column_stats, pg_predicate_stats and pg_extension_availability. All seven reach this same loop on every PostgreSQL target, every cycle, where one connection per database per cycle is exactly what connect: was added to name.

    So the connect:/open:/drain: split shipped here has been emitting in production on the Aurora fleet since it landed. What is genuinely un-exercised is the Azure arm specifically: all 84 SQL Server targets are RDS, which does not host Azure SQL DB (engine_edition = 3, service_objective NULL on probed members — the shipped collector's own witness, since that column is populated only WHEN EngineEdition = 5).

    Why the distinction matters rather than being pedantic: I filed the deferred fault-path half as #2896 with a stated expectation that it might not be worth building because the branch may never run. That reasoning was wrong on the same misreading, and #2896's own investigation is what caught it. It has since landed (#2922) — and found a latent hazard created by this issue's change: the per-iteration clear sat below the stopwatch, so a fault in the watermark read, the adaptive-shrink store write, or BuildQuery reached the catch arms holding the previous database's flag and stamps. Reading the split there would have attributed one database's connect: to another. Now moved to the top of the try.

    No action needed here — this issue's change is correct and the follow-up is closed. Recording the correction because "answerable, not answered" undersold what had actually shipped, and because anyone reading this issue to understand the branch's reach would have been misled by my note.

  5. added 2 commits that reference this issue on Sep 10, 2026
    a0cd424
    c9536a7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions