Skip to content

baldur.services — Bulkhead

Per-domain concurrency isolation: the bulkhead base class and its semaphore implementations, the per-domain registry, the @bulkhead decorator, policy adapters for composition, and the rejection/timeout exceptions.

Core & implementations

Bulkhead

Bases: ABC

Bulkhead abstract interface.

Prevents a failure in one component from propagating to other components through resource isolation.

Usage (context manager): bulkhead = SemaphoreBulkhead("database", max_concurrent=10)

with bulkhead.acquire(timeout=5.0):
    do_database_work()

Usage (decorator): @bulkhead.wrap def do_work(): pass

name abstractmethod property

name: str

Bulkhead name (domain identifier).

acquire abstractmethod

acquire(
    timeout: float | None = None,
) -> Generator[None, None, None]

Acquire the resource (context manager).

Parameters:

Name Type Description Default
timeout float | None

Wait timeout (seconds). If None, fail immediately (non-blocking).

None

Yields:

Type Description
None

None

Raises:

Type Description
BulkheadFullError

When resource acquisition fails

try_acquire abstractmethod

try_acquire(timeout: float | None = None) -> bool

Attempt to acquire the resource.

Parameters:

Name Type Description Default
timeout float | None

Maximum time (seconds) the implementation may wait for capacity — an upper bound on waiting, not a guarantee of it. None means no waiting (immediate verdict). Implementations whose capacity model already buffers bursts (bounded-queue thread pools) may return an immediate verdict for any timeout value.

None

Returns:

Type Description
bool

True if acquisition succeeded, False if it failed

release abstractmethod

release() -> None

Release the resource.

try_acquire_async async

try_acquire_async(timeout: float | None = None) -> bool

Attempt to acquire a seat from a coroutine.

Takes a seat on the same count as :meth:try_acquire, so sync and async callers of one compartment share its capacity. This default gives the immediate verdict (self.try_acquire(None)) for any timeout — it never waits and never touches the event loop, which satisfies the contract (timeout is an upper bound on waiting). Implementations that can wait without blocking the loop override it.

Non-abstract so existing third-party subclasses stay instantiable.

Parameters:

Name Type Description Default
timeout float | None

Upper bound on waiting (seconds). None means no waiting.

None

Returns:

Type Description
bool

True if a seat was taken (release it with :meth:release)

acquire_async async

acquire_async(
    timeout: float | None = None,
) -> AsyncGenerator[None, None]

Hold a seat for the body of an async with block.

Built on :meth:try_acquire_async and :meth:release.

Parameters:

Name Type Description Default
timeout float | None

Upper bound on waiting (seconds). None means no waiting.

None

Yields:

Type Description
AsyncGenerator[None, None]

None

Raises:

Type Description
BulkheadFullError

When no seat is available within timeout

get_state abstractmethod

get_state() -> BulkheadState

Return the current state.

execute

execute(
    fn: Callable[..., T],
    *args: Any,
    timeout: float | None = None,
    **kwargs: Any
) -> T

Execute a function inside the bulkhead compartment.

Default implementation: acquire a permit (bounded by timeout), run fn in the calling thread, release on return. timeout bounds the admission wait only — once admitted, fn occupies the caller until it returns. Implementations that offload execution (dedicated worker pools) override this to also bound execution time.

Non-abstract so existing third-party subclasses stay instantiable.

Parameters:

Name Type Description Default
fn Callable[..., T]

Function to execute

required
*args Any

Positional arguments

()
timeout float | None

Admission-wait timeout (seconds). If None, fail immediately when no permit is available (non-blocking).

None
**kwargs Any

Keyword arguments

{}

Returns:

Type Description
T

Function execution result

Raises:

Type Description
BulkheadFullError

When no permit is available within timeout

wrap

wrap(fn: Callable[..., T]) -> Callable[..., T]

Decorator that wraps a function with the bulkhead.

Parameters:

Name Type Description Default
fn Callable[..., T]

Function to wrap

required

Returns:

Type Description
Callable[..., T]

Function with the bulkhead applied

SemaphoreBulkhead

SemaphoreBulkhead(
    name: str, max_concurrent: int = 10, fair: bool = True
)

