Skip to content

baldur.adapters.memory — Core State Repositories

In-memory implementations of the highest-traffic repository interfaces: failed-operation persistence and circuit-breaker state. For testing, standalone (non-framework) usage, and prototyping.

InMemoryFailedOperationRepository

InMemoryFailedOperationRepository()

Bases: FailedOperationRepository

In-memory implementation of FailedOperationRepository.

Thread-safe storage for DLQ entries in memory. Data is lost when the process exits.

Maintains status/domain indexes for O(1) lookup instead of O(n) scan.

create

create(
    domain: str,
    failure_type: str,
    error_message: str = "",
    error_code: str = "",
    entity_type: str | None = None,
    entity_id: str | None = None,
    entity_refs: dict[str, Any] | None = None,
    user_id: int | None = None,
    snapshot_data: dict[str, Any] | None = None,
    request_data: dict[str, Any] | None = None,
    response_data: dict[str, Any] | None = None,
    metadata: dict[str, Any] | None = None,
    retry_count: int = 0,
    max_retries: int = 2,
    next_action_hint: str = "",
    recommended_action: str = "",
    expires_at: datetime | None = None,
) -> FailedOperationData

Create a new failed operation record (domain-neutral).

get_by_id

get_by_id(id: str) -> FailedOperationData | None

Get a failed operation by ID.

get_pending_by_domain

get_pending_by_domain(
    domain: str, limit: int = 100
) -> list[FailedOperationData]

Get pending operations for a specific domain.

get_pending_count_by_domain

get_pending_count_by_domain(domain: str) -> int

Get count of pending operations for a domain.

update_status

update_status(
    id: str,
    status: str,
    resolution_type: str = "",
    resolution_note: str = "",
    resolved_by_id: int | None = None,
    recommended_action: str = "",
) -> bool

Update the status of a failed operation.

increment_retry_count

increment_retry_count(id: str) -> bool

Increment retry count and update last_retry_at.

mark_as_resolved

mark_as_resolved(
    id: str,
    resolution_type: str,
    resolution_note: str = "",
    resolved_by_id: int | None = None,
) -> bool

Mark a failed operation as resolved.

get_expired_operations

get_expired_operations(
    before_date: datetime, limit: int = 100
) -> list[FailedOperationData]

Get operations that have expired.

bulk_update_status

bulk_update_status(ids: list[str], status: str) -> int

Bulk update status for multiple operations.

find_by_status

find_by_status(
    status: str,
    domain: str | None = None,
    failure_type: str | None = None,
    limit: int = 100,
) -> list[FailedOperationData]

Find operations by status with optional filters.

find

find(
    *,
    status: str | None = None,
    domain: str | None = None,
    failure_type: str | None = None,
    offset: int = 0,
    limit: int = 100
) -> list[FailedOperationData]

Paginated cross-status query ordered by created_at DESC.

count

count(
    *,
    status: str | None = None,
    domain: str | None = None,
    failure_type: str | None = None
) -> int

Count operations matching filters (pre-slice set size).

count_created_in_window

count_created_in_window(
    start: datetime, end: datetime
) -> int

Count entries whose created_at is within the inclusive [start, end].

find_replayable_page

find_replayable_page(
    *,
    max_retries: int,
    domain: str | None = None,
    failure_type: str | None = None,
    source: str | None = None,
    limit: int = 100,
    cursor: str | None = None
) -> ReplayablePage

Select replayable entries from cursor onward, oldest first.

One O(n) pass over the status (or status+domain) index, keeping the limit smallest by (created_at, id) with heapq.nsmallest — O(n log limit) and no materialized sort. This backing is the zero-config default and the pass runs under the same lock create() takes, so sorting a full domain on every call would block capture for the length of each sort.

The pass has no scan bound: the index is a Python set with no useful iteration order, so stopping partway through would leave the walk unable to say what it had examined, and next_cursor would skip the members it never looked at.

find_sla_breached

find_sla_breached(
    current_time: datetime,
    sla_thresholds: dict[str, timedelta],
) -> list[FailedOperationData]

Find operations that have breached their SLA.

find_expired

find_expired(
    current_time: datetime,
) -> list[FailedOperationData]

Find operations past their retention period.

get_statistics

get_statistics() -> dict[str, Any]

Get statistics about failed operations.

