Skip to content

Providers

A Provider is a first-class connection object. It owns the vendor URL, the native client (a Redis pool, an asyncpg pool, ...), and the lifecycle of both. Components like Coordination, Cache, and RateLimiterRegistry accept a Provider directly and use its matching adapter under the hood.

Five providers ship today: RedisProvider, ValkeyProvider, PostgresProvider, SQLiteProvider, and MemoryProvider. More will follow.

Pass a Provider to every Component that needs the same connection:

from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.coordination import Coordination
from grelmicro.providers.redis import RedisProvider
from grelmicro.resilience import RateLimiterRegistry

redis = RedisProvider("redis://localhost:6379/0")

micro = Grelmicro(uses=[
    Coordination(redis),
    Cache(redis),
    RateLimiterRegistry(redis),
])

async with micro:
    ...

Components dispatch to the Provider's factory methods (provider.lock(), provider.cache(), provider.ratelimiter()). The Adapter classes (RedisLockAdapter, RedisCacheAdapter, RedisRateLimiterAdapter) stay public as escape hatches but rarely appear in user code.

Import policy: prefer Providers over concrete adapters

App code should import a Provider and pass it to Components and Registries, not import concrete adapter classes. The Provider owns the connection and hands each Component the right adapter, so one URL change swaps every backend at once. Import an adapter directly only for an escape hatch: a bespoke client the factory does not build, or a per-process Memory backend in a test. Adapters live in their backend submodule (grelmicro.resilience.circuitbreaker.sqlite) and the top-level package re-exports them (from grelmicro.resilience import SQLiteCircuitBreakerAdapter).

Listing the Provider is optional

A Provider held by a Component is discovered and lifecycled for you, so you can drop the top-level entry and let the Components carry it:

micro = Grelmicro(uses=[
    Coordination(redis),
    Cache(redis),
    RateLimiterRegistry(redis),
])

The shared redis opens once, before the Components that hold it. Listing it explicitly is still valid and lets you control where it sits in the lifecycle order.

Recipe 1: env-driven

Construct the Provider without arguments and let it read REDIS_* from the environment:

from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.coordination import Coordination
from grelmicro.providers.redis import RedisProvider

redis = RedisProvider()  # reads REDIS_URL or REDIS_HOST + REDIS_PORT + ...

micro = Grelmicro(uses=[
    Coordination(redis),
    Cache(redis),
])

Set REDIS_URL (or REDIS_HOST + REDIS_PORT + REDIS_DB + REDIS_PASSWORD) in the environment.

Recipe 2: split pools by env prefix

Two Redis instances (or two databases) live behind different prefixes. Each prefix gets its own Provider:

cache_redis = RedisProvider(env_prefix="CACHE_REDIS_")
session_redis = RedisProvider(env_prefix="SESSION_REDIS_")

micro = Grelmicro(uses=[
    cache_redis,
    session_redis,
    Coordination(session_redis),
    Cache(cache_redis),
])

Set CACHE_REDIS_URL and SESSION_REDIS_URL (or the decomposed forms). The two components talk to two pools.

Recipe 3: bring your own client

You already own a Redis client (custom retry, sentinel, auth, or a testcontainers fixture). Wrap it with from_client:

import redis.asyncio as redis

from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.providers.redis import RedisProvider

client = redis.Redis(host="prod.cache", socket_timeout=5)
redis_provider = RedisProvider.from_client(client)  # caller owns the client

micro = Grelmicro(uses=[Cache(redis_provider)])

Pass own=True to hand ownership to the provider. It will close the client when the provider exits, useful in pytest fixtures:

@pytest.fixture
async def redis_provider(redis_container):
    async with RedisProvider.from_client(
        redis_container.get_client(), own=True
    ) as provider:
        yield provider

Construction forms

RedisProvider("redis://localhost:6379")      # positional URL
RedisProvider(url="redis://...")             # keyword URL
RedisProvider(host="x", port=6379, db=0)     # decomposed kwargs
RedisProvider()                              # env-driven (REDIS_*)
RedisProvider(env_prefix="CACHE_REDIS_")     # custom env prefix
RedisProvider(env_load=False)                # kwargs only, no env
RedisProvider.from_config(RedisConfig(...))  # from a config object
RedisProvider.from_client(client)            # bring-your-own client

Sentinel and Cluster

RedisProvider switches topology from the URL scheme. The scheme rides the same url field, so REDIS_URL alone selects standalone, Sentinel, or Cluster with no other code change. ValkeyProvider reads the same schemes and builds the Valkey equivalents.

Standalone stays as before:

RedisProvider("redis://localhost:6379/0")

