Bulkhead#
Resilience4j's Bulkhead limits the number of concurrent calls to a service, shedding load when the concurrency ceiling is reached. It is implemented as a semaphore-backed abstraction with a single production implementation: SemaphoreBulkhead.
Key configuration parameters (set via BulkheadConfig):
maxConcurrentCallsβ the semaphore permit countmaxWaitDurationβ how long to wait for a permit before throwingBulkheadFullException;0= fail-fastfairCallHandlingEnabledβ whether the underlying semaphore uses fair ordering
Events published: CallPermitted, CallRejected, CallFinished
Synchronous API#
The core synchronous lifecycle is:
- Acquire β
tryAcquirePermission()(returns bool) oracquirePermission()(throws on rejection) - Execute your call
- Release β always call
onComplete()when done, orreleasePermission()if the permit was acquired but not used
Decorator helpers covering Supplier, Callable, Runnable, Function, Consumer, CompletionStage, and Future types are static methods on the Bulkhead interface. The BulkheadFuture wrapper ensures onComplete() is called exactly once when Future.get() returns .
β οΈ
decorateCompletionStagecallstryAcquirePermission()(fail-fast, non-queued) and wraps the result in a newCompletableFuture. It does not use the async permit queue described below .
Non-Blocking Async Permit Acquisition (acquirePermissionAsync)#
Added in PR #2506 to fix a critical issue: the previous Reactor operators blocked the calling thread (Netty event loop) when maxWaitDuration > 0, degrading reactive throughput.
Bulkhead.acquirePermissionAsync() returns a CompletableFuture<Void> that never parks the calling thread . Three completion paths in SemaphoreBulkhead:
| Condition | Result |
|---|---|
| Permit immediately available | Completes successfully at once |
Full + maxWaitDuration = 0 | Fails with BulkheadFullException immediately |
Full + maxWaitDuration > 0 | Queued as a PendingPermission; granted FIFO as calls complete |
PendingPermission is a CompletableFuture<Void> subclass with an atomic state machine (PENDING β GRANTED | EXPIRED | CANCELLED). Grant, expiry (via a shared ScheduledExecutorService), and cancellation compete on a single CAS β only the winner completes the future, preventing permit leaks .
Cancellation contract: Calling cancel() on the returned future removes the request from the queue without publishing a CallRejected event (the caller withdrew the request). If cancel() returns false, a permit was already granted and must still be released .
Drain loop: grantPendingPermissions() uses a work-in-progress counter to avoid recursion when synchronous continuations immediately release a permit . Event consumer failures do not interrupt the drain β they are rethrown once the drain is complete.
Backward compatibility: The interface defines a blocking-bridge default implementation of acquirePermissionAsync() for custom Bulkhead subclasses. SemaphoreBulkhead overrides it with the fully non-blocking version .
Kotlin Integration#
The resilience4j-kotlin module provides coroutine-aware extensions in two files.
Suspend Functions β Bulkhead.kt#
Bulkhead.executeSuspendFunction(block)β acquires a permit, runs the suspend block, then callsonComplete(). On cancellation, callsreleasePermission()instead ofonComplete().Bulkhead.decorateSuspendFunction(block)β returns a decorated suspend lambda .- Internal
acquirePermissionSuspend(): fast-path callsacquirePermission()directly whenmaxWaitDuration = 0; otherwise dispatches toDispatchers.IOto avoid blocking the coroutine dispatcher.
β οΈ The Kotlin suspend extensions still use the synchronous
acquirePermission()path (notacquirePermissionAsync()). Using a non-zeromaxWaitDurationwill block an IO thread. For reactive use cases, preferacquirePermissionAsync()via the Reactor operators.
Flow Operator β FlowBulkhead.kt#
Flow<T>.bulkhead(bulkhead: Bulkhead): Flow<T>
Wraps an upstream Flow with bulkhead protection :
- Acquires a permit via
acquirePermissionSuspend()before collecting from upstream. - Attaches an
onCompletionhandler: callsreleasePermission()on cancellation,onComplete()otherwise.
Cancellation Detection β Cancellation.kt#
isCancellation(coroutineContext, error) returns true when the coroutine's Job is cancelled or the error is a CancellationException . Both executeSuspendFunction and the Flow operator use this to decide whether to call releasePermission() (no work done) vs. onComplete() (work completed normally or with a non-cancellation error).
Key Source Files#
| File | Purpose |
|---|---|
Bulkhead.java | Interface: decorators, acquirePermissionAsync() contract |
SemaphoreBulkhead.java | Semaphore-based implementation with non-blocking permit queue |
Bulkhead.kt | Kotlin suspend-function extensions |
FlowBulkhead.kt | Kotlin Flow operator |
Cancellation.kt | Cancellation-detection utility for Kotlin extensions |