Adds pending-specific breakdowns for the daily report: - pending_by_domain: {domain: pending_count} (required by update_dlq_pending_gauges — pre-existing bug fix) - pending_by_domain_and_failure_type: {domain: {failure_type: count}} (powers DLQPendingBreakdown in daily report)

Memory adapter iterates the pending index (O(N) in-memory).

get_facet_counts

get_facet_counts(
    *, status: str | None = None, domain: str | None = None
) -> dict[str, dict[str, int]]

Faceted status×domain counts via the 1D/2D indexes.

by_status is scoped by domain; by_domain is scoped by status (faceted-search semantics). Empty buckets are dropped explicitly with if ids: _remove_from_index discards ids without deleting an emptied set, so a fully-drained status/domain key lingers as an empty set and would otherwise surface as :0, breaking zero-drop parity with the SQL/Redis adapters.

try_acquire_for_replay

try_acquire_for_replay(
    id: str, max_retries: int, force: bool = False
) -> FailedOperationData | None

Atomically acquire a DLQ entry for replay.

force=True bypasses the cap gate (operator cap-override): it accepts a {PENDING, REQUIRES_REVIEW} source, resets retry_count to a fresh budget (this redrive is attempt 1), and stamps the metadata history scar before the reset. See FailedOperationRepository.try_acquire_for_replay.

complete_replay

complete_replay(
    id: str,
    success: bool,
    resolution_type: str = "",
    note: str = "",
    resolved_by_id: int | None = None,
    error_details: dict[str, Any] | None = None,
) -> bool

Complete a replay operation by updating the final status.

release_stale_replaying

release_stale_replaying(
    older_than_minutes: int = 30,
) -> int

Release DLQ entries stuck in REPLAYING state.

clear

clear() -> None

Clear all entries (for testing).

archive_old_resolved

archive_old_resolved(older_than_days: int = 30) -> int

Archive resolved entries older than N days.

purge_archived

purge_archived(
    ids: list[str] | None = None,
    older_than_days: int | None = None,
) -> int

Permanently delete archived entries.

count_archived_older_than

count_archived_older_than(older_than_days: int) -> int

Count archived entries older than N days.

count_all

count_all() -> int

Return active DLQ item count (excludes resolved/rejected/archived).

Matches Redis adapter semantics where resolved entries are removed from the PENDING_KEY sorted set.

count_by_domain

count_by_domain(domain: str) -> int

Return active DLQ item count for a domain (same population as :meth:count_all, scoped to one domain).

The per-domain cap is enforced against this number, so it has to exclude the same finished entries the global count does — otherwise a domain's cap trips on entries that were resolved days ago.

get_oldest_ids

get_oldest_ids(
    count: int, domain: str | None = None
) -> list[str]

Return IDs of the oldest evictable items (by created_at).

Evictable = neither finished nor in flight; see :data:_NON_EVICTABLE_STATUSES for why the candidate set is narrowed rather than filtered after the fact.

delete

delete(entry_id: str) -> bool

Delete a single entry by ID. Returns True if deleted.

evict_oldest

evict_oldest(count: int, domain: str | None = None) -> int

Delete the oldest items, skipping entries in protected statuses.

get_cleanup_stats

get_cleanup_stats() -> dict[str, Any]

Get statistics for cleanup operations.

compress_and_evict_oldest

compress_and_evict_oldest(
    count: int, domain: str | None = None
) -> int

Compress then evict oldest entries (in-memory implementation).

Same logical flow as Redis adapter but uses Python dict/list.

store_compressed_entry

store_compressed_entry(entry: DLQCompressedEntry) -> bool

Store compressed entry in memory dict.

get_compressed_entries

get_compressed_entries(
    domain: str | None = None,
    status: str | None = None,
    limit: int = 100,
) -> list[DLQCompressedEntry]

Query compressed entries from memory, newest first.

get_compressed_entry

get_compressed_entry(
    entry_id: str,
) -> DLQCompressedEntry | None

Return a single compressed entry by id, or None if absent.

get_compressed_entries_before

get_compressed_entries_before(
    *,
    status: str,
    before: datetime,
    limit: int = 100,
    offset: int = 0,
    after: datetime | None = None
) -> list[DLQCompressedEntry]

Query compressed entries in a cutoff window, oldest first.

get_compressed_summary

get_compressed_summary() -> dict[str, Any]

Aggregate statistics of compressed entries.

update_compressed_status

update_compressed_status(
    entry_id: str, new_status: str
) -> bool

Transition compressed entry lifecycle status.

