Skip to content

Commit 5bcbfdb

Browse files
authored
feat!(spawn-docker-jdk): replace OkHttp and junixsocket with pure JDK HTTP (#11)
1 parent f19ee61 commit 5bcbfdb

File tree

75 files changed

+1651
-1720
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1651
-1720
lines changed

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Spawn is a Java 25 framework for programmatically launching and controlling processes, JVMs, and Docker containers. It provides a unified abstraction (`Platform` / `Application` / `Process`) over different execution environments. The core pattern: define a `Specification`, call `platform.launch(spec)`, get back an `Application` with `CompletableFuture`-based lifecycle hooks.
66

7-
**Stack**: Java 25, Maven, OkHttp3, Jackson, junixsocket, proprietary `build.base.*` and `build.typesystem.injection` libraries from Workday Artifactory
7+
**Stack**: Java 25, Maven, Jackson, junixsocket, proprietary `build.base.*` and `build.codemodel.injection`
88

99
**Structure**: 8 Maven modules in a monorepo, each mapping to a JPMS module:
1010
- `spawn-option` → shared option types
@@ -14,18 +14,18 @@ Spawn is a Java 25 framework for programmatically launching and controlling proc
1414
- `spawn-local-platform` → local OS process launcher (`LocalMachine`)
1515
- `spawn-local-jdk` → JDK detection + `LocalJDKLauncher`
1616
- `spawn-docker` → Docker Engine API interfaces
17-
- `spawn-docker-okhttp`OkHttp-based Docker implementation (slated for replacement with Java HTTP Client)
17+
- `spawn-docker-jdk`JDK HTTP Client-based Docker implementation (uses `java.net.http` + junixsocket)
1818

1919
For detailed architecture, see [docs/CODEBASE_MAP.md](docs/CODEBASE_MAP.md).
2020

2121
## Build
2222

2323
```bash
2424
./mvnw clean install # build all modules + run tests
25-
./mvnw clean install -pl spawn-docker-okhttp # build specific module
25+
./mvnw clean install -pl spawn-docker-jdk # build specific module
2626
```
2727

28-
Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")`. The `spawn-docker-okhttp` module requires `--enable-native-access=ALL-UNNAMED` (configured in surefire).
28+
Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")`. The `spawn-docker-jdk` module requires `--enable-native-access=ALL-UNNAMED` (configured in surefire).
2929

3030
## Key Conventions
3131

docs/CODEBASE_MAP.md

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ graph TB
6161
DockerOptions["Docker option types"]
6262
end
6363
64-
subgraph DockerImpl["spawn-docker-okhttp (OkHttp impl)"]
64+
subgraph DockerImpl["spawn-docker-jdk (JDK impl)"]
6565
AbstractSession
6666
SessionFactories["Session.Factory impls"]
6767
Commands["Command classes"]
@@ -115,7 +115,7 @@ spawn.build/
115115
├── spawn-local-platform/ # JPMS: build.spawn.platform.local — OS process launcher
116116
├── spawn-local-jdk/ # JPMS: build.spawn.platform.local.jdk — JDK detection
117117
├── spawn-docker/ # JPMS: build.spawn.docker — Docker Engine API interfaces
118-
├── spawn-docker-okhttp/ # JPMS: build.spawn.docker.okhttpOkHttp implementation
118+
├── spawn-docker-jdk/ # JPMS: build.spawn.docker.jdkJDK HTTP Client implementation
119119
└── pom.xml # Parent POM; manages versions, Checkstyle, Surefire
120120
```
121121

@@ -335,9 +335,9 @@ Server (listens on spawn:// URI)
335335

336336
---
337337

338-
### `spawn-docker-okhttp`
338+
### `spawn-docker-jdk`
339339

340-
**Purpose:** OkHttp-based concrete implementation of `spawn-docker` interfaces.
340+
**Purpose:** JDK-native concrete implementation of `spawn-docker` interfaces. Uses `java.net.http.HttpClient` for TCP and `java.nio.channels.SocketChannel` with `UnixDomainSocketAddress` for Unix domain sockets. No third-party HTTP dependencies.
341341
**Entry point:** Four `Session.Factory` implementations discovered via `ServiceLoader`, in priority order:
342342
1. `UnixDomainSocketBasedSession.Factory` — Unix socket (`/var/run/docker.sock` or Docker Desktop socket)
343343
2. `LocalHostBasedSessionFactory` — TCP `localhost:2375`
@@ -347,28 +347,25 @@ Server (listens on spawn:// URI)
347347
**Key files:**
348348
| File | Purpose |
349349
|------|---------|
350-
| `AbstractSession.java` | OkHttp client + DI context + event streaming; self-implements `Images`; `Authenticate` on construction |
351-
| `TCPSocketBasedSession.java` | TCP variant; configures OkHttp timeouts; no connection pooling |
352-
| `UnixDomainSocketBasedSession.java` | Unix domain socket via junixsocket; `ConnectedDomainSocket` adapter for OkHttp |
353-
| `command/AbstractCommand.java` | Template method for OkHttp request/response lifecycle |
354-
| `command/AbstractBlockingCommand.java` | Short-lived synchronous commands; infinite timeout |
350+
| `HttpTransport.java` | Thin transport interface; `Request` record + `Response` interface |
351+
| `Http11Parser.java` | HTTP/1.1 response parser for raw socket streams (Content-Length + chunked) |
352+
| `JavaHttpClientTransport.java` | `HttpTransport` impl using `java.net.http.HttpClient` for TCP |
353+
| `UnixSocketHttpTransport.java` | `HttpTransport` impl using `UnixDomainSocketAddress` + `Http11Parser` |
354+
| `AbstractSession.java` | `HttpTransport` + DI context + event streaming; self-implements `Images`; `Authenticate` on construction |
355+
| `TCPSocketBasedSession.java` | TCP variant using `JavaHttpClientTransport` |
356+
| `UnixDomainSocketBasedSession.java` | Unix domain socket variant using `UnixSocketHttpTransport` |
357+
| `command/AbstractCommand.java` | Template method for `HttpTransport` request/response lifecycle |
358+
| `command/AbstractBlockingCommand.java` | Short-lived synchronous commands |
355359
| `command/AbstractNonBlockingCommand.java` | Streaming commands (events, attach); keeps response open |
356360
| `command/AbstractEventBasedBlockingCommand.java` | Subscribes to events before sending request; used by `PullImage` |
357361
| `command/FrameProcessor.java` | Demultiplexes Docker's 8-byte-header binary stream protocol |
358-
| `event/GetSystemEvents.java` | Streaming JSON parser; publishes `StatusEvent`; virtual thread |
359-
| `event/StatusEvent.java` | Docker event with `"status"` field |
360-
| `model/OkHttpBasedContainer.java` | Full `Container` impl; `@PostInject` wires event subscription for `onStart/onExit` |
361-
| `model/OkHttpBasedImage.java` | `Image` impl; `start()` creates then starts container; auto-removes on start failure |
362+
| `event/GetSystemEvents.java` | Streaming JSON parser; publishes `ActionEvent`; virtual thread |
363+
| `model/DockerContainer.java` | Full `Container` impl; `@PostInject` wires event subscription for `onStart/onExit` |
364+
| `model/DockerImage.java` | `Image` impl; `start()` creates then starts container; auto-removes on start failure |
362365
| `model/AbstractJsonBasedResult.java` | DI-injected `Session`, `JsonNode`, `ObjectMapper` for all model classes |
363366

364-
**Why slated for replacement with Java HTTP Client:**
365-
- OkHttp 5.x (Kotlin) pulls in `kotlin.stdlib` as runtime dependency
366-
- Unix domain socket support requires third-party `junixsocket` with native binaries
367-
- Java 16+ has `java.net.UnixDomainSocketAddress`; Java 11+ has `java.net.http.HttpClient`
368-
- The `ConnectedDomainSocket` adapter (200+ LOC boilerplate) would be eliminated entirely
369-
370-
**Known bugs in `spawn-docker-okhttp`:**
371-
- `GetSystemEvents` and `OkHttpBasedContainer` have debug `System.out.println` calls in production code
367+
**Known bugs:**
368+
- `GetSystemEvents` and `DockerContainer` have debug `System.out.println` calls in production code
372369
- `CopyFiles` constructor validation inverts the check (throws when file has content instead of when it's empty)
373370
- `NetworkInformation.driver()` reads lowercase `"driver"` but Docker API returns `"Driver"` → always returns empty string
374371
- `ContainerInformation.links()`: splits on `:``ArrayIndexOutOfBoundsException` if link string has no colon

pom.xml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,8 @@
7373
<byte-buddy.version>1.18.4</byte-buddy.version>
7474
<jackson-core.version>2.21.2</jackson-core.version>
7575
<junit.version>6.0.3</junit.version>
76-
<junixsocket.version>2.10.1</junixsocket.version>
7776
<jakarta-inject.version>2.0.1</jakarta-inject.version>
7877
<mockito.version>5.23.0</mockito.version>
79-
<okhttp.version>4.12.0</okhttp.version>
8078
<codemodel.version>0.19.0</codemodel.version>
8179

8280
<!-- Plugin Dependency Versions -->
@@ -107,7 +105,7 @@
107105
<module>spawn-option</module>
108106

109107
<module>spawn-docker</module>
110-
<module>spawn-docker-okhttp</module>
108+
<module>spawn-docker-jdk</module>
111109
</modules>
112110

113111
<dependencyManagement>
Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
<version>${revision}</version>
1212
</parent>
1313

14-
<artifactId>spawn-docker-okhttp</artifactId>
14+
<artifactId>spawn-docker-jdk</artifactId>
1515

16-
<name>Spawn Docker (OkHttp Client)</name>
16+
<name>Spawn Docker (JDK Client)</name>
1717

1818
<dependencies>
1919
<dependency>
@@ -76,23 +76,6 @@
7676
<version>${codemodel.version}</version>
7777
</dependency>
7878

79-
<dependency>
80-
<groupId>com.kohlschutter.junixsocket</groupId>
81-
<artifactId>junixsocket-common</artifactId>
82-
<version>${junixsocket.version}</version>
83-
</dependency>
84-
85-
<dependency>
86-
<groupId>com.kohlschutter.junixsocket</groupId>
87-
<artifactId>junixsocket-native-common</artifactId>
88-
<version>${junixsocket.version}</version>
89-
</dependency>
90-
91-
<dependency>
92-
<groupId>com.squareup.okhttp3</groupId>
93-
<artifactId>okhttp</artifactId>
94-
<version>${okhttp.version}</version>
95-
</dependency>
9679

9780
<dependency>
9881
<groupId>jakarta.inject</groupId>
@@ -154,18 +137,24 @@
154137
<artifactId>maven-surefire-plugin</artifactId>
155138
<version>${maven-surefire-plugin.version}</version>
156139
<configuration>
157-
<argLine>--enable-native-access=ALL-UNNAMED</argLine>
140+
<argLine>--add-modules jdk.httpserver --add-reads build.spawn.docker.jdk=jdk.httpserver</argLine>
158141
<useSystemClassLoader>true</useSystemClassLoader>
159142
<useManifestOnlyJar>false</useManifestOnlyJar>
160143
</configuration>
161144
</plugin>
145+
<plugin>
146+
<groupId>org.apache.maven.plugins</groupId>
147+
<artifactId>maven-compiler-plugin</artifactId>
148+
<configuration>
149+
<compilerArgs>
150+
<arg>--add-reads=build.spawn.docker.jdk=jdk.httpserver</arg>
151+
</compilerArgs>
152+
</configuration>
153+
</plugin>
162154
<plugin>
163155
<groupId>org.apache.maven.plugins</groupId>
164156
<artifactId>maven-dependency-plugin</artifactId>
165157
<configuration>
166-
<ignoredUnusedDeclaredDependencies>
167-
<unusedDeclaredDependency>com.kohlschutter.junixsocket:junixsocket-native-common</unusedDeclaredDependency>
168-
</ignoredUnusedDeclaredDependencies>
169158
<ignoredNonTestScopedDependencies>
170159
<ignoredNonTestScopedDependency>build.codemodel:codemodel-foundation</ignoredNonTestScopedDependency>
171160
<ignoredNonTestScopedDependency>build.codemodel:jdk-codemodel</ignoredNonTestScopedDependency>

spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/AbstractSession.java renamed to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java

Lines changed: 26 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
package build.spawn.docker.okhttp;
1+
package build.spawn.docker.jdk;
22

33
/*-
44
* #%L
5-
* Spawn Docker (OkHttp Client)
5+
* Spawn Docker (JDK Client)
66
* %%
77
* Copyright (C) 2026 Workday, Inc.
88
* %%
@@ -37,32 +37,27 @@
3737
import build.spawn.docker.Network;
3838
import build.spawn.docker.Networks;
3939
import build.spawn.docker.Session;
40-
import build.spawn.docker.okhttp.command.Authenticate;
41-
import build.spawn.docker.okhttp.command.BuildImage;
42-
import build.spawn.docker.okhttp.command.Command;
43-
import build.spawn.docker.okhttp.command.CreateNetwork;
44-
import build.spawn.docker.okhttp.command.DeleteNetwork;
45-
import build.spawn.docker.okhttp.command.GetSystemEvents;
46-
import build.spawn.docker.okhttp.command.GetSystemInformation;
47-
import build.spawn.docker.okhttp.command.InspectImage;
48-
import build.spawn.docker.okhttp.command.InspectNetwork;
49-
import build.spawn.docker.okhttp.command.PullImage;
50-
import build.spawn.docker.okhttp.model.OkHttpBasedImage;
40+
import build.spawn.docker.jdk.command.Authenticate;
41+
import build.spawn.docker.jdk.command.BuildImage;
42+
import build.spawn.docker.jdk.command.Command;
43+
import build.spawn.docker.jdk.command.CreateNetwork;
44+
import build.spawn.docker.jdk.command.DeleteNetwork;
45+
import build.spawn.docker.jdk.command.GetSystemEvents;
46+
import build.spawn.docker.jdk.command.GetSystemInformation;
47+
import build.spawn.docker.jdk.command.InspectImage;
48+
import build.spawn.docker.jdk.command.InspectNetwork;
49+
import build.spawn.docker.jdk.command.PullImage;
50+
import build.spawn.docker.jdk.model.DockerImage;
5151
import build.spawn.docker.option.DockerAPIVersion;
5252
import build.spawn.docker.option.DockerRegistry;
5353
import build.spawn.docker.option.IdentityToken;
5454
import com.fasterxml.jackson.databind.ObjectMapper;
55-
import okhttp3.HttpUrl;
56-
import okhttp3.OkHttpClient;
5755

5856
import java.nio.charset.StandardCharsets;
5957
import java.nio.file.Path;
6058
import java.util.Base64;
6159
import java.util.Objects;
6260
import java.util.Optional;
63-
import java.util.function.Supplier;
64-
import java.util.logging.Level;
65-
import java.util.logging.Logger;
6661

6762
/**
6863
* An abstract {@link Session} implementation.
@@ -79,9 +74,9 @@ public class AbstractSession
7974
private final Configuration configuration;
8075

8176
/**
82-
* The {@link OkHttpClient}.
77+
* The {@link HttpTransport} for communicating with the Docker Engine.
8378
*/
84-
private final OkHttpClient httpClient;
79+
private final HttpTransport transport;
8580

8681
/**
8782
* The dependency injection {@link Context} to use for creating {@link Command}s.
@@ -115,38 +110,21 @@ public class AbstractSession
115110
private final GetSystemEvents systemEvents;
116111

117112
/**
118-
* Constructs an {@link AbstractSession} using the specified {@link OkHttpClient} and {@link HttpUrl.Builder}
119-
* {@link Supplier}s and provided {@link Configuration}.
120-
* <p>
121-
* The {@link OkHttpClient} {@link Supplier} is used to obtain {@link OkHttpClient}s when required by a
122-
* {@link Session} and more specifically, when {@link Command}s for the {@link Session} need to be
123-
* {@link Command#submit()}ted for execution. {@link Session}s are free to cache and reuse {@link OkHttpClient}s
124-
* for {@link Command}s, so it should not be assumed that a new {@link OkHttpClient} is created per {@link Command}
125-
* or a single {@link OkHttpClient} is used for all {@link Command}s.
126-
* <p>
127-
* The {@link HttpUrl.Builder} {@link Supplier} is used to obtain base {@link HttpUrl}s from which {@link Command}s
128-
* may be constructed for submission with a {@link OkHttpClient}. Each {@link Command} is guaranteed to be
129-
* provided (usually injected) with a new {@link HttpUrl.Builder}, thus allowing per-{@link Command} customization.
113+
* Constructs an {@link AbstractSession} using the specified {@link HttpTransport} and {@link Configuration}.
130114
*
131-
* @param injectionFramework the {@link InjectionFramework} to use for {@link build.codemodel.injection.Dependency} injection
132-
* @param clientSupplier the {@link OkHttpClient} {@link Supplier}
133-
* @param httpUrlBuilderSupplier the {@link HttpUrl.Builder} {@link Supplier}
134-
* @param configuration the {@link Configuration}
115+
* @param injectionFramework the {@link InjectionFramework} to use for {@link build.codemodel.injection.Dependency} injection
116+
* @param transport the {@link HttpTransport} for communicating with the Docker Engine
117+
* @param configuration the {@link Configuration}
135118
*/
136119
@SuppressWarnings("unchecked")
137120
protected AbstractSession(final InjectionFramework injectionFramework,
138-
final Supplier<OkHttpClient> clientSupplier,
139-
final Supplier<HttpUrl.Builder> httpUrlBuilderSupplier,
121+
final HttpTransport transport,
140122
final Configuration configuration) {
141123

142124
Objects.requireNonNull(injectionFramework, "The InjectionFramework must not be null");
143-
Objects.requireNonNull(clientSupplier, "The Supplier<OkHttpClient> must not be null");
144-
Objects.requireNonNull(httpUrlBuilderSupplier, "The Supplier<HttpUrl.Builder> must not be null");
125+
Objects.requireNonNull(transport, "The HttpTransport must not be null");
145126

146-
Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
147-
148-
// establish the HttpClient from the Supplier (allowing us to connect to the Docker Engine)
149-
this.httpClient = clientSupplier.get();
127+
this.transport = transport;
150128

151129
this.configuration = configuration == null
152130
? Configuration.empty()
@@ -173,8 +151,7 @@ protected AbstractSession(final InjectionFramework injectionFramework,
173151

174152
this.context.addResolver(ConfigurationResolver.of(configuration));
175153

176-
this.context.bind(OkHttpClient.class).to(this.httpClient);
177-
this.context.bind(HttpUrl.Builder.class).to(httpUrlBuilderSupplier);
154+
this.context.bind(HttpTransport.class).to(this.transport);
178155
this.context.bind(Session.class).to(this);
179156
this.context.bind(AbstractSession.class).to(this);
180157
this.context.bind((Class) getClass()).to(this);
@@ -249,7 +226,7 @@ protected AbstractSession(final InjectionFramework injectionFramework,
249226
.getEncoder()
250227
.encode(json.getBytes(StandardCharsets.UTF_8)));
251228

252-
return (Authenticator) builder -> builder.addHeader("X-Registry-Auth", encoded);
229+
return (Authenticator) builder -> builder.withHeader("X-Registry-Auth", encoded);
253230
})
254231
.orElse(Authenticator.NONE));
255232

@@ -316,7 +293,7 @@ public Optional<Image> get(final String nameOrId, final Configuration configurat
316293
.inject(new InspectImage(nameOrId, configuration))
317294
.submit()
318295
.map(info -> createContext()
319-
.inject(new OkHttpBasedImage(info.imageId())));
296+
.inject(new DockerImage(info.imageId())));
320297
}
321298

322299
@Override
@@ -351,16 +328,7 @@ public void close() {
351328
// close the reading of System Events
352329
this.systemEvents.close();
353330

354-
// the OkHttpClient doesn't require shutdown to clean up
355-
// but to stop accepting requests / responses we need to stop the internal executor service
356-
this.httpClient
357-
.dispatcher()
358-
.executorService()
359-
.shutdownNow();
360-
361-
this.httpClient
362-
.connectionPool()
363-
.evictAll();
331+
// HttpTransport implementations manage their own connection lifecycle
364332
}
365333

366334
class NetworksImpl

spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/Authenticator.java renamed to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Authenticator.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
package build.spawn.docker.okhttp;
1+
package build.spawn.docker.jdk;
22

33
/*-
44
* #%L
5-
* Spawn Docker (OkHttp Client)
5+
* Spawn Docker (JDK Client)
66
* %%
77
* Copyright (C) 2026 Workday, Inc.
88
* %%
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,21 +20,19 @@
2020
* #L%
2121
*/
2222

23-
import okhttp3.Request;
24-
25-
import java.util.function.Function;
23+
import java.util.function.UnaryOperator;
2624

2725
/**
28-
* A {@link Function} to configure authentication for a {@link Request.Builder}.
26+
* A {@link UnaryOperator} to configure authentication on an {@link HttpTransport.Request}.
2927
*
3028
* @author brian.oliver
3129
* @since Aug-2022
3230
*/
3331
public interface Authenticator
34-
extends Function<Request.Builder, Request.Builder> {
32+
extends UnaryOperator<HttpTransport.Request> {
3533

3634
/**
3735
* An {@link Authenticator} that performs no authentication.
3836
*/
39-
Authenticator NONE = builder -> builder;
37+
Authenticator NONE = request -> request;
4038
}

0 commit comments

Comments
 (0)