Sentinel lists the Sentinel hosts in the authority. The first path segment is the master service name. An optional second segment is the database index.

RedisProvider("redis+sentinel://host1:26379,host2:26379/mymaster/0")

Cluster lists the seed nodes. The client discovers the rest of the topology from them.

RedisProvider("redis+cluster://host1:6379,host2:6379")

Credentials in the URL userinfo apply to both the Sentinel connections and the data connections. Use the factory methods when the Sentinel password differs from the data password:

RedisProvider.sentinel(
    sentinels=[("host1", 26379), ("host2", 26379)],
    service_name="mymaster",
    db=0,
    password="data-password",
    sentinel_kwargs={"password": "sentinel-password"},
)

RedisProvider.cluster(
    nodes=[("host1", 6379), ("host2", 6379)],
    password="cluster-password",
)

safe_url and repr() redact the password for every scheme, including the multi-host forms.

Failover on Sentinel

The Sentinel client re-resolves the master when it changes. During that brief window an in-flight command can error. Wrap the call in the resilience patterns (retry and circuit breaker) to ride through the failover.

The hash-tag rule on Cluster

A Redis Cluster shards keys across slots by a hash of the key. A command or script that touches several keys must keep them in one slot, or the cluster rejects it as a cross-slot error.

The cache adapter and the lock adapter both run multi-key operations. On Cluster, give their prefix a hash tag so every key they touch lands in one slot. A hash tag is any substring in braces: the cluster hashes only what is inside the first {...}.

provider = RedisProvider("redis+cluster://host1:6379,host2:6379")
cache = provider.cache(prefix="{myapp}cache")
lock = provider.lock(prefix="{myapp}")

Without a hash tag, the adapter raises a ValueError at construction that names the fix. Standalone and Sentinel need no hash tag, since every key lives on one server. The rate limiter, circuit breaker, schedule, and leader-election adapters touch one key per call and work on Cluster as is.

Factory methods

Each Provider exposes factory methods that return its matching adapter:

Method Returns RedisProvider ValkeyProvider PostgresProvider SQLiteProvider MemoryProvider
.lock(**kwargs) LockBackend implementation
.schedule(**kwargs) ScheduleBackend impl
.leaderelection(**kwargs) LeaderElectionBackend impl N/A
.cache(**kwargs) CacheBackend implementation
.ratelimiter(**kwargs) RateLimiterBackend impl
.circuitbreaker(**kwargs) CircuitBreakerBackend impl

Factories that do not apply raise NotImplementedError with a message pointing to the right alternative. Coordination(provider), Cache(provider), RateLimiterRegistry(provider), and CircuitBreakerRegistry(provider) call these factories.

Readiness check

Every connection provider ships a built-in check() readiness probe: Redis and Valkey run PING, Postgres and SQLite run SELECT 1, and Memory returns ready right away. A HealthChecks registers it as a provider:{short_name} check, one provider at a time with health.add_provider(provider) or for the whole app with HealthChecks(auto_health=True). See Health Checks.

Valkey

ValkeyProvider is a subclass of RedisProvider. It connects to a Valkey server using the valkey-py client (valkey.asyncio) and serves the same adapter set as RedisProvider: Lock, LeaderElection, Schedule, TTLCache, RateLimiter, and CircuitBreaker.

Install the valkey extra before using it:

pip install "grelmicro[valkey]"
from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.coordination import Coordination
from grelmicro.providers.valkey import ValkeyProvider
from grelmicro.resilience import RateLimiterRegistry

valkey = ValkeyProvider("redis://localhost:6379/0")

micro = Grelmicro(uses=[
    Coordination(valkey),
    Cache(valkey),
    RateLimiterRegistry(valkey),
])

Set VALKEY_URL (or VALKEY_HOST + VALKEY_PORT + VALKEY_DB + VALKEY_PASSWORD) for env-driven construction.

Construction forms:

ValkeyProvider("redis://localhost:6379")     # positional URL
ValkeyProvider(url="redis://...")            # keyword URL
ValkeyProvider(host="x", port=6379, db=0)   # decomposed kwargs
ValkeyProvider()                             # env-driven (VALKEY_*)
ValkeyProvider(env_prefix="CACHE_VALKEY_")  # custom env prefix
ValkeyProvider(env_load=False)              # kwargs only, no env
ValkeyProvider.from_config(RedisConfig(...))  # from a config object
ValkeyProvider.from_client(client)           # bring-your-own client

ValkeyProvider reads the same redis+sentinel:// and redis+cluster:// schemes as RedisProvider and builds the Valkey Sentinel and Cluster clients. The factory methods ValkeyProvider.sentinel(...) and ValkeyProvider.cluster(...) and the Cluster hash-tag rule apply the same way.

