Skip to content

Coordination

grelmicro.coordination

Coordination primitives for distributed locking and leader election.

Coordination

Coordination(
    source: Provider | type[Provider] | None = None,
    *,
    lock: Provider
    | LockBackend
    | type[Provider | LockBackend]
    | None = None,
    election: Provider
    | LeaderElectionBackend
    | type[Provider | LeaderElectionBackend]
    | None = None,
    schedule: Provider
    | ScheduleBackend
    | type[Provider | ScheduleBackend]
    | None = None,
    name: str = "default",
)

Coordination component: wraps backends and exposes coordination primitives.

Registered as micro.coordination after Grelmicro.use(Coordination(...)). Exposes lock(...), tasklock(...), and leaderelection(...) so users do not need to pass backend= on every primitive.

A single positional Provider resolves every primitive: the component calls provider.lock() for the lock backend, provider.leaderelection() for the election backend, and provider.schedule() for the cron schedule backend. The lock=, election=, and schedule= keywords set each backend independently, so locks can run on one vendor and leader election on another. Each accepts a Provider, a backend instance, or a zero-arg class.

Example
from grelmicro import Grelmicro
from grelmicro.coordination import Coordination
from grelmicro.providers.redis import RedisProvider

redis = RedisProvider("redis://localhost:6379/0")
micro = Grelmicro(uses=[redis, Coordination(redis)])

async with micro:
    async with micro.coordination.lock("cart"):
        ...
    leader = micro.coordination.leaderelection("worker")

Read more in the Coordination docs.

Initialize the component with the wrapped backends.

PARAMETER DESCRIPTION
source

A Provider (e.g. RedisProvider) that resolves both primitives. The component calls provider.lock() for the lock backend and provider.leaderelection() for the election backend. A zero-arg Provider class is instantiated for you. Use lock=/election= to set either backend independently.

TYPE: Provider | type[Provider] | None DEFAULT: None

lock

The lock backend. A Provider resolves it via provider.lock(), a LockBackend instance is used directly, and a zero-arg class is instantiated for you. Overrides the lock backend resolved from source.

TYPE: Provider | LockBackend | type[Provider | LockBackend] | None DEFAULT: None

election

The leader election backend. A Provider resolves it via provider.leaderelection(), a LeaderElectionBackend instance is used directly, and a zero-arg class is instantiated for you. Overrides the election backend resolved from source.

TYPE: Provider | LeaderElectionBackend | type[Provider | LeaderElectionBackend] | None DEFAULT: None

schedule

The cron schedule backend. A Provider resolves it via provider.schedule(), a ScheduleBackend instance is used directly, and a zero-arg class is instantiated for you. Overrides the schedule backend resolved from source.

TYPE: Provider | ScheduleBackend | type[Provider | ScheduleBackend] | None DEFAULT: None

name

Registration name. Multiple Coordination components may coexist on one Grelmicro under different names.

TYPE: str DEFAULT: 'default'

kind class-attribute

kind: str = 'coordination'

name property

name: str

Return the registration name.

lock_backend property

lock_backend: LockBackend

The underlying LockBackend.

RAISES DESCRIPTION
CoordinationBackendError

If no lock backend is wired.

election_backend property

election_backend: LeaderElectionBackend

The underlying LeaderElectionBackend.

RAISES DESCRIPTION
CoordinationBackendError

If no leader election backend is wired.

schedule_backend property

schedule_backend: ScheduleBackend

The underlying ScheduleBackend.

RAISES DESCRIPTION
CoordinationBackendError

If no schedule backend is wired.

lock

lock(name: str, **kwargs: Any) -> Lock

Construct a Lock bound to this component's lock backend.

RAISES DESCRIPTION
CoordinationBackendError

If no lock backend is wired.

tasklock

tasklock(name: str, **kwargs: Any) -> TaskLock

Construct a TaskLock bound to this component's lock backend.

RAISES DESCRIPTION
CoordinationBackendError

If no lock backend is wired.

leaderelection

leaderelection(name: str, **kwargs: Any) -> LeaderElection

Construct a LeaderElection bound to this component's election backend.

RAISES DESCRIPTION
CoordinationBackendError

If no leader election backend is wired.

Lock

Lock(
    name: str,
    *,
    backend: LockBackend | str | None = None,
    worker: str | UUID | None = None,
    lease_duration: Seconds | None = None,
    retry_interval: Seconds | None = None,
    retry_jitter: float | None = None,
    env_prefix: str | None = None,
    env_load: bool | None = None,
)

