Message-ID: From: "vlsi (@vlsi)" To: "pgjdbc/pgjdbc" Date: Mon, 15 Jun 2026 18:48:30 +0000 Subject: [pgjdbc/pgjdbc] PR #4188: feat: built-in Unix domain socket factory for Java 17+ List-Id: X-GitHub-Additions: 695 X-GitHub-Author-Id: 213894 X-GitHub-Author-Login: vlsi X-GitHub-Base: master X-GitHub-Changed-Files: 14 X-GitHub-Commits: 1 X-GitHub-Deletions: 6 X-GitHub-Draft: true X-GitHub-Head-Branch: claude/recursing-dubinsky-4a4eef X-GitHub-Head-SHA: 462305c3ff383a85fb7e5c4237a720750376ee7b X-GitHub-Issue: 4188 X-GitHub-Labels: enhancement X-GitHub-Merge-SHA: 211aa59b4e18aaa6683d01af2e885d9aeee1ace9 X-GitHub-Repo: pgjdbc/pgjdbc X-GitHub-State: open X-GitHub-Type: pull_request X-GitHub-Url: https://github.com/pgjdbc/pgjdbc/pull/4188 Content-Type: text/plain; charset=utf-8 ## Why [Issue #3457](https://github.com/pgjdbc/pgjdbc/issues/3457) asks the driver to use `java.net.UnixDomainSocketAddress` (Java 16+) so local connections can go over a Unix domain socket without a third-party dependency such as junixsocket. ## What - Add `org.postgresql.unixsocket.UnixDomainSocketFactory`, packaged in the multi-release JAR so it activates automatically on Java 17 and later. - The base class (Java 8) is a stub that fails fast with a translatable `GT.tr` message. - The working implementation lives under `META-INF/versions/17`. `UnixDomainSocket extends Socket` re-exposes a Unix NIO `SocketChannel` through the slice of the `Socket` API that `PGStream` uses: timed reads and writes via selectors (so `socketTimeout` and async-notify peeks keep working), plus the buffer and keep-alive accessors. TCP-only options such as `TCP_NODELAY` are accepted and ignored. - Wire the `java17` source set into the Gradle build (`addMultiReleaseContents()` for both `jar` and `shadowJar`), and add a matching `jdkge17` profile to `reduced-pom.xml` so the Maven source-distribution build also produces `META-INF/versions/17`. - docker-compose: a `DOMAIN_SOCKET` toggle bind-mounts the server socket directory to the host and widens permissions so host-side tests can connect. - CI: a `domain_socket` matrix axis (Linux, Java 17+) routes every test connection through the factory via `-Ddomain_socket=` in `TestUtil`. - Docs and changelog updated. Usage: ``` jdbc:postgresql://localhost/test?socketFactory=org.postgresql.unixsocket.UnixDomainSocketFactory&socketFactoryArg=/var/run/postgresql ``` `socketFactoryArg` is the socket directory; the port from the URL selects `.s.PGSQL.`. The host is ignored. ## How to verify - `META-INF/versions/17` layout confirmed in the Gradle `jar`/`shadowJar` and in the Maven jar built from `:postgresql:sourceDistribution` (with `Multi-Release: true`). - End-to-end against PostgreSQL 16 over a Unix socket on Java 21: `SELECT 1`, `version()`, `LISTEN`, and `socketTimeout=10` all succeed. (On macOS the socket must be reached from inside the Docker VM, since Docker Desktop bind mounts cannot carry an `AF_UNIX` connection to the host; this works directly on Linux CI.) - `checkstyle`, `forbidden-apis`, and `autostyle` pass for the new sources. - `UnixDomainSocketFactoryTest` covers the path in CI when the `domain_socket` axis is active and is skipped otherwise. Closes #3457 🤖 Generated with [Claude Code](https://claude.com/claude-code) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eb3692405d..939271cea5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -164,15 +164,23 @@ jobs: CREATE_REPLICAS: ${{ matrix.replication }} PG_IMAGE: ${{ matrix.pg_version == 'HEAD' && 'pgjdbc/postgres-devel:ci-head' || '' }} PG_PULL_POLICY: ${{ matrix.pg_version == 'HEAD' && 'build' || 'missing' }} + DOMAIN_SOCKET: ${{ matrix.domain_socket }} # The below run command is long, however, it is intentional, and it makes the output nicer in GitHub UI # language=bash run: | - echo "Starting PostgreSQL via docker compose down; PGV=$PGV TZ=$TZ XA=$XA SSL=$SSL SCRAM=$SCRAM CREATE_REPLICAS=$CREATE_REPLICAS docker compose up" + echo "Starting PostgreSQL via docker compose down; PGV=$PGV TZ=$TZ XA=$XA SSL=$SSL SCRAM=$SCRAM CREATE_REPLICAS=$CREATE_REPLICAS DOMAIN_SOCKET=$DOMAIN_SOCKET docker compose up" - docker compose down -v --rmi local || true + # Expose the server's Unix domain socket to the host only for domain_socket runs, via a + # compose override that bind-mounts a host directory outside the repository. + COMPOSE_FILES="-f docker-compose.yml" + if [ "$DOMAIN_SOCKET" = "yes" ]; then + COMPOSE_FILES="$COMPOSE_FILES -f docker-compose.domain-socket.yml" + mkdir -p /tmp/pgjdbc-postgresql-socket + fi + docker compose $COMPOSE_FILES down -v --rmi local || true sed -i -r '/- (543[3-4]):\1/d' docker-compose.yml - docker compose up -d --wait || { docker compose logs; exit 1; } - docker compose logs + docker compose $COMPOSE_FILES up -d --wait || { docker compose $COMPOSE_FILES logs; exit 1; } + docker compose $COMPOSE_FILES logs - name: Start PostgreSQL PGV=${{ matrix.pg_version }} if: ${{ runner.os != 'Linux' }} uses: ikalnytskyi/action-setup-postgres@c4dda34aae1c821e3a771b68b73b13af3198a7ee # v8 diff --git a/.github/workflows/matrix.mjs b/.github/workflows/matrix.mjs index c851ccb1b2..c2976ab934 100644 --- a/.github/workflows/matrix.mjs +++ b/.github/workflows/matrix.mjs @@ -193,6 +193,17 @@ matrix.addAxis({ ] }); +// Connect over a Unix domain socket via org.postgresql.unixsocket.UnixDomainSocketFactory. +// Requires Java 17+ (multi-release JAR) and a Unix host, so it is constrained below. +matrix.addAxis({ + name: 'domain_socket', + title: x => x.value === 'yes' ? 'domain_socket' : '', + values: [ + {value: 'yes', weight: 3}, + {value: 'no', weight: 10}, + ] +}); + matrix.addAxis({ name: 'autosave', title: x => x.value === 'never' ? '' : 'autosave ' + x.value, @@ -269,7 +280,7 @@ function lessThan(minVersion) { matrix.setNamePattern([ 'java_version', 'java_distribution', 'pg_version', 'query_mode', 'scram', 'ssl', 'hash', 'os', 'server_tz', 'tz', 'locale', - 'check_anorm_sbt', 'gss', 'replication', 'slow_tests', + 'check_anorm_sbt', 'gss', 'domain_socket', 'replication', 'slow_tests', 'adaptive_fetch', 'rewrite_batch_inserts', 'query_timeout', 'autosave', 'cleanupSavepoints', 'cpu_count' ]); @@ -290,6 +301,17 @@ matrix.imply({java_distribution: {value: 'oracle'}}, {java_version: v => v === e // TODO: Semeru does not ship Java 21 builds yet matrix.exclude({java_distribution: {value: 'semeru'}, java_version: '21'}) matrix.imply({gss: {value: 'yes'}}, {os: {value: 'ubuntu-latest'}}) +// Unix domain sockets need Java 16+ (we ship the implementation in META-INF/versions/17) and a +// Unix host where the server socket can be bind-mounted from the container. +matrix.imply({domain_socket: {value: 'yes'}}, {os: {value: 'ubuntu-latest'}}) +matrix.imply({domain_socket: {value: 'yes'}}, {java_version: v => v === eaJava || v >= 17}) +// GSS encryption negotiates over TCP and is not used for local Unix socket connections +matrix.exclude({domain_socket: {value: 'yes'}, gss: {value: 'yes'}}) +// unix_socket_directories (plural) was introduced in PostgreSQL 9.3; 9.1/9.2 use the singular GUC +matrix.exclude({domain_socket: {value: 'yes'}, pg_version: lessThan('9.3')}) +// The replication host-chooser tests (MultiHostsConnectionTest) compare inet_server_addr() across +// TCP hosts, which is null over a Unix socket, so keep those runs on TCP +matrix.exclude({domain_socket: {value: 'yes'}, replication: {value: 'yes'}}) // ikalnytskyi/action-setup-postgres supports PostgreSQL 14+ only matrix.exclude({os: {value: ['windows-latest', 'macos-latest']}, pg_version: lessThan('14')}); // HEAD is built from pgdg-snapshot inside Docker, which only runs on Linux. @@ -319,6 +341,7 @@ matrix.generateRow({java_version: matrix.axisByName.java_version.values.slice(-2 // Ensure we test all query_mode values matrix.ensureAllAxisValuesCovered('query_mode'); matrix.ensureAllAxisValuesCovered('gss'); +matrix.ensureAllAxisValuesCovered('domain_socket'); matrix.ensureAllAxisValuesCovered('xa'); matrix.ensureAllAxisValuesCovered('ssl'); matrix.ensureAllAxisValuesCovered('replication'); @@ -365,6 +388,7 @@ include.forEach(v => { v.slow_tests = v.slow_tests.value; v.xa = v.xa.value; v.gss = v.gss.value; + v.domain_socket = v.domain_socket.value; v.ssl = v.ssl.value; v.scram = v.scram.value; v.check_anorm_sbt = v.check_anorm_sbt.value; @@ -451,6 +475,10 @@ include.forEach(v => { if (v.gss === 'no') { testJvmArgs.push('-DskipGssEncryption=true'); } + if (v.domain_socket === 'yes') { + // The docker-compose override bind-mounts the server socket directory to this host path + testJvmArgs.push('-Ddomain_socket=/tmp/pgjdbc-postgresql-socket'); + } if (v.cpu_count.value === '1') { // Constrains ForkJoinPool common pool to a single worker, exposing FJP submit/compensation // overhead in code paths like LazyCleaner. See https://github.com/pgjdbc/pgjdbc/issues/4037 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1429474664..58f095ffb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Security ### Added +* feat: add a built-in Unix domain socket factory `org.postgresql.unixsocket.UnixDomainSocketFactory`, built on `java.net.UnixDomainSocketAddress` and shipped in the multi-release JAR so it activates automatically on Java 17 and later. Set `socketFactory=org.postgresql.unixsocket.UnixDomainSocketFactory` and `socketFactoryArg=` (e.g. `/var/run/postgresql`) to connect over a Unix socket without an extra dependency such as junixsocket [Issue #3457](https://github.com/pgjdbc/pgjdbc/issues/3457) * feat: invalidate the prepared-statement cache after CREATE/DROP/ALTER so callers no longer trip on "cached plan must not change result type" without opting into `autosave=ALWAYS`. Controlled by the new `flushCacheOnDdl` connection property (default `true`); set to `false` for the prior behaviour. * feat: add `connectThreadFactory` connection property to customize the `ThreadFactory` used to spawn the worker thread that runs the connection attempt when `loginTimeout` is in effect. The value is the fully qualified name of a class implementing `java.util.concurrent.ThreadFactory`. With a null value, the default, the driver retains the prior behavior of using a daemon thread named `"PostgreSQL JDBC driver connection thread"`. Useful for testing timeout behaviour or for applications that want detailed control of all driver-created threads. diff --git a/docker/postgres-server/docker-compose.domain-socket.yml b/docker/postgres-server/docker-compose.domain-socket.yml new file mode 100644 index 0000000000..a311494fa4 --- /dev/null +++ b/docker/postgres-server/docker-compose.domain-socket.yml @@ -0,0 +1,10 @@ +# Compose override that exposes the server's Unix domain socket directory to the host, so tests can +# connect via org.postgresql.unixsocket.UnixDomainSocketFactory. It is applied only for the +# domain_socket CI runs (docker compose -f docker-compose.yml -f docker-compose.domain-socket.yml). +# The host path lives outside the repository so that Docker creating it as root does not interfere +# with the Gradle build directory. The entrypoint (DOMAIN_SOCKET=yes) widens the socket permissions +# so the host user can reach it. +services: + pgdb: + volumes: + - ${PG_SOCKET_DIR:-/tmp/pgjdbc-postgresql-socket}:/var/run/postgresql diff --git a/docker/postgres-server/docker-compose.yml b/docker/postgres-server/docker-compose.yml index e47b527fb1..9e10ed871e 100644 --- a/docker/postgres-server/docker-compose.yml +++ b/docker/postgres-server/docker-compose.yml @@ -31,6 +31,7 @@ services: - AUTO_VACUUM=${AUTO_VACUUM:-no} - TRACK_COUNTS=${TRACK_COUNTS:-no} - CREATE_REPLICAS=${CREATE_REPLICAS:-no} + - DOMAIN_SOCKET=${DOMAIN_SOCKET:-no} - POSTGRES_USER=postgres - POSTGRES_PASSWORD= - POSTGRES_DB=postgres diff --git a/docker/postgres-server/scripts/entrypoint.sh b/docker/postgres-server/scripts/entrypoint.sh index ccb5c469f5..f125eff388 100755 --- a/docker/postgres-server/scripts/entrypoint.sh +++ b/docker/postgres-server/scripts/entrypoint.sh @@ -58,6 +58,20 @@ main () { pg_opts="${pg_opts} -c max_prepared_transactions=64" fi + if is_option_enabled "${DOMAIN_SOCKET:-no}"; then + # The socket directory is bind-mounted from the host so that tests running on the host can + # connect via a Unix domain socket. Make it writable by postgres and world-accessible so the + # host user (a different uid) can connect to the socket. + mkdir -p /var/run/postgresql + chown postgres:postgres /var/run/postgresql + chmod 0777 /var/run/postgresql + add_pg_opt "-c unix_socket_directories=/var/run/postgresql" + add_pg_opt "-c unix_socket_permissions=0777" + # Allow local (Unix socket) connections for the unprivileged test user. The default + # pg_hba.conf only grants local access to the postgres superuser. + echo "local all all trust" >> "${pg_hba}" + fi + if is_pg_version_less_than "10"; then add_pg_opt "-c max_locks_per_transaction=256" fi diff --git a/docs/content/documentation/use.md b/docs/content/documentation/use.md index b87d221132..52f84f9857 100644 --- a/docs/content/documentation/use.md +++ b/docs/content/documentation/use.md @@ -482,6 +482,25 @@ A value of zero disables this check. ### Unix sockets +On Java 17 and later the driver ships a built-in Unix domain socket factory, so no extra dependency +is needed. It is built on `java.net.UnixDomainSocketAddress` and packaged in the multi-release JAR, +so it activates automatically when the driver runs on a supported runtime. + +Add `?socketFactory=org.postgresql.unixsocket.UnixDomainSocketFactory&socketFactoryArg=[socket-directory]` +to the connection URL, where `socketFactoryArg` is the directory that holds the server socket (for +example `/var/run/postgresql`). The driver appends `.s.PGSQL.` to that directory, so the port +in the URL still selects the socket file. You may also point `socketFactoryArg` directly at a socket +file. The host part of the URL is ignored, so `localhost` is a convenient placeholder: + +``` +jdbc:postgresql://localhost/test?socketFactory=org.postgresql.unixsocket.UnixDomainSocketFactory&socketFactoryArg=/var/run/postgresql +``` + +On Java 16 and earlier the built-in factory is unavailable and the constructor fails fast; use +junixsocket instead (below). + +#### junixsocket + By adding junixsocket you can obtain a socket factory that works with the driver. Code can be found [here](https://github.com/kohlschutter/junixsocket) and instructions [here](https://kohlschutter.github.io/junixsocket/dependency.html) diff --git a/pgjdbc/build.gradle.kts b/pgjdbc/build.gradle.kts index 2f1a281281..a69d5d090e 100644 --- a/pgjdbc/build.gradle.kts +++ b/pgjdbc/build.gradle.kts @@ -54,6 +54,15 @@ val java11 by sourceSets.creating { compileClasspath += sourceSets.main.get().output } +// Create a separate source set for Java 17+ specific code (e.g., Unix domain sockets) +val java17 by sourceSets.creating { + java { + srcDir("src/main/java17") + } + // Make java17 source set depend on main source set (to access PGProperty and friends) + compileClasspath += sourceSets.main.get().output +} + if (buildParameters.testJdkVersion >= 11) { // By default, Gradle uses "test classes" dir for classpath, so multi-release jar is not used there // So we explicitly prepend the classpath with Java 11 classes @@ -62,6 +71,13 @@ if (buildParameters.testJdkVersion >= 11) { } } +if (buildParameters.testJdkVersion >= 17) { + // Same as above: make the Java 17 multi-release classes visible to tests + tasks.test { + classpath = java17.output + classpath + } +} + // Configure the java11 source set to compile with Java 11 tasks.named(java11.compileJavaTaskName) { options.release.set(11) @@ -69,10 +85,20 @@ tasks.named(java11.compileJavaTaskName) { dependsOn(tasks.compileJava) } +// Configure the java17 source set to compile with Java 17 +tasks.named(java17.compileJavaTaskName) { + options.release.set(17) + // Ensure main classes are compiled before java17 classes + dependsOn(tasks.compileJava) +} + fun CopySpec.addMultiReleaseContents() { into("META-INF/versions/11") { from(java11.output) } + into("META-INF/versions/17") { + from(java17.output) + } } // Add java11 compiled classes to the main JAR @@ -139,6 +165,7 @@ dependencies { implementation("org.checkerframework:checker-qual:3.55.1") java11.implementationConfigurationName("org.checkerframework:checker-qual:3.55.1") + java17.implementationConfigurationName("org.checkerframework:checker-qual:3.55.1") testKitSourcesWithoutAnnotations(projects.testkit) diff --git a/pgjdbc/reduced-pom.xml b/pgjdbc/reduced-pom.xml index 4d15172bd3..f9a471201d 100644 --- a/pgjdbc/reduced-pom.xml +++ b/pgjdbc/reduced-pom.xml @@ -201,6 +201,43 @@ + + jdkge17 + + [17,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + compile-java17 + compile + + compile + + + 17 + + ${project.basedir}/src/main/java17 + + true + + + + + + +