Skip to content

feat: Close a client, which ends everything it is doing at once. - #12

Merged
goloroden merged 1 commit into
parse-lines-oncefrom
close-client
Oct 8, 2026
Merged

goloroden merged 1 commit into
parse-lines-oncefrom
close-client

Conversation

@goloroden

Copy link
Copy Markdown
Member

A client had no close(). The threads of its HTTP client lived on until garbage collection, and there was no way to end what it was doing, e.g. observers that are still running when an application shuts down.

What changes:

  • Client implements AutoCloseable, so it works with try-with-resources, and Spring calls close() on shutdown.
  • close() ends everything at once. Requests that are running and streams that are open, whether they have started or not, end with a CancellationException, as if they had been aborted.
  • Every request afterwards throws an IllegalStateException ("client is closed") without being sent, as is the convention for a closed resource in Java.
  • The client closes the HTTP client it created itself, but not one it was given, since that one belongs to the caller.
  • Closing a closed client does nothing.
  • A request registers under the same lock under which close() marks the client as closed. Either close() sees the request and ends it, or the request sees that the client is closed.
  • The README describes how to close the client.

The last PR of the series on the API before the first release. Builds on #11.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz

A client had no close(), so the threads of its HTTP client lived on
until garbage collection, and there was no way to end what it was doing
when an application shut down.

Client now implements AutoCloseable. Closing it ends requests that are
running and streams that are open, whether they have started or not,
with a CancellationException, as if they had been aborted. Every request
afterwards throws an IllegalStateException without being sent. The
client closes the HTTP client it created, but not one it was given,
since that one belongs to the caller. A request registers under the
same lock under which close() marks the client as closed, so that no
request slips through in between.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz
@goloroden
goloroden requested a review from a team as a code owner October 8, 2026 05:55
@goloroden goloroden self-assigned this Oct 8, 2026
@goloroden
goloroden merged commit 6734e48 into parse-lines-once Oct 8, 2026
2 checks passed
@goloroden
goloroden deleted the close-client branch October 8, 2026 07:08
goloroden added a commit that referenced this pull request Oct 8, 2026
…nt less CPU per event. (#11)

* chore: Move the SDK to the namespace io.thenativeweb, as group ID and as Java package.

Wherever a registry requires a namespace, the client SDKs and the image
of EventSourcingDB use thenativeweb: Packagist, Go modules, Docker Hub.
Maven requires one, so the artifacts are now io.thenativeweb:eventsourcingdb
and io.thenativeweb:eventsourcingdb-testcontainers. The Java packages
follow the group ID, as is the convention in Java, and are now
io.thenativeweb.eventsourcingdb and its testcontainers subpackage.

Nothing has been published yet, so no caller is affected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz

* feat: Annotate the public API with JSpecify, and check the code with NullAway.

Java can not say whether a value may be null. Both packages are now
@NullMarked, so everything is non-null unless it is marked @nullable,
as the optional fields of EventCandidate and of the options are.
Kotlin reads these annotations, and Spring Boot 4 uses them, too.

NullAway checks the code against the annotations as part of the build,
and runs as the only check of Error Prone. Making LineStream pass it
made explicit that it reads only once it has opened its stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz

* feat: Let callers hand over the HTTP client, and give up connecting after 10 seconds by default.

The client created an HTTP client of its own that nobody could
configure, without a timeout for connecting, a proxy, or TLS. An
unreachable server left a request waiting for the timeout of the
operating system.

A Client now takes ClientOptions, whose HTTP client it sends requests
with if one is given. It does not close that HTTP client, since it
belongs to the caller. The HTTP client it creates otherwise gives up
connecting after 10 seconds. There is deliberately no timeout for whole
requests, since it would end every stream that observes events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz

* feat: Let callers hand over the JSON mapper for the data of events.

The client serialized and deserialized the data of events with a JSON
mapper of its own, so the configuration of an application, e.g. its
modules or its naming strategy, did not apply to its events.

ClientOptions now takes a data mapper, which the client uses to write
the data of events and in Event.data(Class). It applies to the data of
events only: the client keeps reading everything else the server sends
with its own mapper, so that the configuration of an application can
not break it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz

* feat: Run an EventQL query whose rows are deserialized into a given type.

runEventQlQuery returned every row as a JsonNode, so callers converted
each row themselves. It now also takes the type to deserialize each row
into, e.g. a record that matches the projection, and uses the data
mapper of the client for that, since rows carry the data of the
application. A row that does not match the type ends the stream with
the exception of the mapper. Errors that the server reports stay text,
as in the other client SDKs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz

* fix: Read each line of a stream only once, which takes about a sixth less CPU per event.

To keep the data of an event as the server wrote it, the client parsed
each line of a stream four times: the line as a tree, the line again to
find its payload, the payload as a tree, and the payload again to find
its data. It now reads each line in a single pass, which builds the
tree and cuts out the text of the data along the way. The same reader
handles the events that writing returns.

Measured against the same server, with 20,000 events and the median of
15 rounds, the CPU time of the client per event dropped from about 16.0
to 13.7 microseconds. The OpenCQRS client took about 11.6 to 12.2 in the
same runs. The throughput stays the same, since the server bounds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz

* feat: Close a client, which ends everything it is doing at once. (#12)

A client had no close(), so the threads of its HTTP client lived on
until garbage collection, and there was no way to end what it was doing
when an application shut down.

Client now implements AutoCloseable. Closing it ends requests that are
running and streams that are open, whether they have started or not,
with a CancellationException, as if they had been aborted. Every request
afterwards throws an IllegalStateException without being sent. The
client closes the HTTP client it created, but not one it was given,
since that one belongs to the caller. A request registers under the
same lock under which close() marks the client as closed, so that no
request slips through in between.


Claude-Session: https://claude.ai/code/session_01Hc2MPSLu7HDHTm8HiBsrDz

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant