Skip to content

[BUG] Registration-time collision check: refuse a new registration whose host + actual database matches an existing one #2280

Description

@erikdarlingdata

Split out of #2228, whose sweep-time tripwire shipped in #2277. That issue's core assertion — "no DB_NAME() cross-check exists" — is no longer true; this is layer 2 of the fix shape it proposed.

What is still wrong

Nothing stops a NEW registration being created for a host + database that an existing registration already resolves to. The tripwire in #2277 will notice at connect and log loudly, but only after the registration exists and has begun collecting — so the duplicate history it warns about has already started accumulating under a second identity.

The fix shape, from #2228

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.

Why it is more than the tripwire moved earlier

The check needs the actual database, which only the target can answer — so registration has to connect before it can decide. That is reached three different ways, and they do not share a path:

  • The Viewer's Add Server dialog already has a Test Connection button, so it can connect — but the check has to be mandatory rather than optional, or it only protects the operators who press it.
  • add_servers (MCP) already does an in-process connection probe, so this is the cheapest of the three to wire.
  • The darling.json seed runs before any connection exists, at service start, for every server at once. Refusing there is a different question — it would mean a server silently not being monitored because of a collision, which is worse than collecting it twice with a loud log.

That asymmetry is worth settling before implementing: the honest answer may be that the seed keeps the tripwire only, and the two interactive paths refuse.

Also unresolved, from the same investigation

Whether an override is needed at all. Two registrations deliberately pointing at one database is not obviously useful — but 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, so the check must compare on the full identity rather than on (host, database) alone or it will refuse that valid pair.

Activity

  1. erikdarlingdata commented on Aug 15, 2026

    @erikdarlingdata
    OwnerAuthor

    Fixed in #2283, merged to dev.

    add_servers now refuses a registration whose connection lands in a database another registration already covers. The probe already ran in-process at Add, and since #2277 it returns the database the connection actually reached — so the answer was in hand at exactly the moment the decision had to be made.

    Keyed on the full identity, which is what keeps the two legitimate pairs working: a read-only-intent registration alongside its read-write twin for one database, and a PostgreSQL instance alongside a SQL Server on one host. Silent when the probe reports no database (unknown is not colliding) and when the entry reaches the database it names (already ruled on by the declared-duplicate gate).

    It also fixed a regression #2218 had introduced in this same method, which turned out to be the more urgent half. The duplicate gate built its key from host, database and read-only intent only — the 3-argument form — so once engine and port joined the identity the gate was keying on a narrower identity than the store derives. A PostgreSQL instance on a host that already had a SQL Server registration read as a duplicate and was refused: a valid pair rejected because the gate could not see what distinguished them. ExistingServersSql did not even select the columns, so there was nothing to notice; it compiled and every existing test passed. It now reads engine, port and has an explicit pin that also asserts the two identities genuinely differ, so the columns cannot quietly become decorative.

    Two things from scoping that are recorded on the method rather than left to be rediscovered:

    • The file seed keeps the tripwire only. A server silently unmonitored because of a collision at service start is worse than one collected twice with a loud log, and refusing there is a startup-time decision nobody is present to see.
    • The honest limit: existing rows record only the database they declare, so this catches "the new one lands where an existing one lives" and not "both mis-resolve to a database neither names". Closing that needs the actual database persisted per row; Trip when a registration is connected to a database it does not name (#2228) #2277's tripwire reports it at connect for both in the meantime.
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