Skip to content

[java] Move worker lifecycle to Client and RunHandle - #1555

Merged
brandur merged 3 commits into
masterfrom
brandur-java-client-lifecycle
Oct 27, 2026
Merged

brandur merged 3 commits into
masterfrom
brandur-java-client-lifecycle

Conversation

@brandur

@brandur brandur commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Move worker configuration and startup onto Client. Workers now holds typed handler registrations, and Client.start() returns an AutoCloseable RunHandle with stop(), stopAndCancel(), and runtime queue/periodic controls.

var workers = new Workers().add(email, context -> sendEmail(context.args()));
var client = Client.builder(database).queue("default", 10).workers(workers).build();
try (var run = client.start()) {
  application.awaitStop();
}

Subscriptions and event types also move to Client, so callers can subscribe before startup and retain subscriptions across restarts. A client permits only one active run, including while a timed-out stop drains. Successful shutdown allows restart with the same client ID; stopping an old handle cannot affect a later run. Insert-only clients still need no lifecycle calls.

Update the existing runtime tests, conformance adapter, examples, API inventory, and changelog. Add regression coverage for concurrent starts, startup failure recovery, registry snapshots, local insert wakeups, subscriptions, restarts, and both stop modes.

Validation:

  • make lint/java test/java/tools test/java/sqlite check/java/package
  • make test/java/postgres against a disposable local database
  • Compiled and ran the README quick start against SQLite
  • Exercised the conformance adapter with pre-start jobs, completion events, and two start/stop cycles

Put runtime configuration and startup on Client, consistent with the
other River clients. Workers becomes a registry of typed handlers, while
Client.start() returns an AutoCloseable RunHandle for one execution.
Shutdown and runtime queue/periodic controls belong to that handle.

For example:

    var workers = new Workers().add(email, context -> sendEmail(context.args()));
    var client = Client.builder(database)
        .queue("default", 10)
        .workers(workers)
        .build();
    try (var run = client.start()) {
      application.awaitStop();
    }

RunHandle.stop() and close() drain active attempts; stopAndCancel() also
requests cancellation. A client permits only one active run, including
while a timed-out stop drains. Successful shutdown allows restart with
the same client ID, and stopping an old handle cannot affect a later run.
Insert-only clients still need no lifecycle calls, and the application
continues to own its database pool.

Move subscriptions and event types to Client so callers can subscribe
before startup without missing immediately completed jobs. Subscriptions
remain registered across restarts until closed. Registries and builders
are snapshotted, and each restart restores configured queues and periodic
jobs rather than carrying over changes made to the previous run.

Update the conformance adapter, examples, API inventory, and changelog,
including a migration guide from 0.1.0. Add regression coverage for
concurrent starts, startup failure recovery, registry snapshots, local
insert wakeups, subscriptions, restarts, and both stop modes.

Validation:
- make lint/java test/java/tools test/java/sqlite check/java/package
- make test/java/postgres against a disposable local database
- Compile and run the README quick start against SQLite
- Exercise the conformance adapter with pre-start jobs, completion
  events, and two start/stop cycles
- Java CI on Java 21/25 and Postgres 14-18
A built client stores its effective rescueAfter value, including the
default derived from jobTimeout. Copying that value through toBuilder()
treated it as an explicit override, so increasing the job timeout could
incorrectly fail validation. For example, copying a default client and
setting a two-hour timeout retained the one-hour rescue delay:

    client.toBuilder().jobTimeout(Duration.ofHours(2)).build();

Track whether rescueAfter was explicitly configured. Recompute an
implicit delay when building the copied configuration; the example now
gets the expected three-hour delay. Preserve explicit overrides and
continue rejecting values shorter than the job timeout.

Add a regression test covering both implicit and explicit rescue delays.
Validated with make lint/java, all 72 affected lifecycle/runtime/registry
tests on SQLite, and Java CI on Java 21/25 and Postgres 14-18.
@brandur
brandur force-pushed the brandur-java-client-lifecycle branch from 8f797d0 to 1b12141 Compare October 27, 2026 03:26
@brandur

brandur commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

@bgentry Found a piece of the Java API that was weirdly inconsistent here (instead of getting a RunHandle that you'd called stop on like in Rust, in Java you got a Workers instead and called stop on that, which is fairly odd because Workers does something else in every other language.

I'm going to do a quick patch here while we can still break things without much trouble:

Language Startup Shutdown
Go Client.Start() Client.Stop() / StopAndCancel()
Ruby Client#start On Client
JavaScript client.start() → RunHandle On RunHandle
Rust client.start() → RunHandle On RunHandle
Java, previously Workers.Builder.start() → Workers On Workers
Java, updated client.start() → RunHandle RunHandle.stop() / stopAndCancel() / close()

Link the lifecycle changelog entry to PR #1555. Remove the migration
guide for the short-lived previous API; the current API remains
documented in the README and API comparison.
@brandur
brandur merged commit f570cc6 into master Oct 27, 2026
30 checks passed
@brandur
brandur deleted the brandur-java-client-lifecycle branch October 27, 2026 03:44
@brandur brandur mentioned this pull request Oct 27, 2026
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