Bases: Bulkhead

Semaphore-style bulkhead with one seat count for sync and async callers.

Limits the concurrent execution count to prevent resource exhaustion. Suitable for I/O-bound work (DB queries, cache lookups, etc.).

Features: - One capacity shared by sync callers (any thread) and async callers (any event loop) - Timeout-bounded waiting that never blocks an event loop - Rejection statistics tracking

Admission rules: - A caller takes a seat itself, under the compartment lock, when fewer than max_concurrent are taken; a seat is never handed to a waiter. A release wakes the oldest waiter, which re-checks — an arriving caller may take the freed seat first (no first-come order), and the woken waiter then waits again for its remaining time. - A waiter whose deadline passes re-checks once and takes a free seat if there is one. - A sync call made on a thread that is running an event loop gives the immediate verdict: the seat it would wait for can be held by a coroutine of the very loop it would block. - release() never waits for a lock, so it is safe wherever it runs — including a finally that the garbage collector runs for an abandoned coroutine.

Parameters:

Name Type Description Default
name str

Bulkhead name (domain identifier)

required
max_concurrent int

Maximum concurrent execution count

10
fair bool

Ignored. Waiting callers get no first-come order (see the class docstring).

True

name property

name: str

Bulkhead name.

acquire

acquire(
    timeout: float | None = None,
) -> Generator[None, None, None]

Acquire a seat for the body of a with block.

Parameters:

Name Type Description Default
timeout float | None

Wait timeout (seconds). If None, fail immediately (non-blocking). Ignored on a thread running an event loop (immediate verdict).

None

Yields:

Type Description
None

None

Raises:

Type Description
BulkheadFullError

When resource acquisition fails

try_acquire

try_acquire(timeout: float | None = None) -> bool

Attempt to take a seat, waiting up to timeout for one.

Parameters:

Name Type Description Default
timeout float | None

Upper bound on waiting (seconds). None means no waiting (immediate verdict). On a thread that is running an event loop the verdict is always immediate.

None

Returns:

Type Description
bool

True if a seat was taken (release it with :meth:release)

release

release() -> None

Give a seat back. Never waits for a lock.

try_acquire_async async

try_acquire_async(timeout: float | None = None) -> bool

Attempt to take a seat from a coroutine, waiting up to timeout.

Waits on a future of the running loop, so the loop keeps serving other work, and any loop in the process can wait on the same compartment.

Parameters:

Name Type Description Default
timeout float | None

Upper bound on waiting (seconds). None means no waiting (immediate verdict).

None

Returns:

Type Description
bool

True if a seat was taken (release it with :meth:release)

acquire_async async

acquire_async(
    timeout: float | None = None,
) -> AsyncGenerator[None, None]

Hold a seat for the body of an async with block.

Parameters:

Name Type Description Default
timeout float | None

Upper bound on waiting (seconds). None means no waiting.

None

Yields:

Type Description
AsyncGenerator[None, None]

None

Raises:

Type Description
BulkheadFullError

When no seat is available within timeout

get_state

get_state() -> BulkheadState

Return the current state (sync and async callers alike).

AsyncSemaphoreBulkhead

AsyncSemaphoreBulkhead(name: str, max_concurrent: int = 10)

Async handle over one bulkhead compartment.

Constructed directly, it builds its own :class:SemaphoreBulkhead. The registry's get_async(name) returns a handle over the registered compartment instead, so the async callers it admits count against that compartment's capacity and appear in its state.

Features: - Seats shared with the compartment's sync callers - Timeout-bounded waiting on the running loop (any loop) - Rejection statistics tracked by the compartment

Parameters:

Name Type Description Default
name str

Bulkhead name (domain identifier)

required
max_concurrent int

Maximum concurrent executions

10

name property

name: str

Bulkhead name.

acquire async

acquire(
    timeout: float | None = None,
) -> AsyncGenerator[None, None]

Hold a seat for the body of an async with block.

Parameters:

Name Type Description Default
timeout float | None

Upper bound on waiting (seconds). None fails immediately (non-blocking). A thread-pool compartment always gives the immediate verdict.

None

Yields:

Type Description
AsyncGenerator[None, None]

None

Raises:

Type Description
BulkheadFullError

When resource acquisition fails

try_acquire async

try_acquire(timeout: float | None = None) -> bool

Acquisition attempt, mirroring this class's acquire timeout contract.

Parameters:

Name Type Description Default
timeout float | None

Maximum time (seconds) to wait for capacity — an upper bound on waiting, not a guarantee of it. None means no waiting (immediate verdict).

None

Returns:

Type Description
bool

True on success (release it with :meth:release), False on failure

release async

release() -> None

Release the resource.

get_state

get_state() -> BulkheadState

Return the compartment's state (sync and async callers alike).

BulkheadState dataclass

BulkheadState(
    name: str,
    bulkhead_type: BulkheadType,
    max_concurrent: int,
    active_count: int,
    waiting_count: int,
    rejected_count: int,
    last_rejection_time: datetime | None = None,
    queue_size: int = 0,
)

Bulkhead current state data.

name instance-attribute

name: str

Bulkhead name (domain identifier)

bulkhead_type instance-attribute

bulkhead_type: BulkheadType

Bulkhead type

max_concurrent instance-attribute

max_concurrent: int

Capacity — the number of seats the compartment admits at once. For a thread-pool compartment this is its worker count plus its queue size.

active_count instance-attribute

active_count: int

Seats taken — by sync and async callers alike. For a thread-pool compartment this counts running, queued and held calls.

waiting_count instance-attribute

waiting_count: int

Callers waiting — for a seat (semaphore compartment) or, for a thread-pool compartment, admitted tasks no worker has started yet

rejected_count instance-attribute

rejected_count: int

Total number of rejected requests

last_rejection_time class-attribute instance-attribute

last_rejection_time: datetime | None = None

Last rejection time

queue_size class-attribute instance-attribute

queue_size: int = 0

Waiting-queue seats included in max_concurrent (thread-pool compartments; 0 for a semaphore compartment)

available_permits property

available_permits: int

Number of available permits.

utilization_percent property

utilization_percent: float

Utilization (0-100%).

BulkheadType

Bases: str, Enum

Bulkhead type.

SEMAPHORE class-attribute instance-attribute

SEMAPHORE = 'semaphore'

Semaphore-based - limits only the concurrent execution count

THREAD_POOL class-attribute instance-attribute

THREAD_POOL = 'thread_pool'

Thread-pool-based - isolated execution in a dedicated thread pool

Registry

BulkheadRegistry

BulkheadRegistry(settings: BulkheadSettings | None = None)

Per-domain bulkhead registry.

Integrates with the existing ConnectionType and also supports custom domains.

Features: - Automatic registration of default bulkheads per ConnectionType - Custom domain support - Async views that share each compartment's seats - Fine-grained bulkheads per DB alias / cache instance

Parameters:

Name Type Description Default
settings BulkheadSettings | None

Bulkhead settings. Uses defaults if None.

None

get

get(name: str | ConnectionType) -> Bulkhead

Look up a bulkhead.

Parameters:

Name Type Description Default
name str | ConnectionType

Domain name or ConnectionType

required

Returns:

Type Description
Bulkhead

Bulkhead instance

Raises:

Type Description
BulkheadNotFoundError

Unregistered domain. Subclasses KeyError, so existing except KeyError consumers keep working; the message names the missing domain and lists the registered compartments.

get_or_create

get_or_create(
    name: str,
    max_concurrent: int | None = None,
    bulkhead_type: str = "semaphore",
) -> Bulkhead

Look up or create a bulkhead.

Parameters:

Name Type Description Default
name str

Domain name

required
max_concurrent int | None

Maximum concurrent execution count (default if None)

None
bulkhead_type str

"semaphore" or "thread_pool". A thread-pool request routes through the overridable builder seam — on the base registry it falls back to semaphore isolation (see _build_thread_pool_bulkhead).

'semaphore'

Returns:

Type Description
Bulkhead

Bulkhead instance

get_async

get_async(
    name: str | ConnectionType,
) -> AsyncSemaphoreBulkhead

Look up the async view of a registered compartment.