Bases: Reconfigurable[LockConfig], BaseLock

Lock.

This lock is a distributed lock that is used to acquire a resource across multiple workers. The lock is acquired asynchronously and can be extended multiple times manually. The lock is automatically released after a duration if not extended.

Supports live reconfiguration via reconfigure(new_config). A swap takes effect on the next call. In-flight calls keep the config they started with. The worker field cannot change. Changing it raises ValueError. See Live reconfiguration.

Initialize the lock.

PARAMETER DESCRIPTION
name

The name of the resource to lock.

It will be used as the lock name so make sure it is unique on the lock backend.

TYPE: str

backend

The distributed lock backend used to acquire and release the lock.

Accepts a backend instance, the name of a registered backend (e.g. "analytics"), or None to use the registered "default" backend.

TYPE: LockBackend | str | None DEFAULT: None

worker

The worker identity.

By default, a random 16-character hex token is generated.

TYPE: str | UUID | None DEFAULT: None

lease_duration

The duration in seconds for the lock to be held by default.

Default: 60. When unset and env reads are enabled (see env_load and GREL_ENV_LOAD), resolves from the environment variable GREL_LOCK_LEASE_DURATION for the default instance (GREL_LOCK_{NAME_UPPER}_LEASE_DURATION for a named one) if present, otherwise falls back to the LockConfig default.

TYPE: Seconds | None DEFAULT: None

retry_interval

The duration in seconds between attempts to acquire the lock.

Default: 0.1. Must be >= 0.001 to prevent flooding the lock backend. When unset and env reads are enabled (see env_load and GREL_ENV_LOAD), resolves from the environment variable GREL_LOCK_RETRY_INTERVAL for the default instance (GREL_LOCK_{NAME_UPPER}_RETRY_INTERVAL for a named one) if present, otherwise falls back to the LockConfig default.

TYPE: Seconds | None DEFAULT: None

retry_jitter

Factor for randomized jitter applied to each retry sleep.

Default: 0.1. Each sleep becomes retry_interval * uniform(1 - retry_jitter, 1 + retry_jitter). Set to 0 to disable jitter. When unset and env reads are enabled (see env_load and GREL_ENV_LOAD), resolves from the environment variable GREL_LOCK_RETRY_JITTER for the default instance (GREL_LOCK_{NAME_UPPER}_RETRY_JITTER for a named one) if present, otherwise falls back to the LockConfig default.

TYPE: float | None DEFAULT: None

env_prefix

Override the auto-derived environment variable prefix.

Default: GREL_LOCK_ for the default instance, GREL_LOCK_{NAME_UPPER}_ for a named one. Set this to a custom prefix when the application uses a different naming convention, for example MYAPP_LOCK_CART_.

TYPE: str | None DEFAULT: None

env_load

Whether to read environment variables.

When None (the default), follow the process-wide GREL_ENV_LOAD flag. Pass True or False to override the flag for this construction.

TYPE: bool | None DEFAULT: None

name property

name: str

Return the lock identity.

backend property

backend: LockBackend

Bound lock backend, resolved on each call.

When a backend instance was passed at construction it is always returned. Otherwise the active Grelmicro app is consulted via Grelmicro.current() on every access so that micro.override(Coordination(...)) blocks take effect.

RAISES DESCRIPTION
OutOfContextError

No backend resolved in this scope. Pass backend= (a MemoryLockAdapter() for a per-process lock), register a Coordination Component, or run the call inside async with micro: or after micro.install(app).

from_thread property

from_thread: ThreadLockAdapter

Return the lock adapter for a worker thread.

from_config classmethod

from_config(
    name: str,
    config: LockConfig,
    *,
    backend: LockBackend | str | None = None,
) -> Self

Construct a Lock from a name and a pre-built LockConfig.

PARAMETER DESCRIPTION
name

The name of the resource to lock.

Acts as the instance identity. Used as the backend lock key and exposed via the name property.

TYPE: str

config

The pre-built lock configuration.

Use this path when the configuration is assembled at startup from a settings tree (for example YAML, Vault, or a pydantic-settings aggregator). The environment path is bypassed and the config is used as-is.

TYPE: LockConfig

backend

The distributed lock backend used to acquire and release the lock.

Accepts a backend instance, the name of a registered backend (e.g. "analytics"), or None to use the registered "default" backend.

TYPE: LockBackend | str | None DEFAULT: None

acquire async

acquire(*, timeout: Seconds | None = None) -> LockHandle

Acquire the lock.

Returns the LockHandle for this acquisition, carrying the ownership token and the strictly increasing fencing_token.

PARAMETER DESCRIPTION
timeout

Maximum number of seconds to wait for the lock.

When None (the default), waits indefinitely. When set to a positive number, retries until the deadline then raises TimeoutError.

TYPE: Seconds | None DEFAULT: None

RAISES DESCRIPTION
LockReentrantError

If the lock is already acquired (nested usage is not supported).

LockAcquireError

If the lock cannot be acquired due to an error on the backend.

TimeoutError

If timeout is set and the lock was not acquired within that time.

extend async

extend() -> LockHandle

Renew the lease for another lease_duration without releasing.

The fencing token is unchanged when the lease is still held. If the lease has expired or was released by another path, the backend returns None and this method raises LockNotOwnedError.

RETURNS DESCRIPTION
LockHandle

The handle for this lock with the same fencing token.

TYPE: LockHandle

RAISES DESCRIPTION
LockNotOwnedError

If this task does not hold the lock or the lease was lost.

LockAcquireError

If the backend call fails.

acquire_nowait async

acquire_nowait() -> LockHandle

Acquire the lock, without blocking.

Returns the LockHandle for this acquisition, carrying the ownership token and the strictly increasing fencing_token.

RAISES DESCRIPTION
LockReentrantError

If the lock is already acquired (nested usage is not supported).

WouldBlockError

If the lock cannot be acquired without blocking.

LockAcquireError

If the lock cannot be acquired due to an error on the backend.

release async

release() -> None

Release the lock.

RAISES DESCRIPTION
LockNotOwnedError

If the lock is not owned by the current token.

LockReleaseError

If the lock cannot be released due to an error on the backend.

locked async

locked() -> bool

Check if the lock is acquired.

RAISES DESCRIPTION
LockLockedCheckError

If the lock cannot be checked due to an error on the backend.

owned async

owned() -> bool

Check if the lock is owned by the current token.

RAISES DESCRIPTION
LockBackendError

If the lock cannot be checked due to an error on the backend.

do_acquire async

do_acquire(token: str, *, duration: Seconds) -> int | None

Acquire the lock.

This method should not be called directly. Use acquire instead.

PARAMETER DESCRIPTION
token

The token to register on the backend.

TYPE: str

duration

The lease duration to request, in seconds. The caller captures this from self._config.lease_duration at the start of the operation so the request is consistent across retries even when reconfigure runs concurrently.

TYPE: Seconds

RETURNS DESCRIPTION
int | None

int | None: The fencing token if the lock was acquired, None if the lock was not acquired.

RAISES DESCRIPTION
LockAcquireError

If the lock cannot be acquired due to an error on the backend.

do_release async

do_release(token: str) -> bool

Release the lock.

This method should not be called directly. Use release instead.

RETURNS DESCRIPTION
bool

True if the lock was released, False otherwise.

TYPE: bool

RAISES DESCRIPTION
LockReleaseError

Cannot release the lock due to backend error.

do_owned async

do_owned(token: str) -> bool

Check if the lock is owned by the current token.

This method should not be called directly. Use owned instead.

RETURNS DESCRIPTION
bool

True if the lock is owned by the current token, False otherwise.

TYPE: bool

RAISES DESCRIPTION
LockOwnedCheckError

Cannot check if the lock is owned due to backend error.

do_thread_acquire async

do_thread_acquire(
    thread_id: int, *, timeout: Seconds | None = None
) -> LockHandle

Acquire the lock from a worker thread (blocking).

Runs on the event loop so the reentrant check and backend acquire are atomic with respect to other threads.

RAISES DESCRIPTION
LockReentrantError

If the lock is already acquired (nested usage is not supported).

LockAcquireError

If the lock cannot be acquired due to an error on the backend.

TimeoutError

If timeout is set and the lock was not acquired within that time.

do_thread_extend async

do_thread_extend(thread_id: int) -> LockHandle

Renew the lease from a worker thread without releasing.

Runs on the event loop so the ownership check and backend acquire are atomic with respect to other threads.

RAISES DESCRIPTION
LockNotOwnedError

If this thread does not hold the lock or the lease was lost.

LockAcquireError

If the backend call fails.

do_thread_acquire_nowait async

do_thread_acquire_nowait(thread_id: int) -> LockHandle

Acquire the lock from a worker thread (non-blocking).

Runs on the event loop so the reentrant check and backend acquire are atomic with respect to other threads.

RAISES DESCRIPTION
LockReentrantError

If the lock is already acquired (nested usage is not supported).

WouldBlockError

If the lock cannot be acquired without blocking.

LockAcquireError

If the lock cannot be acquired due to an error on the backend.

do_thread_release async

do_thread_release(thread_id: int) -> None

Release the lock from a worker thread.

Runs on the event loop so the backend release is atomic with respect to other threads.

RAISES DESCRIPTION
LockNotOwnedError

If the lock is not owned by the current token.

LockReleaseError

If the lock cannot be released due to an error on the backend.

TaskLock

TaskLock(
    name: str = "default",
    *,
    backend: LockBackend | str | None = None,
    worker: str | UUID | None = None,
    min_hold_duration: Seconds | None = None,
    lease_duration: Seconds | None = None,
    env_prefix: str | None = None,
    env_load: bool | None = None,
)

Bases: Reconfigurable[TaskLockConfig], LockPrimitive

Task Lock.

A distributed lock for scheduled tasks. Unlike a regular Lock, TaskLock does not release immediately on context manager exit. Instead, it keeps the lock held for at least min_hold_duration seconds to prevent re-execution on other nodes.

There is no background task that maintains the lock active during execution. The lock relies entirely on the TTL (lease_duration) set at acquire time.

This lock is designed to be used as the lock parameter of @task.interval.

Supports live reconfiguration via reconfigure(new_config). A swap takes effect on the next call. An exit re-acquire uses the config the call entered with. The worker field cannot change. Changing it raises ValueError. See Live reconfiguration.

Initialize the task lock.

PARAMETER DESCRIPTION
name

The name of the resource to lock.

It will be used as the lock name so make sure it is unique on the lock backend.

Defaults to "default". When used as the lock of @task.interval, the default name is re-stamped to the task name so it does not need to be repeated.

TYPE: str DEFAULT: 'default'

backend

The distributed lock backend used to acquire and release the lock.

By default, it will use the lock backend registry to get the default lock backend.

TYPE: LockBackend | str | None DEFAULT: None

worker

The worker identity.

By default, a UUIDv1 is generated.

TYPE: str | UUID | None DEFAULT: None

min_hold_duration

The minimum duration in seconds to hold the lock after task completion.

Default: 1. Prevents re-execution on other nodes before this duration has elapsed. When unset and env reads are enabled (see env_load and GREL_ENV_LOAD), resolves from the environment variable GREL_TASKLOCK_MIN_HOLD_DURATION for the default instance (GREL_TASKLOCK_{NAME_UPPER}_MIN_HOLD_DURATION for a named one) if present, otherwise falls back to the TaskLockConfig default.

TYPE: Seconds | None DEFAULT: None

lease_duration

The maximum duration in seconds to hold the lock (deadlock protection).

Default: 60. Acts as the TTL on acquire. When unset and env reads are enabled (see env_load and GREL_ENV_LOAD), resolves from the environment variable GREL_TASKLOCK_LEASE_DURATION for the default instance (GREL_TASKLOCK_{NAME_UPPER}_LEASE_DURATION for a named one) if present, otherwise falls back to the TaskLockConfig default.

TYPE: Seconds | None DEFAULT: None

env_prefix

Override the auto-derived environment variable prefix.

Default: GREL_TASKLOCK_ for the default instance, GREL_TASKLOCK_{NAME_UPPER}_ for a named one. Set this to a custom prefix when the application uses a different naming convention.

TYPE: str | None DEFAULT: None

env_load

Whether to read environment variables.

When None (the default), follow the process-wide GREL_ENV_LOAD flag. Pass True or False to override the flag for this construction.

TYPE: bool | None DEFAULT: None

name property

name: str

Return the task lock identity.

backend property

backend: LockBackend

Bound lock backend, resolved on each call.

When a backend instance was passed at construction it is always returned. Otherwise the active Grelmicro app is consulted via Grelmicro.current() on every access so that micro.override(Coordination(...)) blocks take effect.

RAISES DESCRIPTION
OutOfContextError

No backend resolved in this scope. Pass backend= (a MemoryLockAdapter() for a per-process lock), register a Coordination Component, or run the call inside async with micro: or after micro.install(app).

from_thread property

from_thread: ThreadTaskLockAdapter

Return the task lock adapter for worker thread.

from_config classmethod

from_config(
    name: str,
    config: TaskLockConfig,
    *,
    backend: LockBackend | str | None = None,
) -> Self

Construct a TaskLock from a name and a pre-built TaskLockConfig.

PARAMETER DESCRIPTION
name

The name of the resource to lock.

Acts as the instance identity. Used as the backend lock key and exposed via the name property.

TYPE: str

config

The pre-built task lock configuration.

Use this path when the configuration is assembled at startup from a settings tree (for example YAML, Vault, or a pydantic-settings aggregator). The environment path is bypassed and the config is used as-is.

TYPE: TaskLockConfig

backend

The distributed lock backend used to acquire and release the lock.

By default, it will use the lock backend registry to get the default lock backend.

TYPE: LockBackend | str | None DEFAULT: None

refresh async

refresh() -> None

Renew the lease for another lease_duration without releasing.

RAISES DESCRIPTION
LockNotOwnedError

If this task does not hold the lock or the lease was lost.

LockAcquireError

If the backend call fails.

locked async

locked() -> bool

Check if the lock is acquired.

RAISES DESCRIPTION
LockLockedCheckError

If the lock cannot be checked due to an error on the backend.

do_acquire async

do_acquire(token: str, *, duration: Seconds) -> bool

Acquire the lock.

This method should not be called directly. Use the context manager instead.

PARAMETER DESCRIPTION
token

The token to register on the backend.

TYPE: str

duration

The lease duration to request, in seconds. The caller captures this from self._config.lease_duration at the start of the operation so a concurrent reconfigure cannot change the duration mid-acquire.

TYPE: Seconds

RETURNS DESCRIPTION
bool

True if the lock was acquired, False if the lock was not acquired.

TYPE: bool

RAISES DESCRIPTION
LockAcquireError

If the lock cannot be acquired due to an error on the backend.

do_release async

do_release(token: str) -> bool

Release the lock.

This method should not be called directly. Use the context manager instead.

RETURNS DESCRIPTION
bool

True if the lock was released, False otherwise.

TYPE: bool

RAISES DESCRIPTION
LockReleaseError

Cannot release the lock due to backend error.

do_reacquire async

do_reacquire(token: str, duration: float) -> bool

Re-acquire the lock with a specific duration.

This method should not be called directly. Use the context manager instead.

RETURNS DESCRIPTION
bool

True if the lock was re-acquired, False otherwise.

TYPE: bool

RAISES DESCRIPTION
LockReleaseError

Cannot re-acquire the lock due to backend error.

do_thread_enter async

do_thread_enter() -> None

Acquire the lock from a worker thread.

Runs entirely on the event loop so the reentrant check, token generation, and backend acquire are atomic with respect to other threads.

RAISES DESCRIPTION
WouldBlockError

If the lock is already held by another worker.

LockAcquireError

If the lock cannot be acquired due to a backend error.

LockReentrantError

If the lock is already acquired (nested usage is not supported).

do_thread_exit async

do_thread_exit() -> None

Release or extend the lock from a worker thread.

Runs entirely on the event loop so the token generation and backend release are atomic with respect to other threads.

RAISES DESCRIPTION
LockReleaseError

If the lock cannot be released due to a backend error.

do_exit async

do_exit(token: str, *, min_hold_duration: Seconds) -> None

Handle exit logic: release or re-acquire based on elapsed time.

PARAMETER DESCRIPTION
token

The token used to release or re-acquire the lock.

TYPE: str

min_hold_duration

The minimum hold duration to enforce, in seconds. The caller captures this from self._config.min_hold_duration at the start of the operation so the comparison and the remaining-duration calculation always agree.

TYPE: Seconds

LeaderElection

LeaderElection(
    name: str,
    *,
    backend: LeaderElectionBackend | str | None = None,
    worker: str | UUID | None = None,
    lease_duration: Seconds | None = None,
    renew_deadline: Seconds | None = None,
    retry_interval: Seconds | None = None,
    retry_jitter: float | None = None,
    backend_timeout: Seconds | None = None,
    error_interval: Seconds | None = None,
    env_prefix: str | None = None,
    env_load: bool | None = None,
    metadata: Mapping[str, str] | None = None,
)

Bases: Reconfigurable[LeaderElectionConfig], LockPrimitive, Task

Leader Election.

The leader election is a synchronization primitive with the worker as scope. It runs as a task to acquire or renew the distributed lock.

Supports live reconfiguration via reconfigure(new_config). A swap takes effect on the next renew loop iteration. The worker field cannot change. Changing it raises ValueError. See Live reconfiguration.

Initialize the leader election.

PARAMETER DESCRIPTION
name

The name of the resource representing the leader election.

It will be used as the lock name so make sure it is unique on the distributed lock backend.

TYPE: str

backend

The leader election backend used to acquire and renew leadership.

By default, it resolves the Coordination component from the active app.

