Soklet Logo

Core Concepts

Application Lifecycle

One Soklet lifecycle owns every HTTP, SSE, and MCP transport in its configuration. Choose the form that matches your application's lifetime:

ApplicationStarting pointWhat ends the lifetime?
Standalone serverSoklet.run(config)JVM shutdown or an application shutdown request
Test or finite workSoklet.run(config, action)The callback returns or throws
Embedded serviceSoklet.run(config)The host calls requestShutdown() and awaits completion

Construct fresh transports for each lifecycle. A transport already claimed by another run cannot be reused; Soklet throws TransportOwnershipException.

Standalone Applications

Soklet soklet = Soklet.run(config);
System.out.println("Soklet is ready");
ShutdownResult result = soklet.awaitShutdown();

run(config) returns only after all configured transports reach readiness. Startup failure throws synchronously after attempted teardown. Perform required application initialization before calling run.

Every run installs a JVM shutdown hook by default. The process remains alive after run(config) returns until lifecycle finalization finishes. Neither awaitShutdown() nor awaitShutdownAsync() initiates shutdown: they observe its outcome. requestShutdown() requests termination promptly and is safe to call repeatedly.

Soklet is one-shot and does not implement AutoCloseable. There is no separate public start() or close() operation.

Scoped Work

For tests and embedded work whose lifetime fits inside a block, prefer the scoped overload:

ShutdownResult result = Soklet.run(config, () -> {
  // Listeners are ready; run application work or network assertions.
});

The nested Soklet.RunAction<E> receives no lifecycle handle. It runs exactly once on the calling thread after readiness and can throw checked exceptions. Startup failure never invokes it.

Returning or throwing requests shutdown and waits for full finalization before control leaves run. Ordinary tests can ignore the returned ShutdownResult and rely on success-or-exception behavior. See the complete live-listener test for a runnable example.

The callback itself has no imposed timeout. External shutdown does not forcibly interrupt or terminate it; the scoped call still needs the callback to return or throw. Give network requests and other potentially blocking work their own deadlines.

Embedded Hosts

Keep a handle when an application's existing host starts and stops services at separate times. This adapter offers the host a blocking stop and an asynchronous stop:

import com.soklet.ShutdownResult;
import com.soklet.Soklet;
import com.soklet.SokletConfig;
import java.util.concurrent.CompletionStage;

public final class EmbeddedServer {
  private final Soklet soklet;

  public EmbeddedServer(SokletConfig config) {
    this.soklet = Soklet.run(config);
  }

  public ShutdownResult stop() throws InterruptedException {
    this.soklet.requestShutdown();
    return this.soklet.awaitShutdown();
  }

  public CompletionStage<ShutdownResult> stopAsync() {
    this.soklet.requestShutdown();
    return this.soklet.awaitShutdownAsync();
  }
}

Create the adapter after shared dependencies are ready. The constructor returns after Soklet is ready, or throws if startup fails. Stop Soklet before releasing databases, telemetry, or logging that active requests still need.

A synchronous host can sequence those operations directly:

void stopAndRelease(EmbeddedServer server, Runnable releaseSharedResources)
    throws InterruptedException {
  server.stop();
  releaseSharedResources.run();
}

An asynchronous host that keeps the JVM alive until the returned stage completes can compose the same sequence without assigning an executor just to wait:

java.util.concurrent.CompletionStage<Void> stopAndReleaseAsync(
    EmbeddedServer server, Runnable releaseSharedResources) {
  return server.stopAsync().thenRun(releaseSharedResources);
}

Both examples release resources only after successful finalization. If shutdown fails or the blocking wait is interrupted, the host must handle that outcome before releasing dependencies. Observe the returned async stage to handle shutdown or resource-release failures. The host owns JVM liveness until that stage completes, coordination with its other services, and releasing shared resources once. For a standalone process, put required release in ShutdownCleanup instead.

Observing Asynchronous Completion

You can also observe shutdown without requesting it:

Soklet soklet = Soklet.run(config);
// Observe terminal outcome without requesting shutdown.
soklet.awaitShutdownAsync().whenComplete((result, failure) -> {
  if (failure != null)
    failure.printStackTrace();
  else
    System.out.println(result);
});

awaitShutdownAsync() returns a cached, read-only completion stage for the same outcome as awaitShutdown(). The blocking await throws a lifecycle failure directly; async failure callbacks receive a java.util.concurrent.CompletionException whose cause is that failure. Unwrap it before inspecting lifecycle-specific fields such as getShutdownResult(). Canceling or completing a future derived from the stage cannot stop Soklet or change its outcome. Use requestShutdown() to request termination.

Keep completion callbacks bounded. They run on a daemon handoff and do not keep the JVM alive. Even normal programmatic shutdown can exit before those callbacks run if main has returned and the host supplies no other liveness. During JVM exit, post-await code may also be cut short. Put required application cleanup in the specification below.

Application Cleanup

Supply a bounded ShutdownCleanup for application-owned resources that can be released after Soklet proves core shutdown complete:

Soklet soklet = Soklet.run(config,
  ShutdownCleanup.fromTimeoutAndAction(
    Duration.ofSeconds(5),
    shutdownResult -> applicationResources.close()));
