Message-ID: From: "x4m (@x4m)" To: "pgjdbc/pgjdbc" Date: Tue, 21 Apr 2026 18:38:42 +0000 Subject: [pgjdbc/pgjdbc] PR #4036: Add DNS SRV discovery via jdbc:postgresql+srv:// URL scheme List-Id: X-GitHub-Author-Id: 6000069 X-GitHub-Author-Login: x4m X-GitHub-Issue: 4036 X-GitHub-Repo: pgjdbc/pgjdbc X-GitHub-State: open X-GitHub-Type: pull_request X-GitHub-Url: https://github.com/pgjdbc/pgjdbc/pull/4036 Content-Type: text/plain; charset=utf-8 ## Add DNS SRV discovery via `jdbc:postgresql+srv://` URL scheme ### Problem When running a replicated PostgreSQL cluster, clients need to know the address of every node. Today the only way to express this in a JDBC connection string is a hard-coded comma-separated host list: ``` jdbc:postgresql://pg1.example.com,pg2.example.com,pg3.example.com/mydb?targetServerType=primary ``` This creates operational coupling: every time a node is added, removed, or replaced, the connection string in every application must be updated and redeployed. Managed database providers and HA tooling (Patroni, pg_auto_failover, etc.) cannot change the cluster topology without coordinating with application teams. ### Solution DNS SRV records ([RFC 2782](https://datatracker.ietf.org/doc/html/rfc2782)) were designed exactly for this: a single name that maps to an ordered, weighted list of `(host, port)` endpoints. MongoDB adopted `mongodb+srv://` for the same reason. This PR adds a `jdbc:postgresql+srv://` URL scheme (and a `srvhost=` connection property) that tells the driver to resolve `_postgresql._tcp.` at connect time and feed the returned targets — sorted by priority then weight per RFC 2782 — into the existing multi-host `HostChooser` / `MultiHostChooser` pipeline: ```java // Instead of hard-coding every node: Connection conn = DriverManager.getConnection( "jdbc:postgresql+srv://cluster.example.com/mydb?targetServerType=primary", "user", "password"); ``` DNS at the provider's side: ``` _postgresql._tcp.cluster.example.com. SRV 10 1 5432 pg1.example.com. _postgresql._tcp.cluster.example.com. SRV 10 1 5432 pg2.example.com. _postgresql._tcp.cluster.example.com. SRV 20 1 5433 pg-replica.example.com. ``` Adding or removing a node now requires only a DNS record change — no application config or restart. ### How it integrates with the existing multi-host machinery The SRV-resolved `HostSpec[]` is passed directly into `ConnectionFactoryImpl.openConnectionImpl()` unchanged. Every existing feature continues to work without modification: | Feature | Works with SRV | |---|---| | `targetServerType=primary/secondary/...` | ✓ | | `loadBalanceHosts=true` | ✓ | | `hostRecheckSeconds` | ✓ | | `GlobalHostStatusTracker` | ✓ | | `connectTimeout` | ✓ | | SSL / `sslmode` | ✓ | ### API **New `PGProperty`:** ```java PGProperty.SRV_HOST // key: "srvhost" ``` **Supported URL forms:** ``` # +srv scheme (recommended) jdbc:postgresql+srv://cluster.example.com/mydb?targetServerType=primary&sslmode=require # srvhost= connection property jdbc:postgresql:mydb?srvhost=cluster.example.com&targetServerType=primary ``` `srvhost` is mutually exclusive with an explicit host in the URL authority. Specifying both is rejected with a warning during `parseURL()`. ### Implementation **`SRVLookup` (new class)** — resolves `_postgresql._tcp.` using JNDI DNS (`javax.naming.directory.DirContext`), which ships with every JDK since 1.3. No new dependencies are added. ```java // DNS query: _postgresql._tcp.cluster.example.com IN SRV // JNDI returns each record as the string "priority weight port target" // Records are sorted: priority ASC, weight DESC (RFC 2782) HostSpec[] specs = SRVLookup.resolve("cluster.example.com"); ``` **`Driver.parseURL()`** — detects the `+srv` scheme prefix before normal parsing, strips it, and records the SRV domain in `PGProperty.SRV_HOST`. Also accepts `srvhost=` as a query parameter. **`Driver.hostSpecs()`** — if `SRV_HOST` is set, delegates to `SRVLookup.resolve()` instead of the normal comma-split logic. ### Testing **Unit tests (no database, no DNS server):** | Test | What it covers | |---|---| | `parseURLSrvScheme` | `jdbc:postgresql+srv://` sets `SRV_HOST` | | `parseURLSrvSchemeWithUser` | `+srv` plus query params parses correctly | | `parseURLSrvKeyword` | `srvhost=` property sets `SRV_HOST` | | `parseURLSrvAndHostMutuallyExclusive` | returns null when both host and srvhost are given | | `parseURLNormalUnchanged` | plain JDBC URLs are unaffected | | `parseAndSortByPriorityAscending` | lower priority number wins | | `parseAndSortByWeightDescendingWithinPriority` | higher weight wins within same priority | | `parseAndSortStripsTrailingDot` | FQDN trailing dot is normalised | | `parseAndSortMixedPriorityAndWeight` | 4-record ordering matches mmatvei.ru real data | | `parseAndSortEmptyListThrows` | error on empty record list | **Live DNS test (no database required):** `testResolveSRVLive` queries four real public SRV records at `_postgresql._tcp.mmatvei.ru` and verifies RFC 2782 priority ordering end-to-end: ``` [0] pg4.mmatvei.ru:5432 (priority 96) [1] pg3.mmatvei.ru:5432 (priority 97) [2] pg2.mmatvei.ru:5432 (priority 99) [3] pg.mmatvei.ru:5432 (priority 100) ``` If the system resolver has a stale negative cache, set `PGJDBC_TEST_SRV_DNS_SERVER=` to query a specific authoritative server. The test skips automatically when the domain is unreachable, so it never breaks an offline build. ### Prior art and related discussion - pgjdbc issue discussing multi-host improvements: https://github.com/pgjdbc/pgjdbc/issues/1870 - pgx SRV PR (same feature for the Go driver): https://github.com/jackc/pgx/pull/2538 - Original libpq proposal (2019): https://www.postgresql.org/message-id/CAK_s-G2_3S09_EA+nRxxefMW+0-UwKE=Uj6bCdBpPncPVRpM_g@mail.gmail.com - MongoDB `+srv` scheme for comparison: https://www.mongodb.com/docs/manual/reference/connection-string/#dns-seed-list-connection-format