TYPE: LeaderElectionBackend | str | None DEFAULT: None

worker

The worker identity.

By default, a UUIDv1 will be generated.

TYPE: str | UUID | None DEFAULT: None

lease_duration

The duration in seconds after the lock will be released if not renewed.

Default: 15. If the worker becomes unavailable, the lock can only be acquired by another worker after it has expired. When unset and env reads are enabled (see env_load and GREL_ENV_LOAD), resolves from the environment variable GREL_LEADERELECTION_LEASE_DURATION for the default instance (GREL_LEADERELECTION_{NAME_UPPER}_LEASE_DURATION for a named one) if present, otherwise falls back to the LeaderElectionConfig default.

TYPE: Seconds | None DEFAULT: None

renew_deadline

The duration in seconds that the leader worker will try to acquire the lock before giving up.

Default: 10. Must be shorter than the lease duration. In case of multiple errors, the leader worker will lose the lead to prevent split-brain scenarios and ensure that only one worker is the leader at any time.

TYPE: Seconds | None DEFAULT: None

retry_interval

The duration in seconds between attempts to acquire or renew the lock.

Default: 2. Must be shorter than the renew deadline. A shorter schedule enables faster leader elections but may increase load on the distributed lock backend, while a longer schedule reduces load but can delay new leader elections.

TYPE: Seconds | None DEFAULT: None

retry_jitter

The jitter factor applied to the retry interval.

Default: 0.1. Each sleep becomes retry_interval * uniform(1 - retry_jitter, 1 + retry_jitter), so contending workers spread their attempts instead of retrying in lockstep. Set 0 to disable. Must be >= 0 and < 1.

TYPE: float | None DEFAULT: None

backend_timeout

The duration in seconds for waiting on backend for acquiring and releasing the lock.

Default: 5. This value determines how long the system will wait before giving up the current operation.

TYPE: Seconds | None DEFAULT: None

error_interval

The duration in seconds between logging error messages.

Default: 30. If shorter than the retry interval, it will log every error. It is used to prevent flooding the logs when the lock backend is unavailable.

TYPE: Seconds | None DEFAULT: None

env_prefix

Override the auto-derived environment variable prefix.

Default: GREL_LEADERELECTION_ for the default instance, GREL_LEADERELECTION_{NAME_UPPER}_ for a named one. Set this to a custom prefix when the application uses a different naming convention.

TYPE: str | None DEFAULT: None

env_load

Whether to read environment variables.

When None (the default), follow the process-wide GREL_ENV_LOAD flag. Pass True or False to override the flag for this construction.

TYPE: bool | None DEFAULT: None

metadata

Free-form key/value pairs stored on the lease while this worker leads, for observability (pod name, version, region). Other workers read them via LeaderElection.record.

TYPE: Mapping[str, str] | None DEFAULT: None

name property

name: str

Return the task name.

record property

record: LeaderRecord | None

The most recent LeaderRecord seen from the backend.

None until the first acquire/renew attempt completes. Reflects the current leader (record.holder), when they acquired and renewed the lease, how many times leadership has changed, and the holder's metadata. Updated on every renew loop iteration.

backend property

Bound coordination backend, resolved on each call.

When a backend instance was passed at construction it is always returned. Otherwise the active Grelmicro app is consulted via Grelmicro.current() on every access so that micro.override(Coordination(...)) blocks take effect. The backend comes from the Coordination component, whose election backend can point at a different vendor than its lock backend.

RAISES DESCRIPTION
OutOfContextError

No backend resolved in this scope. Pass backend= (a MemoryLeaderElectionAdapter() for a per-process election), register a Coordination Component, or run the call inside async with micro: or after micro.install(app).

from_config classmethod

from_config(
    name: str,
    config: LeaderElectionConfig,
    *,
    backend: LeaderElectionBackend | str | None = None,
    metadata: Mapping[str, str] | None = None,
) -> Self

Construct a LeaderElection from a name and a pre-built LeaderElectionConfig.

PARAMETER DESCRIPTION
name

The name of the resource representing the leader election.

Acts as the instance identity. Used as the backend lock key and exposed via the name property.

TYPE: str

config

The pre-built leader election configuration.

Use this path when the configuration is assembled at startup from a settings tree (for example YAML, Vault, or a pydantic-settings aggregator). The environment path is bypassed and the config is used as-is.

TYPE: LeaderElectionConfig

backend

The leader election backend used to acquire and renew leadership.

By default, it resolves the Coordination component from the active app.

TYPE: LeaderElectionBackend | str | None DEFAULT: None