Postgres

PostgresProvider ships all factory methods: .lock(), .leaderelection(), .cache(), .ratelimiter(), .circuitbreaker(), and .schedule(). The provider wraps an asyncpg.Pool and opens it lazily on __aenter__.

from grelmicro import Grelmicro
from grelmicro.coordination import Coordination
from grelmicro.providers.postgres import PostgresProvider

postgres = PostgresProvider("postgresql://localhost/app")

micro = Grelmicro(uses=[
    Coordination(postgres),
])

Set POSTGRES_URL (or POSTGRES_HOST + POSTGRES_PORT + POSTGRES_DB + POSTGRES_USER + POSTGRES_PASSWORD) for env-driven construction.

For two pools (writer + reader), split by env prefix:

write = PostgresProvider(env_prefix="WRITE_POSTGRES_")
read = PostgresProvider(env_prefix="READ_POSTGRES_")

micro = Grelmicro(uses=[
    write,
    read,
    Coordination(write),
    Coordination(read, name="read"),
])

Construction forms:

PostgresProvider("postgresql://localhost/app")  # positional URL
PostgresProvider(url="postgresql://...")        # keyword URL
PostgresProvider(host="db", port=5432, database="app", user="u", password="pw")
PostgresProvider()                              # env-driven (POSTGRES_*)
PostgresProvider(env_prefix="WRITE_POSTGRES_")  # custom env prefix
PostgresProvider(env_load=False)                # kwargs only, no env
PostgresProvider.from_config(PostgresConfig(...))
PostgresProvider.from_client(pool)              # bring-your-own pool

SQLite

SQLiteProvider ships the .lock(), .ratelimiter(), .cache(), .circuitbreaker(), and .schedule() factories. The provider owns one aiosqlite connection (autocommit, WAL) and a shared lock that adapters borrow.

from grelmicro import Grelmicro
from grelmicro.providers.sqlite import SQLiteProvider
from grelmicro.resilience import RateLimiterRegistry

sqlite = SQLiteProvider("app.db")

micro = Grelmicro(uses=[
    RateLimiterRegistry(sqlite),
])

Set SQLITE_PATH for env-driven construction. Construction forms:

SQLiteProvider("app.db")                  # positional path
SQLiteProvider(path="app.db")             # keyword path
SQLiteProvider()                          # env-driven (SQLITE_PATH)
SQLiteProvider(env_prefix="CACHE_SQLITE_")  # custom env prefix
SQLiteProvider(env_load=False)            # kwargs only, no env
SQLiteProvider.from_config(SQLiteConfig(...))
SQLiteProvider.from_client(connection)    # bring-your-own connection

Lifecycle

The Provider is opened when the Grelmicro app enters and closed when the app exits. Components borrow the Provider's client without managing its lifecycle.

Always list the Provider before the Components that depend on it. uses= opens items in declaration order. PostgresProvider builds its asyncpg.Pool on __aenter__, so a Component placed before its Provider would access provider.client before the pool exists and raise OutOfContextError. Grelmicro.__aenter__ warns on this ordering, but the correct fix is to list the Provider first.

Memory

MemoryProvider ships every factory: .lock(), .leaderelection(), .schedule(), .cache(), .ratelimiter(), and .circuitbreaker(). It owns no connection. State lives in process and disappears on restart, so it is for tests and single-process apps. Reach for Redis, Postgres, or SQLite for durable, distributed coordination.

from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.coordination import Coordination
from grelmicro.providers.memory import MemoryProvider
from grelmicro.resilience import CircuitBreakerRegistry, RateLimiterRegistry

memory = MemoryProvider()

micro = Grelmicro(uses=[
    memory,
    Coordination(lock=memory.lock(), election=memory.leaderelection()),
    Cache(memory.cache()),
    RateLimiterRegistry(memory.ratelimiter()),
    CircuitBreakerRegistry(memory.circuitbreaker()),
])

Each factory hands back one cached adapter per kind, so the provider owns a single in-process store per kind. memory.lock() called twice returns the same backend, so a later call re-fetches the live store for a test or an introspection. Wire each kind into one component, the same way you would a Redis adapter. A lone MemoryProvider resolves every kind, so uses=[memory, Coordination(memory)] wires the lock, election, and schedule backends from it.

To wire a single component, pass the provider straight in:

from grelmicro import Grelmicro
from grelmicro.coordination import Coordination
from grelmicro.providers.memory import MemoryProvider

memory = MemoryProvider()

micro = Grelmicro(uses=[
    Coordination(memory),
])

You can still pass a raw adapter (MemoryLockAdapter, MemoryCacheAdapter, ...) to its Component when you do not want a provider.