Coordination
The coordination package gives you distributed Lock, ReadWriteLock,
TaskLock, and LeaderElection: the primitives that keep work correct when your
service runs as many replicas.
- Lock: mutual exclusion across workers. Hold a shared resource one caller at a time.
- Read-Write Lock: many readers at once, one writer alone. For a resource read far more often than it is written.
- Task Lock: a lock for scheduled tasks. It holds long enough to stop another worker re-running the same tick.
- Leader Election: elect one worker to play a long-lived role. Run a job at most once across all replicas.
All four are technology agnostic and run on the same backends (see Backends). Pick Redis, PostgreSQL, SQLite, Kubernetes, or in-memory without changing your code.
Use them together with Tasks and TaskRouter to control task execution across
a cluster (see the Task Scheduler).
Thread safety
The primitives are built for one async event loop and are not
thread-safe. Sync access from worker threads goes through from_thread
adapters, which dispatch operations to the event loop. Do not share instances
across event loops or threads without the adapter.
Quick start
Guard a shared resource with a distributed Lock. One provider line says where
the lock state lives:
from grelmicro import Grelmicro
from grelmicro.coordination import Lock
from grelmicro.providers.redis import RedisProvider
redis = RedisProvider("redis://localhost:6379/0")
micro = Grelmicro(uses=[redis])
lock = Lock("cart")
async def checkout() -> None:
async with lock:
...
async def main() -> None:
# The lock resolves its backend inside the app scope.
async with micro:
await checkout()
One caller holds cart at a time, on any worker. The next caller waits for the
release.
Redis needs the redis extra: pip install "grelmicro[redis]". Postgres,
SQLite, and Kubernetes work the same way, see Backends.
Backends
Load a backend before using any primitive. A Coordination component wraps the
backends and resolves them for you.
Install
Each backend needs its own extra:
- Redis:
pip install "grelmicro[redis]" - PostgreSQL:
pip install "grelmicro[postgres]" - SQLite:
pip install "grelmicro[sqlite]" - Kubernetes:
pip install "grelmicro[kubernetes]"
See the installation guide for uv and poetry.
Wire a Coordination component like this:
from grelmicro import Grelmicro
from grelmicro.coordination import Coordination
from grelmicro.providers.redis import RedisProvider
redis = RedisProvider("redis://localhost:6379/0")
micro = Grelmicro(uses=[Coordination(redis)])
import os
from grelmicro import Grelmicro
from grelmicro.coordination import Coordination
from grelmicro.providers.postgres import PostgresProvider
postgres = PostgresProvider(os.environ["POSTGRES_URL"])
micro = Grelmicro(uses=[Coordination(postgres)])
from grelmicro import Grelmicro
from grelmicro.coordination import Coordination
from grelmicro.coordination.kubernetes import (
KubernetesLeaderElectionAdapter,
KubernetesLockAdapter,
)
micro = Grelmicro(
uses=[
Coordination(
lock=KubernetesLockAdapter(namespace="default"),
election=KubernetesLeaderElectionAdapter(namespace="default"),
)
]
)
from grelmicro import Grelmicro
from grelmicro.coordination import Coordination
from grelmicro.providers.sqlite import SQLiteProvider
sqlite = SQLiteProvider("locks.db")
micro = Grelmicro(uses=[Coordination(lock=sqlite)])
from grelmicro import Grelmicro
from grelmicro.providers.memory import MemoryProvider
# Memory keeps state in the process: tests and single-process apps.
micro = Grelmicro(uses=[MemoryProvider()])
Warning
Store connection URLs in a proper place, such as environment variables, not inline like the examples above.
A Provider resolves every primitive in one line: Coordination(redis) calls
redis.lock() for the lock backend, redis.readwritelock() for the read-write
lock backend, and redis.leaderelection() for the election backend. Set each
backend on its own with lock=, rwlock=, and election=, so locks can run on
one vendor and leader election on another. Each argument accepts a Provider, a
backend instance, or a zero-arg class. See Providers.
| Redis | PostgreSQL | Kubernetes | SQLite | Memory | |
|---|---|---|---|---|---|
| Use case | Production | Production | Production (K8s-native) | Home lab / Local testing | Testing only |
| Multi-node | Yes | Yes | Yes | No | No |
| Persistence | Yes | Yes | Yes (etcd-backed) | Yes | No |
| Extra infrastructure | Required | None if already in stack | None (uses existing K8s API) | None | None |
| Lock performance | Best | Good | Moderate | Good | Best |
Tip
Feel free to create your own backend and contribute it. The backend
protocols (LockBackend, ReadWriteLockBackend, LeaderElectionBackend,
ScheduleBackend) are exported from grelmicro.coordination.
Choosing a backend
Pick the backend that matches your deployment, not the fastest one on paper.
- Memory: use for tests and single-process apps. State lives in the process and disappears on restart. Never use it across nodes: each node holds its own locks and leader election is meaningless.
- Redis: use for distributed locks when you want the lowest latency. Acquire and renew are single round-trips, so this is the fastest distributed option. Reach for it when lock throughput matters and you already run or can add Redis.
- PostgreSQL: use when Postgres is already in your stack. It needs no extra infrastructure and gives transactional, durable locks. Slightly slower than Redis, but the right default when you want one fewer moving part.
- SQLite: use for a single node that needs persistent locks with no operational overhead. State survives restarts on local disk, but it does not coordinate across nodes. Good for home labs and single-instance deployments.
- Kubernetes: use for leader election in a Kubernetes-native deployment. It builds on the Kubernetes Lease API and reuses the existing API server, so no extra infrastructure is needed. It guarantees one holder at a time within the configured lease, backed by etcd. It does not give you the low-latency, high-throughput locking of Redis: prefer it for coarse leader election, not for hot-path resource locks.
Reference
See the API reference for every option.