metadata

Free-form key/value pairs stored on the lease while this worker leads, for observability.

TYPE: Mapping[str, str] | None DEFAULT: None

is_running

is_running() -> bool

Check if the leader election task is running.

is_leader

is_leader() -> bool

Check if the current worker is the leader.

This is an advisory local view. The result reflects the last backend response plus the configured renew_deadline. During a backend partition the answer can remain True until the renew deadline elapses, even if another worker has already acquired leadership through a reachable backend.

For work that cannot tolerate stale local leadership, use is_leader_confirmed_within with a tighter freshness bound, or fence each backend write with the lock token.

RETURNS DESCRIPTION
bool

True if the current worker is the leader (subject to the

bool

uncertainty window described above), False otherwise.

last_confirmation_age

last_confirmation_age() -> float | None

Seconds since the last backend response that confirmed leadership.

Returns None until the first acquisition succeeds, and is reset to None whenever leadership is lost. The value grows during a backend partition because the underlying timestamp is only refreshed when the backend responds with "you still hold the lock".

is_leader_confirmed_within

is_leader_confirmed_within(max_age: float) -> bool

Stricter than is_leader: require a recent backend confirmation.

Returns True only when the local view says leader AND the last backend response confirming leadership is at most max_age seconds old. Use this for fan-out work that must not run on a worker whose leadership is uncertain (for example, a global migration step or a single-writer task that is not separately fenced).

PARAMETER DESCRIPTION
max_age

Maximum acceptable age in seconds since the last backend-confirmed renewal. Typical values are less than renew_deadline.

TYPE: float

wait_for_leader async

wait_for_leader() -> None

Wait until the current worker is the leader.

wait_lose_leader async

wait_lose_leader() -> None

Wait until the current worker is no longer the leader.

lead async

lead(
    func: Callable[..., Awaitable[T]],
    /,
    *args: object,
    repeat: bool = False,
) -> T | None

Run func(*args) only while this worker holds leadership.

Waits for leadership, then runs func(*args) in a child task. The moment leadership is lost the task is cancelled, so no stale work outlives the lease. This is the difference from a plain if is_leader(): check, which keeps running until the next poll.

Returns func's result if it finishes while still leader, or None if leadership was lost first and the body was cancelled. With repeat=True, waits to re-acquire and runs func again instead of returning, looping until the surrounding scope is cancelled.

Requires the LeaderElection service to be running concurrently (wired through Tasks, or run as a task), since it renews the lease and drives the leadership changes lead waits on.

Any exception raised by func propagates out of lead.

Cancellation is cooperative: it takes effect at the body's next await. Pair with is_leader_confirmed_within or a fencing token for writes that must never overlap a successor.

async def emit_metrics() -> None:
    while True:
        await push_snapshot()
        await asyncio.sleep(10)

await election.lead(emit_metrics, repeat=True)
PARAMETER DESCRIPTION
func

Coroutine function to run only while this worker leads.

TYPE: Callable[..., Awaitable[T]]

repeat

Re-run func after re-acquiring leadership instead of returning once leadership is lost.

TYPE: bool DEFAULT: False

guard

guard() -> _LeaderGuard

Return a non-blocking synchronization guard.

The guard raises WouldBlock if the current worker is not the leader, making it suitable for use as the sync parameter of IntervalTask.

Unlike using LeaderElection directly (which blocks until leader), the guard skips the current tick and retries on the next interval.

LeaderElectionConfig

Bases: BaseLockConfig

Leader Election Config.

Leader election based on a leased reentrant distributed lock.

lease_duration class-attribute instance-attribute

lease_duration: Seconds = 15

The lease duration in seconds.

renew_deadline class-attribute instance-attribute

renew_deadline: Seconds = 10

The renew deadline in seconds.

retry_interval class-attribute instance-attribute

retry_interval: Seconds = 2

The retry interval in seconds.

backend_timeout class-attribute instance-attribute

backend_timeout: Seconds = 5

The backend timeout in seconds.

error_interval class-attribute instance-attribute

error_interval: Seconds = 30

The error interval in seconds.

retry_jitter class-attribute instance-attribute

retry_jitter: float = 0.1

Factor for randomized jitter applied to each retry sleep.

Each sleep becomes retry_interval * uniform(1 - retry_jitter, 1 + retry_jitter). Set to 0 to disable jitter. Must be >= 0 and < 1.

LeaderElectionBackend

Bases: Protocol

Leader Election Backend Protocol.