InMemoryCircuitBreakerStateRepository

InMemoryCircuitBreakerStateRepository()

Bases: CircuitBreakerStateRepository

In-memory implementation of CircuitBreakerStateRepository.

Thread-safe storage for circuit breaker states in memory.

failure_count and success_count are plain cumulative counters, so a reset written by update_state sticks — matching the Redis and SQL repositories. Rate evidence for the failure-rate trigger lives in the circuit breaker service's outcome window, not here.

get_by_service_name

get_by_service_name(
    service_name: str,
) -> CircuitBreakerStateData | None

Get circuit breaker state by service name.

get_or_create

get_or_create(service_name: str) -> CircuitBreakerStateData

Get or create a circuit breaker state.

Double-checked locking — steady-state callers (entry exists) bypass the RLock entirely and pay only a GIL-atomic dict.get. Only the first-call create needs the lock. Mirrors the precedent in protect.py and the project-wide policy in factory/base.py ("read-only dict operations rely on CPython GIL atomicity").

Removes the surviving read-path acquire that was the contention floor under high concurrency.

hydrate_snapshot

hydrate_snapshot(
    snapshot: CircuitBreakerStateData,
    skip_if_local_pin_active: bool = False,
) -> bool

Create-or-replace a row from a durable snapshot, pin fields included.

The single primitive for wholesale restores from the durable layer: the layered repository's construction-time load and its miss-hydration lane. It carries the manual-control fields that a per-field state copy drops, so an operator's Block/Allow survives into a process that did not take it.

Copied from the snapshot: state, failure_count, success_count, opened_at, last_failure_at, manually_controlled, controlled_by_id, control_reason, manual_override_expires_at, metadata.

Preserved (layer-local, never carried by a snapshot): id, created_at, and the half-open window fields, which belong to the atomic slot-acquire primitives rather than to bulk transfer.

Deliberately NOT the two-step set_manual_control + update_state: that primitive restamps opened_at on an OPEN row, which would corrupt the discriminator that decides whether a pin's lift is due, and the two writes are not atomic against a concurrent admission read. One locked replace avoids both.

Parameters:

Name Type Description Default
snapshot CircuitBreakerStateData

the durable row to restore.

required
skip_if_local_pin_active bool

decline the restore, inside the same lock hold that would perform it, when the local row already carries an override still in force. Convergence lanes acting on a deliberately older remote read pass it so a pin placed here after that read is not overwritten by the stale remote view. The wholesale restore lanes leave it off: an absent or never-hydrated local row has no local decision to protect.

False

Returns:

Type Description
bool

True when the row was written, False when the active-pin guard

bool

declined it.

update_state

update_state(
    service_name: str,
    state: str,
    failure_count: int | None = None,
    success_count: int | None = None,
    opened_at: datetime | None = None,
    last_failure_at: datetime | None = None,
    half_open_request_count: int | None = None,
    reset_half_open_count: bool = False,
    clear_opened_at: bool = False,
    skip_if_pinned: bool = False,
    keep_open: bool = False,
) -> bool

Update circuit breaker state.

clear_opened_at, skip_if_pinned and keep_open are all evaluated inside the single lock hold that performs the write, so none can be decided against a row an operator or a concurrent trip replaced in between.

increment_failure_count

increment_failure_count(
    service_name: str,
    last_failure_at: datetime | None = None,
) -> int

Increment failure count.

reset_counts

reset_counts(service_name: str) -> bool

Reset failure/success counts and clear the OPEN-era timestamp.

opened_at is cleared alongside the counters so a CLOSED DTO does not carry a stale OPEN-era timestamp. The L2-authoritative close path in LayeredCircuitBreakerStateRepository invokes this before transitioning L1 to CLOSED.

converge_to_closed_unless_pinned

converge_to_closed_unless_pinned(service_name: str) -> bool

Move an existing row to CLOSED in one lock hold, active pins excepted.

The write half of a convergence lane that has already established, from a healthy shared store, that this service is closed cluster-wide. The pin check and the write must not be split into two calls: an operator forcing the breaker open between them would keep its manual-control fields while the state flipped back to closed, leaving a row that is pinned closed and can no longer trip for the pin's whole lifetime, because outcome recording is suppressed while a pin is in force.

Writes the same fields the store-authoritative close writeback does — counters cleared, OPEN-era timestamp dropped, half-open window discarded — and leaves the manual-control fields exactly as it found them.

