Skip to content

[BUG] N registrations silently resolving to one database get N identities and N copies of every incident: no DB_NAME() cross-check exists #2228

Description

@erikdarlingdata

What

N registrations whose connection strings silently resolve to the SAME database get N distinct identities and N full copies of every collected row, with no warning anywhere. Surfaced by #2220: six Azure SQL DB registrations under one logical server, six server_ids each holding byte-identical deadlock graphs all naming the same source database, one real incident alerting six times.

Mechanism, verified in source

  • Identity is registration-derived, never connection-derived: server_id = ServerIdHelper.GetDeterministicHashCode(server.Config.StorageName). Six registrations carry six identities regardless of where their connections actually land.
  • Collectors stamp the live truth (database_name = DB_NAME() on the Azure per-database paths) but nothing COMPARES that answer to the registered catalog, at --test-connection, at connect, or at sweep time. A missing or wrong Initial Catalog (Azure defaults you into whatever the connection policy resolves) lands the sweep in the wrong database and every collector happily stores the wrong database's rows under the registration's identity.
  • Database-scoped XE makes the [BUG] Blocking/Deadlock alerts cross-talk between Azure SQL databases sharing the same logical server (Darling) #2220 evidence conclusive for this mechanism: a database-scoped session can only capture its own database, so six identical graphs naming Source-DB require six connections IN Source-DB.

Fix shape

Two layers, both cheap because the data is already in hand:

  1. Sweep-time tripwire (the safety net): the connector already round-trips the server on connect; compare DB_NAME() against the registered database and, on mismatch, log loudly to collection_log and raise the existing self-alert path - "registration 'Sibling-A' is connected to database 'Source-DB', not 'Sibling-A': check Initial Catalog". Alert once per registration per mismatch state, not per sweep.
  2. Registration-time collision check: when a new registration's (host, actual DB_NAME()) pair matches an existing registration's, refuse or require an explicit override. This is the concrete half of what [DESIGN] server_id identity carries neither engine nor port, so two instances on one host collide #2218 is already weighing (identity carries neither engine nor port; it also carries neither the server's own answer for the database).

Azure SQL DB is the primary exposure (per-database registrations are the normal shape there), but the tripwire is engine-generic.

Relations

Activity

  1. erikdarlingdata commented on Aug 14, 2026

    @erikdarlingdata
    OwnerAuthor

    Design recommendation covering this, #2218 and #2228 together is on #2218: #2218

    Short version — these are two problems, not one. Identity is derived from mutable input (this issue, and the reason #2218 cannot just append fields), and identity does not reflect what the connection actually reached (#2228). Those pull in opposite directions, so the recommendation is to split the two jobs: a stable surrogate server_id allocated once, plus a separate observed-identity fingerprint used for duplicate detection rather than as a key.

    The migration is cheaper than it looks: seed the surrogate with today's hash value and stop recomputing it, so no collect.* data moves, server_id stays an int, and only future edits behave differently.

  2. erikdarlingdata commented on Aug 14, 2026

    @erikdarlingdata
    OwnerAuthor

    Scoping pass posted on #2218 (comment #2218 (comment) for #2158's half; the census itself is #2218 (comment)). The finding that lands on this issue:

    The fingerprint is not optional — one existing site needs it. StoreConfigProvider:203 is the #2254 file-vs-store diagnostic, and it compares file-derived ids against stored ids deliberately; FileOnlyServerWarningTests.SameNameButADifferentHost_IsStillReportedAsAbsent pins that a name match would get it wrong. Under a stable surrogate a darling.json entry cannot derive the stored id at all, so that comparison has to re-key onto something. Storage name is the cheap answer and reintroduces exactly the confusion the test rules out; the observed-identity fingerprint is the correct one.

    So the two halves of the #2218 design are not independently shippable in either order — the surrogate breaks a diagnostic that only the fingerprint can fix. Worth knowing before we scope them as separate tickets.

    Secondary: whether the fingerprint lives on the config row or in its own table is a real decision, not a detail. Only a separate table can hold history, which is what makes it a detector ("this id has been reached at three different SERVERPROPERTY('ServerName') values this month") rather than a flag that gets overwritten by whichever connect happened last — and last-writer-wins is close to the current behavior this issue is complaining about.

  3. erikdarlingdata commented on Aug 14, 2026

    @erikdarlingdata
    OwnerAuthor

    Scope note from #2220, now that its cause is confirmed (#2265): this tripwire would not have caught it, and the reporter argued that correctly before I did.

    #2220 turned out to be the Azure per-database sweep enumerating master and collecting every sibling database on the logical server under one registration's server_id. Every registration's entry connection was genuinely, correctly scoped to its own database — so a DB_NAME()-vs-registered-catalog check at connect or sweep time passes cleanly while the fan-out happens downstream of it.

    That does not argue against this issue; it narrows what it claims. This tripwire covers a registration whose connection silently resolved elsewhere (bad or missing Initial Catalog), which is still invisible today and still worth a check. It just should not be described as covering sibling-database contamination, because that came from the enumeration rather than from the connection.

  4. erikdarlingdata commented on Aug 15, 2026

    @erikdarlingdata
    OwnerAuthor

    Closing: the cross-check this issue said did not exist now does, shipped in #2277.

    Both engine probes return the database the connection actually reached — DB_NAME() on SQL Server, current_database() on PostgreSQL — and the worker compares it against the registration on every connect. DB_NAME() needs no DMV, so this did not reintroduce the VIEW DATABASE STATE dependency #1535 removed from that probe, and both columns are appended because every read there is positional.

    Why it had to be at connect rather than in the registry, which is worth recording since #2218 was landing at the same time: identity is now assigned rather than derived (#2158, #2218), and neither of those can touch this defect — the two colliding registrations genuinely differ in configuration, so no amount of care in hashing config can tell that they resolve to one database. Only the server can answer what a connection reached.

    Fires at Error on the transition, not per connect: a mismatch is a standing misconfiguration that persists until someone edits the registration, so repeating it every reconnect would bury the one line that matters. The recovery is logged too, so a fix is confirmed rather than merely silent.

    Deliberately silent in three cases, each a false positive that would have made the tripwire worthless: a registration naming no database is server-scoped by design and meant to land wherever the login defaults; a null probe answer is unknown rather than mismatched; and comparison is case-insensitive, which is what SQL Server database names are.

    Your layer 2 — refusing a new registration whose (host, actual database) pair collides — is now #2280. Two things I found while scoping it that belong on the follow-up rather than here: the three registration paths do not share a code path and the file seed probably should not refuse (a server silently unmonitored is worse than one collected twice with a loud log), and the check must compare on the full identity rather than (host, database), because a read-only-intent registration alongside a read-write one for the same database is legitimate and read_only_intent is already part of the identity.

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