Skip to content
Bluetape4k docs1.11

Concurrency and lifecycle

Latest stable Based on Bluetape4k release 1.11.0

Limiting only concurrent calls to an asynchronous dependency still allows an unbounded waiting queue when producers are faster. Shutdown must reject new work, distinguish queued work from already-running external work, and close dependent resources in reverse order.

ConcurrentReducer bounds both active capacity and queue capacity. ShutdownQueue is a final JVM-exit safety net that closes process-wide resources in reverse registration order.

ConcurrentReducer capacity and failure paths

State/signalMeaning
activeCountTasks currently holding permits
queuedCountAccepted tasks not yet started
remainingActiveCapacityPermits available to start immediately
remainingQueueCapacityAdditional tasks that may wait
closedNew submissions rejected and queued promises cancelled

Both maxConcurrency and maxQueueSize must be positive. The implementation combines an ArrayBlockingQueue, a Semaphore, and a single-thread pump executor that advances work after completions.

import io.bluetape4k.concurrent.concurrentReducerOf
import java.util.concurrent.CompletionStage
fun <T> fetchAll(
ids: List<String>,
fetchAsync: (String) -> CompletionStage<T>,
): List<T> = concurrentReducerOf<T>(
maxConcurrency = 8,
maxQueueSize = 64,
).use { reducer ->
val promises = ids.map { id -> reducer.add { fetchAsync(id) } }
promises.map { it.join() }
}

Observe every promise returned by add. A full queue or closed reducer does not synchronously throw at the call site; it returns an already-failed CompletableFuture.

SituationReturned promise
Accepted into queueCompletes with task result/error
Queue fullFails with CapacityReachedException
Reducer closedFails with RejectedExecutionException
Task lambda throwsFails with the same error and releases permit
Task returns a null stageFails with NullPointerException
Caller cancels queued promiseSkipped before start and permit released

join() can wrap the cause in CompletionException. Apply separate policy for overload rejection, task failure, and caller cancellation by inspecting the cause.

close() is idempotent. The first call marks the reducer closed, cancels queued promises with cancel(false), and shuts down its pump executor. It cannot forcibly cancel an external CompletionStage that has already started because the reducer does not own that stage.

Design shutdown in this order:

  1. Stop producers from creating new work.
  2. Drain running work as required, or use the external client’s cancellation API.
  3. Close the reducer to cancel queued work.
  4. Close the client/executor invoked by the reducer.

Shutdown resources in reverse registration order

ShutdownQueue.register(closeable) ignores duplicate registration of the same object. Its JVM shutdown hook uses pollLast() for LIFO closing. closeSafe prevents one close failure from blocking remaining cleanup.

val client = ExternalClient()
val service = Service(client)
ShutdownQueue.register(client) // dependency first
ShutdownQueue.register(service) // dependent wrapper later; closes first

Do not replace normal lifecycle ownership with this queue. Close directly in a Spring bean, request, or test-fixture lifecycle when an earlier deterministic boundary exists. Use ShutdownQueue only to protect against process-exit omissions.

RequirementChoice
Bound active and waiting CompletionStage workConcurrentReducer
Bound only suspend-function concurrencyCoroutine semaphore or mapParallel
Durable delivery and retryBroker or durable queue
Normal shutdown of component-owned resourceDirect close in component lifecycle
JVM-exit safety net for process-wide resourceShutdownQueue
  • Track activeCount, queuedCount, queue-full rejection, and task failure separately.
  • Queue capacity is a hidden latency budget. Estimate worst wait from maxQueueSize / throughput.
  • Record producer-stop, queued-cancellation, running-drain, and dependency-close times separately during shutdown.
  • Unbounded retry on a full queue amplifies overload. Define a retry budget, backoff, and caller-visible 429/503 policy.

Continue with Bounded collections for process-local state and Core recipes for the assembled flow.