Returns:

Type Description
bool

True when the row was transitioned. False when there is no local

bool

row, or an override is in force, in which case nothing is written.

set_manual_control

set_manual_control(
    service_name: str,
    state: str,
    controlled_by_id: int | None = None,
    reason: str = "",
    expires_at: datetime | None = None,
) -> bool

Set manual control override.

clear_manual_control

clear_manual_control(
    service_name: str, preserve_reason: bool = False
) -> bool

Clear manual control override.

Only the manual-control flag is cleared. state and the counters (failure_count, success_count) are not modified. If a state transition is needed, the caller must invoke update_state first.

record_failure

record_failure(
    service_name: str,
) -> CircuitBreakerStateData

Record a failure and return updated state.

Increments the cumulative failure counter. A preceding update_state(failure_count=0) therefore sticks, so the counter reads as failures since the last success — the consecutive-failure evidence the count trigger expects, identical to Redis and SQL.

record_success

record_success(
    service_name: str,
) -> CircuitBreakerStateData

Record a success and return updated state.

Increments the cumulative success counter, leaving the failure counter untouched — resetting it is the service's decision, written through update_state.

record_success_with_close_check

record_success_with_close_check(
    service_name: str, success_threshold: int
) -> CircuitBreakerCloseAttempt

Atomic record-success + threshold-check + close transition.

Whole sequence executes under self._lock: success increment, threshold check, and (if crossed from HALF_OPEN) the close transition are one critical section. Closes the TOCTOU race where multiple stale-view callers each pass the threshold check and emit duplicate CIRCUIT_BREAKER_CLOSED events for the same logical recovery.

record_failure_with_open_check

record_failure_with_open_check(
    service_name: str,
) -> CircuitBreakerOpenAttempt

Atomic record-failure + HALF_OPEN -> OPEN re-open transition.

Whole sequence executes under self._lock: state read and (if the state is HALF_OPEN) the re-open transition are one critical section. Closes the TOCTOU race where multiple stale-view callers each read HALF_OPEN and emit duplicate CIRCUIT_BREAKER_OPENED events for the same logical re-open. Symmetric mirror of record_success_with_close_check; a single HALF_OPEN failure re-opens unconditionally (no threshold).

trip_to_open

trip_to_open(
    service_name: str, failure_count: int
) -> CircuitBreakerOpenAttempt

Atomic CLOSED -> OPEN trip under a single lock acquire.

The pin check, the state read and the OPEN write are one critical section, so the row an operator pinned cannot be overwritten between the check and the write, and two threads that both decided to trip produce exactly one did_open=True. Branch contract: see CircuitBreakerStateRepository.trip_to_open.

get_all_states

get_all_states() -> list[CircuitBreakerStateData]

Get all circuit breaker states.

reset

reset(service_name: str) -> bool

Reset circuit breaker to initial closed state.

atomic_force_open

atomic_force_open(
    service_name: str,
    reason: str = "",
    controlled_by_id: int | None = None,
    ttl_minutes: int | None = None,
) -> tuple[bool, str, str]

Atomically force open a circuit breaker.

atomic_force_close

atomic_force_close(
    service_name: str,
    reason: str = "",
    controlled_by_id: int | None = None,
    ttl_minutes: int | None = None,
) -> tuple[bool, str, str]

Atomically force close a circuit breaker.

atomic_reset

atomic_reset(
    service_name: str,
    reason: str = "",
    controlled_by_id: int | None = None,
) -> tuple[bool, str, str]

Atomically reset a circuit breaker to initial state.

try_acquire_half_open_slot

try_acquire_half_open_slot(
    service_name: str,
    limit: int,
    stuck_timeout_seconds: int,
) -> tuple[bool, str, str]

Atomic HALF_OPEN slot acquisition under RLock.

reset_half_open_count

reset_half_open_count(service_name: str) -> None

Reset HALF_OPEN counter and clear window watermark.

get_open_states

get_open_states(
    limit: int | None = None,
) -> list[CircuitBreakerStateData]

Get OPEN circuit breaker states, oldest-first.

get_all_open

get_all_open() -> list[CircuitBreakerStateData]

Get all open circuit breakers.

delete

delete(service_name: str) -> bool

Delete a circuit breaker state.

delete_state

delete_state(service_name: str) -> bool

Delete circuit breaker state (alias for delete).

clear

clear() -> None

Clear all entries (for testing).