Returns a handle over the registered compartment itself, so async callers take their seats on the same count as its sync callers and appear in its state. Strict: a domain with no registered compartment is treated identically to get() — provisioning must precede the async lookup.

Parameters:

Name Type Description Default
name str | ConnectionType

Domain name or ConnectionType

required

Returns:

Type Description
AsyncSemaphoreBulkhead

AsyncSemaphoreBulkhead handle (cached per name; replaced when the

AsyncSemaphoreBulkhead

name is registered again)

Raises:

Type Description
BulkheadNotFoundError

No compartment registered for name. Subclasses KeyError; message lists the registered compartments.

get_for_database

get_for_database(alias: str = 'default') -> Bulkhead

Return the bulkhead for a DB alias.

Parameters:

Name Type Description Default
alias str

Django DB alias (default, replica, analytics, etc.)

'default'

Returns:

Type Description
Bulkhead

The bulkhead for the given alias

get_for_cache

get_for_cache(name: str = 'default') -> Bulkhead

Return the bulkhead for a cache instance.

Parameters:

Name Type Description Default
name str

Cache name (default, session, etc.)

'default'

Returns:

Type Description
Bulkhead

The bulkhead for the given cache

register

register(bulkhead: Bulkhead) -> None

Register a custom bulkhead.

Overwriting a built-in name (one of the four ConnectionType values) is allowed — it is the only path for swapping a built-in's implementation type and is load-bearing for the re-register capacity flow — but it permanently replaces a settings-owned protection compartment, so a WARNING is emitted to flag the footgun.

The async view for the same name is invalidated so async callers reach the new compartment on next access.

Parameters:

Name Type Description Default
bulkhead Bulkhead

Bulkhead to register

required

unregister

unregister(name: str) -> bool

Unregister a bulkhead.

The async view for the same name is invalidated alongside the sync entry.

Parameters:

Name Type Description Default
name str

Domain name

required

Returns:

Type Description
bool

True if unregistration succeeded, False if the name was not registered

Raises:

Type Description
ValueError

name is a built-in compartment (one of the four ConnectionType values). Built-ins are settings-owned protection compartments — removing one silently degrades isolation, so the mutation is blocked rather than allowed.

get_all_states

get_all_states() -> dict[str, BulkheadState]

Return all bulkhead states.

list_names

list_names() -> list[str]

Return all registered domain names.

get_bulkhead_registry

get_bulkhead_registry() -> BulkheadRegistry

Return the active BulkheadRegistry — the resolution chain.

Chain: the ProviderRegistry.bulkhead_registry slot when populated (a registry overlay registered by an extension package), else the lazily-built base singleton. The registry is the user-facing provisioning API (get_or_create before @bulkhead on custom domains), so a single getter structurally prevents provisioning into an inactive registry.