Optimized for leader election rather than general mutual exclusion: one renewable lease per election that stores a LeaderRecord, held continuously by the elected worker and renewed before it expires. A vendor backs it with whatever native lease it offers (a Redis value, a Postgres row, a Kubernetes Lease), storing the record alongside.

acquire_or_renew async

acquire_or_renew(
    *,
    name: str,
    token: str,
    duration: float,
    metadata: Mapping[str, str] | None = None,
) -> LeaderRecord

Acquire leadership, or renew it when token already holds it.

Returns the resulting LeaderRecord. The caller leads when record.holder == token, otherwise the record describes the current leader. Acquiring from a different (or expired) holder bumps transitions, renewing the same holder only moves renewed_at.

PARAMETER DESCRIPTION
name

Identifier of the election to acquire or renew.

TYPE: str

token

Worker token. The same token renews the lease, a different token may take over once the lease expires.

TYPE: str

duration

Seconds the lease is held before it expires. The leader renews before this elapses.

TYPE: float

metadata

Free-form key/value pairs to store on the lease while this worker holds it.

TYPE: Mapping[str, str] | None DEFAULT: None

release async

release(*, name: str, token: str) -> bool

Release leadership held by token.

Returns True when leadership was released, False when token did not hold it.

PARAMETER DESCRIPTION
name

Identifier of the election to release.

TYPE: str

token

Worker token. The backend only releases a matching holder.

TYPE: str

get async

get(*, name: str) -> LeaderRecord | None

Return the current LeaderRecord, or None when no live lease exists.

PARAMETER DESCRIPTION
name

Identifier of the election to inspect.

TYPE: str

LockBackend

Bases: Protocol

Lock Backend Protocol.

This is the low-level API for the distributed lock backend that is platform agnostic.

Implementations capture the running event loop on __aenter__ in a _loop attribute so lock adapters (Lock.from_thread, TaskLock.from_thread) can dispatch coroutines back into it.

acquire async

acquire(
    *, name: str, token: str, duration: float
) -> int | None

Acquire the lock.

Returns the fencing token when the lock was granted, None when another token already holds it.

The fencing token is a strictly increasing integer per lock name. It increments on every free-to-held transition (a fresh acquire by a new holder, or a takeover of an expired lock) and keeps climbing across release and re-acquire cycles. The same holder renewing or extending its lease receives the same token.

PARAMETER DESCRIPTION
name

Identifier of the lock to acquire.

TYPE: str

token

Caller-supplied ownership token. The same token must be passed to release and owned.

TYPE: str

duration

Seconds the lock is held before the backend may release it automatically. The acquirer should renew before this elapses.

TYPE: float

release async

release(*, name: str, token: str) -> bool

Release the lock.

Returns True when the lock was released, False when the token did not own the lock.

PARAMETER DESCRIPTION
name

Identifier of the lock to release.

TYPE: str

token

Ownership token previously passed to acquire. The backend only releases when the token matches.

TYPE: str

locked async

locked(*, name: str) -> bool

Return True when the lock is currently held by any token.

PARAMETER DESCRIPTION
name

Identifier of the lock to inspect.

TYPE: str

owned async

owned(*, name: str, token: str) -> bool

Return True when the lock is currently held by token.

PARAMETER DESCRIPTION
name

Identifier of the lock to inspect.

TYPE: str

token

Ownership token to compare against the current holder.

TYPE: str

LeaderRecord dataclass

LeaderRecord(
    holder: str,
    lease_duration: float,
    acquired_at: datetime,
    renewed_at: datetime,
    transitions: int,
    metadata: Mapping[str, str] = dict(),
)

The state of a leader election lease.

Unlike a Lock, a leader election lease carries observable state about who leads and since when. The shape follows the Kubernetes LeaderElectionRecord so the same record round-trips through a Redis value, a Postgres row, or a Kubernetes Lease.

holder instance-attribute

holder: str

Token of the worker that currently holds the lease.

lease_duration instance-attribute

lease_duration: float

Seconds the lease is valid from renewed_at before it expires.

acquired_at instance-attribute

acquired_at: datetime

When the current holder first acquired the lease.

renewed_at instance-attribute

renewed_at: datetime

When the current holder last renewed the lease.

transitions instance-attribute

transitions: int

Number of times the lease has changed holder.

metadata class-attribute instance-attribute

metadata: Mapping[str, str] = field(default_factory=dict)

Free-form key/value pairs the holder attached, for observability (pod name, version, region). Empty when none were set.

CoordinationError

Bases: GrelmicroError

Coordination Primitive Error.

This is the base class for all coordination errors.