Subject: [pgjdbc/pgjdbc] PR #4036: Add DNS SRV discovery via jdbc:postgresql+srv:// URL scheme

From: "x4m (@x4m)" <[email-masked]>
Date: Tue, 21 Apr 2026 18:38:42 +0000

## 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.<cluster>` 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.<srvHost>` 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=<nameserver-ip>` 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=[email-masked]
- MongoDB `+srv` scheme for comparison: https://www.mongodb.com/docs/manual/reference/connection-string/#dns-seed-list-connection-format

---

From: "vlsi (@vlsi)" <[email-masked]>
Date: Wed, 22 Apr 2026 05:54:08 +0000

Thanks for the suggestion.
I am leaning towards a pluggable approach for the discovery: https://github.com/pgjdbc/pgjdbc/issues/3367
Have you explored the plugin approach so SRV becomes one of the possibilities?

---

From: "x4m (@x4m)" <[email-masked]>
Date: Wed, 22 Apr 2026 06:26:13 +0000

The plugin API from #3367 doesn't exist yet, so adopting it would block this feature. More importantly, SRV is orthogonal to what #3367 is designed for: dynamic load balancing, connection draining, per-host timeouts, and lifecycle callbacks (registerSuccess, registerFailure, registerDisconnect). SRV needs none of that - it's a one-shot host expansion at connect time, backed by an IETF standard (RFC 2782). I'm proposing this across the PostgreSQL driver ecosystem (pgx, npgsql, libpq) for the same reason: it's a standard building block that belongs to a driver.