ShutdownResult result = soklet.awaitShutdown();

The same specification works with scoped execution: Soklet.run(config, cleanup, action). The four overloads are run(config), run(config, cleanup), run(config, action), and run(config, cleanup, action).

Cleanup runs at most once, only after complete core shutdown, including when the scoped callback failed. Incomplete core shutdown skips cleanup. At the cleanup deadline, Soklet requests interruption of a running action and stops waiting for it. Interruption is cooperative and may fail interruption-sensitive I/O; it does not forcibly terminate arbitrary cleanup code. Respond promptly to interruption and bound blocking operations inside cleanup. Use this for resources exclusive to the Soklet application. The embedded host should coordinate resources shared with other services.

Both await methods and scoped return include configured cleanup, terminal reporting, and release of process registrations. Lifecycle observer delivery has its own lifetime: if cleanup depends on a stateful observer, provide an application-owned delivery barrier. See Lifecycle Observer.

Errors and Interruption

SituationOutcome
Startup failsrun throws; a scoped callback is never invoked.
Startup cancellation from interruption wins before readinessStartup is canceled and run throws after attempted teardown; the caller's interrupt flag is restored.
Readiness wins before startup cancellationStartup succeeds and the caller's interrupt flag is restored; that interruption does not request shutdown.
Scoped callback throws an exception or ErrorFinalization finishes first, then the same failure is rethrown. Any finalization failure is suppressed on it.
Finalization fails without a callback failureA SokletLifecycleException subtype is thrown directly by a blocking await. Async callbacks receive a CompletionException with that lifecycle exception as its cause. Inspect the lifecycle exception's immutable getShutdownResult().
Forced shutdown proves completionThe call can return successfully, with retained diagnostics available in ShutdownResult.
An explicit blocking await is interruptedInterruptedException stops that wait; the lifecycle continues.
Scoped finalization is interruptedSoklet continues joining shutdown and restores the caller's interrupt flag before returning or throwing. An exception thrown by the callback remains its original failure.

Handlers, interceptors, observers, and other work that shutdown must drain should request shutdown and return promptly. Waiting for their own lifecycle to finish can make shutdown wait on that same work. Built-in callback scopes reject blocking self-waits with IllegalStateException; joining the async stage from such work is also unsafe. Await from the host or an independent control thread.

JVM Shutdown

When enabled, the automatic hook joins bounded core shutdown, eligible application cleanup, and terminal reporting. Normal awaits and scoped return also include process-registration release. Normal finalization removes the hook.

Independent JVM hooks execute concurrently with no guaranteed order. Registering a separate database or logging hook does not ensure it runs after Soklet. Coordinate those dependencies through the host or ShutdownCleanup. Do not call System.exit() from a handler, producer, or cleanup action that shutdown must join.

Startup, graceful drain, and forced shutdown use the shared LifecyclePolicy. Those phase budgets do not impose a deadline on application work inside a scoped callback. See Graceful Shutdown for deployment budgeting and signal behavior.

Host-Owned JVM Shutdown

An embedding host may need to deregister from a load balancer or finish a bounded delay before Soklet starts draining. Disable Soklet's automatic hook for that configuration:

SokletConfig config = SokletConfig.withHttpServer(httpServer)
  .jvmShutdownHookEnabled(Boolean.FALSE)
  .build();
Soklet soklet = Soklet.run(config);

jvmShutdownHookEnabled(Boolean) defaults to Boolean.TRUE; passing null restores that default. isJvmShutdownHookEnabled() returns the effective, non-null Boolean. All four run overloads honor the option. Disabling it skips only automatic JVM hook registration. Programmatic shutdown, await methods, configured cleanup, scoped teardown, and the lifecycle owner's normal JVM liveness remain unchanged.

The host owns the termination sequence: prevent new Soklet runs once termination begins, perform any bounded deregistration or delay, then call requestShutdown() and synchronously awaitShutdown() before its own shutdown hook returns. Keep shared dependencies available until shutdown succeeds. Returning from the host hook after merely scheduling asynchronous shutdown does not let Soklet finish: the JVM can exit when all shutdown hooks return, even while its non-daemon lifecycle owner is alive.

Coordinate host startup and termination together. Skipping automatic hook registration also removes the registration-time check for a JVM shutdown already in progress. The host must reject new runs after its termination sequence starts.

Console Input

Application code owns console interaction:

java.io.Console console = System.console();
if (console != null) {
  Soklet.run(config, () -> console.readLine("Press Enter to stop.%n"));
} else {
  Soklet.run(config).awaitShutdown();
}

Enter or end-of-input ends the callback and starts scoped shutdown. Without a native console, this example waits for JVM shutdown. Redirected stdin and IDE input panes do not necessarily provide System.console(). Applications with an existing input loop can instead request shutdown from their own event handler.

Soklet does not install console-input listeners or expose a ShutdownTrigger argument. The Barebones App shows the minimal standalone form: Soklet.run(config), then awaitShutdown(), stopped by Ctrl+C through the JVM shutdown hook.

Previous
Contributing