A provider registered into the slot MUST NOT itself consult the slot (its factory runs while the slot's non-reentrant lock is held — a re-entrant resolution fails loud with RuntimeError).

reset_bulkhead_registry

reset_bulkhead_registry() -> None

Reset the base singleton (for testing).

Clears only the chain's fallback leg; a populated provider slot is owned (and reset) by whoever registered it.

Decorator & policy

bulkhead

bulkhead(
    name: str | ConnectionType,
    timeout: float | None = None,
    fallback: Callable[..., T] | None = None,
) -> Callable[[Callable[..., T]], Callable[..., T]]

Bulkhead decorator (sync/async auto-dispatching).

Detects the function type via asyncio.iscoroutinefunction() and automatically applies the appropriate bulkhead (sync/async).

Custom domains must be provisioned (via get_or_create(), register(), or a policy/settings helper) before the decorated function is first called. An unregistered domain raises BulkheadNotFoundError — naming the registered compartments — on both sync and async callees; the error is raised before any fallback is considered, so fallback does not mask it. The four built-in compartments (database, cache, external_api, message_queue) are always available.

Parameters:

Name Type Description Default
name str | ConnectionType

Domain name or ConnectionType

required
timeout float | None

Resource acquisition wait timeout (seconds). If None, fail immediately.

None
fallback Callable[..., T] | None

Alternative function to call when the bulkhead is full

None

Returns:

Type Description
Callable[[Callable[..., T]], Callable[..., T]]

Decorator with the bulkhead applied

Examples:

Synchronous function (built-in compartment)

@bulkhead(ConnectionType.DATABASE) def db_query(): return execute_query()

Asynchronous function (built-in compartment)

@bulkhead(ConnectionType.DATABASE) async def async_db_query(): return await execute_async_query()

Timeout setting (built-in compartment)

@bulkhead("external_api", timeout=5.0) def call_external_api(): return requests.get(url)

Custom domain with fallback — provision it first, then decorate

from baldur.services.bulkhead import get_bulkhead_registry

get_bulkhead_registry().get_or_create("reports", max_concurrent=5)

@bulkhead("reports", fallback=lambda: {"status": "unavailable"}) def get_data(): return fetch_data()

bulkhead_for_database

bulkhead_for_database(
    alias: str = "default",
    timeout: float | None = None,
    fallback: Callable[..., T] | None = None,
) -> Callable[[Callable[..., T]], Callable[..., T]]

Per-DB-alias bulkhead decorator.

Internally uses BulkheadPolicy/AsyncBulkheadPolicy and looks up the bulkhead for the given alias via the Registry's get_for_database().

Parameters:

Name Type Description Default
alias str

Django DB alias (default, replica, analytics, etc.)

'default'
timeout float | None

Resource acquisition wait timeout (seconds)

None
fallback Callable[..., T] | None

(deprecated) Alternative function to call when the bulkhead is full. Using the BulkheadPolicy + FallbackPolicy combination directly is recommended.

None

Examples:

@bulkhead_for_database("default") def write_to_db(): Model.objects.using("default").create(...)

@bulkhead_for_database("replica") def read_from_replica(): return Model.objects.using("replica").all()

bulkhead_for_cache

bulkhead_for_cache(
    name: str = "default",
    timeout: float | None = None,
    fallback: Callable[..., T] | None = None,
) -> Callable[[Callable[..., T]], Callable[..., T]]

Per-cache-instance bulkhead decorator.

Internally uses BulkheadPolicy/AsyncBulkheadPolicy and looks up the bulkhead for the given cache via the Registry's get_for_cache().

Parameters:

Name Type Description Default
name str

Cache name (default, session, etc.)

'default'
timeout float | None

Resource acquisition wait timeout (seconds)

None
fallback Callable[..., T] | None

(deprecated) Alternative function to call when the bulkhead is full. Using the BulkheadPolicy + FallbackPolicy combination directly is recommended.

None

Examples:

@bulkhead_for_cache("default") def get_cached_value(key: str): return cache.get(key)

@bulkhead_for_cache("session") def get_session_data(session_id: str): return session_cache.get(session_id)

BulkheadPolicy

BulkheadPolicy(
    bulkhead: Bulkhead, timeout: float | None = None
)

Bases: ResiliencePolicy[T]

Synchronous Bulkhead Policy — resource isolation.

Delegates execution polymorphically to Bulkhead.execute() — each implementation supplies its own execution semantics (semaphore: run in the calling thread, timeout bounds the admission wait; worker-pool implementations may offload and also bound execution time).

Exception handling contract (same as CircuitBreakerPolicy): - BulkheadFullError → absorbed into PolicyResult(outcome=REJECTED) - BulkheadTimeoutError → absorbed into PolicyResult(outcome=TIMEOUT) (unreachable on the semaphore path — admission failure raises FullError) - Business exception during function execution → re-propagated via raise

Parameters:

Name Type Description Default
bulkhead Bulkhead

Bulkhead ABC implementation. Injected via DI. Registry lookup uses the bulkhead_policy() factory.

required
timeout float | None

Timeout (seconds) forwarded to Bulkhead.execute(). Semaphore implementations bound the admission wait with it (None = fail immediately, Fast Fail); worker-pool implementations bound execution time with it.

None

name property

name: str

Policy name.

bulkhead_name property

bulkhead_name: str

Domain identifier of the internal Bulkhead instance.

execute

execute(
    func: Callable[..., T],
    *args: Any,
    context: PolicyContext | None = None,
    **kwargs: Any
) -> PolicyResult[T]

Execute the function within the bulkhead resource.

A single polymorphic call — self._bulkhead.execute() — carries the implementation-specific semantics; this policy only maps exceptions to outcomes:

  • BulkheadFullError → PolicyOutcome.REJECTED
  • BulkheadTimeoutError → PolicyOutcome.TIMEOUT
  • Business exceptions during function execution re-propagate via raise.

AsyncBulkheadPolicy

AsyncBulkheadPolicy(
    async_bulkhead: AsyncSemaphoreBulkhead,
    timeout: float | None = None,
)

Asynchronous Bulkhead Policy — AsyncResiliencePolicy Protocol implementation.

Wraps AsyncSemaphoreBulkhead and returns results in PolicyResult form. Follows the same exception handling contract as the synchronous BulkheadPolicy.

The seats it takes are the compartment's own: an AsyncSemaphoreBulkhead from the registry is the async view of the registered compartment, so the async callers this policy admits and the sync callers of the same compartment together never exceed its capacity. timeout bounds the wait for a seat only; on a thread-pool compartment the verdict is immediate and the coroutine runs on its caller's loop while holding the seat.

Parameters:

Name Type Description Default
async_bulkhead AsyncSemaphoreBulkhead

AsyncSemaphoreBulkhead instance (DI).

required
timeout float | None

Resource acquisition timeout (seconds). If None, fail immediately. Waiting never blocks the event loop.

None

name property

name: str

Policy name.

bulkhead_name property

bulkhead_name: str

Domain identifier of the internal AsyncSemaphoreBulkhead.

execute async

execute(
    func: Callable[..., T],
    *args: Any,
    context: PolicyContext | None = None,
    **kwargs: Any
) -> PolicyResult[T]

Execute the function within the asynchronous bulkhead resource.

BulkheadFullError → absorbed into PolicyResult(outcome=REJECTED). Business exceptions during function execution are re-propagated via raise.

bulkhead_policy

bulkhead_policy(
    name: str,
    max_concurrent: int | None = None,
    timeout: float | None = None,
    bulkhead_type: str = "semaphore",
) -> BulkheadPolicy

BulkheadPolicy factory — BulkheadRegistry singleton integration.

Calls the Registry's get_or_create() to guarantee a single global Bulkhead instance for the same name.

The Registry dependency occurs only in this factory function. The BulkheadPolicy class itself does not know the Registry (test-friendly).

Parameters:

Name Type Description Default
name str

Domain name (Registry key)

required
max_concurrent int | None

Maximum concurrent execution count (Registry default if None)

None
timeout float | None

Resource acquisition timeout (fail immediately if None)

None
bulkhead_type str

"semaphore" or "thread_pool"

'semaphore'

Returns:

Type Description
BulkheadPolicy

BulkheadPolicy instance (uses the Registry singleton Bulkhead)

async_bulkhead_policy

async_bulkhead_policy(
    name: str,
    max_concurrent: int | None = None,
    timeout: float | None = None,
) -> AsyncBulkheadPolicy

AsyncBulkheadPolicy factory — BulkheadRegistry singleton integration.

Calls the Registry's get_async() so the policy admits on the registered compartment's own seat count, shared with its sync callers.

Parameters:

Name Type Description Default
name str

Domain name (Registry key)

required
max_concurrent int | None

Maximum concurrent execution count (Registry default if None). The synchronous Bulkhead is provisioned first (so the domain is registry-visible to the admin API, metrics, and shutdown iteration); the async view takes its seats.

None
timeout float | None

Resource acquisition timeout (fail immediately if None)

None

Returns:

Type Description
AsyncBulkheadPolicy

AsyncBulkheadPolicy instance (uses the registered compartment's async view)

Metrics

BulkheadMetricsUpdater

BulkheadMetricsUpdater(interval: float = 10.0)

Background thread that periodically refreshes every bulkhead's metrics.

Pulls the current state of every registered bulkhead and writes it into the Prometheus metrics on a schedule.

Usage

updater = BulkheadMetricsUpdater(interval=10.0) updater.start()

... application runs ...

updater.stop()

Parameters:

Name Type Description Default
interval float

metric update interval (seconds)

10.0

start

start() -> None

Start the metrics updater.

stop

stop() -> None

Stop the metrics updater.

get_metrics_updater

get_metrics_updater(
    interval: float = 10.0,
) -> BulkheadMetricsUpdater

Return the BulkheadMetricsUpdater singleton.

start_metrics_updater

start_metrics_updater(
    interval: float = 10.0,
) -> BulkheadMetricsUpdater

Start the metrics updater (convenience function).

stop_metrics_updater

stop_metrics_updater() -> None

Stop the metrics updater (convenience function).

update_bulkhead_metrics

update_bulkhead_metrics(
    bulkhead_name: str,
    bulkhead_type: str,
    active_count: int,
    max_concurrent: int,
    waiting_count: int,
) -> None

Update bulkhead state gauges (poll path).

Parameters:

Name Type Description Default
bulkhead_name str

bulkhead name

required
bulkhead_type str

bulkhead type (semaphore, thread_pool)

required
active_count int

current number of active requests

required
max_concurrent int

maximum concurrent capacity

required
waiting_count int

number of waiting requests

required

increment_rejected_count

increment_rejected_count(bulkhead_name: str) -> None

Increment the rejected counter (event path).

Parameters:

Name Type Description Default
bulkhead_name str

bulkhead name

required

Exceptions

BulkheadError

BulkheadError(message: str = '', *, code: str = '')

Bases: ResilienceError

Base exception for bulkhead-related failures.

extra_context

extra_context() -> dict

Return structlog-bindable context for bulkhead errors.

BulkheadFullError

BulkheadFullError(
    bulkhead_name: str,
    max_concurrent: int,
    active_count: int,
)

Bases: PolicyRejectedException, BulkheadError

Raised when the bulkhead has no permits available and a new call is rejected.

Multi-inherits PolicyRejectedException so the outer PolicyComposer catch hierarchy classifies bulkhead rejection as PolicyOutcome.REJECTED rather than the generic except Exception branch (which would mislabel them as FAILURE).

Parameters:

Name Type Description Default
bulkhead_name str

bulkhead identifier

required
max_concurrent int

maximum allowed concurrent executions

required
active_count int

number of executions currently active

required

extra_context

extra_context() -> dict

Return structlog-bindable context for bulkhead full errors.

BulkheadTimeoutError

BulkheadTimeoutError(bulkhead_name: str, timeout: float)

Bases: TimeoutPolicyError, BulkheadError

Raised when a bulkhead-managed call exceeds its timeout budget.

Emitted by ThreadPoolBulkhead when the wrapped task does not complete within the configured timeout. Multi-inherits TimeoutPolicyError so the outer PolicyComposer catch hierarchy classifies the failure as PolicyOutcome.TIMEOUT (instead of funneling into the generic except Exception branch as FAILURE).

Parameters:

Name Type Description Default
bulkhead_name str

bulkhead identifier

required
timeout float

configured timeout in seconds

required

extra_context

extra_context() -> dict

Return structlog-bindable context for bulkhead timeout errors.

BulkheadNotFoundError

BulkheadNotFoundError(
    bulkhead_name: str, registered_names: list[str]
)

Bases: BulkheadError, KeyError

Raised when a bulkhead is requested for a domain that has not been provisioned.

Custom domains must be provisioned (via register(), get_or_create(), or a policy/settings helper) before a @bulkhead-decorated function is first called, on both sync and async callees.

Multi-inherits KeyError so the registry's long-standing not-found contract stays intact for existing except KeyError consumers (the traffic-gate skip-degradation path, the admin API 404 mapping), while except BulkheadError / except BaldurError also classify it. Multi-inheritance precedent: BulkheadFullError(PolicyRejectedException, BulkheadError).

__str__ is overridden because KeyError.__str__ would otherwise repr-quote the message (the base hierarchy defines no __str__), mangling the actionable text into "Bulkhead not found: ..." with surrounding quotes.

Parameters:

Name Type Description Default
bulkhead_name str

the requested, unregistered domain

required
registered_names list[str]

currently registered compartment names

required

extra_context

extra_context() -> dict

Return structlog-bindable context for bulkhead not-found errors.