Changelog
0.28.2 - 2026-07-05
Fixed
- 🐛 Make idempotency storage atomic. The fingerprint guard is now written before the response and outlives it, so a failed guard write can never leave a response that a different payload could replay. (#494)
- 🐛 Guard the process-global active-app registry with a lock so two apps starting concurrently cannot both enter and clobber each other's
Log/Trace/Metricsstate. The second raisesMultipleActiveAppsError. (#494) - 🐛 Roll back an opened
Grelmicroapp when a later FastStream startup hook fails, so a failed startup no longer leaves the app open and registered. (#494) - 🐛 Export
SQLiteCircuitBreakerAdapterfromgrelmicro.resilience, matching the other circuit breaker adapters. (#494)
Security
- 🔒 Make the
TaskLocktoken nonce unpredictable (a process-local counter joined with random bytes) so an untrusted in-process caller cannot forge another handle's ownership token. (#494)
Docs
- 📝 Point every ambient-miss
OutOfContextErroratmicro.install(app), and switch the README and first-steps examples to the one-callmicro.install(app)form. (#494) - 📝 Add a first-use mental model, an operator defaults reference, an API conventions page, an adapter import policy, and decision tables for rate limiter methods and task entry points. (#494)
- 📝 Warn that
/healthzalways returns each check'serrorstring, and that cache keys and tags derived from untrusted input must stay bounded. (#494)
0.28.1 - 2026-07-05
Features
- ✨ Make
Tracea true no-op when the exporter auto-disables. With the defaultautoexporter and no endpoint,Traceinstalls no tracer provider, leaves the global untouched, runs no auto-instrumentation, and no longer counts against the single-active-app guard, soTrace()is safe to register unconditionally in dev, test, and CI. An explicitexporter=nonestill installs the provider. (#487)
Fixed
- 🐛 Block a second app that installs
Metricswhile one is active, matchingLogandTrace.Metricsowns the process-global meter provider, so two overlapping apps would clobber it.
Docs
- 📝 Refresh stale docs: the optional-extras table, the capability matrix, the module lists, the
@cachedlockdefault, and theDuplicateFilterttlfield.
0.28.0 - 2026-06-28
Breaking
- 💥 Rename the recurring-task decorator from
@task.interval(seconds=...)to@task.every(seconds=...), pairing it with@task.cron(...)as a verb-form family. Theseconds=keyword is unchanged. Update your call sites. - 💥 Move the framework integration modules into
grelmicro.integrations.grelmicro.fastapibecomesgrelmicro.integrations.fastapi, soGrelmicroMiddlewareis nowfrom grelmicro.integrations.fastapi import GrelmicroMiddleware, and the new FastStream wiring lives atgrelmicro.integrations.faststream. - 💥 Move the FastAPI health router from
grelmicro.health.fastapitogrelmicro.integrations.fastapi, so all FastAPI integration code (middleware, install, health router) lives undergrelmicro.integrations. Updatefrom grelmicro.health.fastapi import health_routertofrom grelmicro.integrations.fastapi import health_router. - 💥 Collapse the four
@task.everylock passthroughs into one typedlock=TaskLock(...). Thelease_duration,min_hold_duration,backend, andworkerparameters are removed. Pass aTaskLockinstead, like@task.every(seconds=60, lock=TaskLock(lease_duration=300)). The lock keeps its default"default"name and is re-stamped to the task name, so you never repeat it. The lock's settings are authoritative.leader=andsync=stay separate, self-documenting parameters. - 💥
HealthChecksnow namespaces its env vars per instance: the default instance keepsGREL_HEALTH_*, but a named instance readsGREL_HEALTH_{NAME_UPPER}_*(matchingLock,CircuitBreaker, and the other named components). Update env vars for any namedHealthChecks. - 💥 The log filters now namespace env vars per instance.
GREL_DUPLICATE_FILTER_*becomesGREL_DUPLICATEFILTER_*andGREL_RATE_LIMIT_FILTER_*becomesGREL_RATELIMITFILTER_*, with a newenv_name=kwarg for a named instance (GREL_DUPLICATEFILTER_{NAME}_*).DuplicateFilter.key_modealso gainslogger,level, andglobalso both filters share one vocabulary. Update env vars andenv_prefix=overrides. - 💥 Replace the six manual circuit-breaker control methods with two operator verbs.
isolate()forces the breaker open until reset,reset()returns it to normal automatic operation startingCLOSED. Replacetransition_to_forced_open()withisolate()andrestart()withreset(). The remainingtransition_to_closed,transition_to_open,transition_to_half_open, andtransition_to_forced_closedare removed. - 💥
CircuitBreakerMetricsandErrorDetailsare now frozen slotted dataclasses instead of Pydantic models. Attribute access is unchanged, but they no longer offer.model_dump()or Pydantic validation. This matchesCircuitBreakerSnapshotandCacheInfo(read-models are dataclasses, Pydantic is reserved for serialization boundaries). - 💥
@cachednow folds concurrent misses of the same key in-process by default (lock="local"instead oflock=False). This removes a silent thundering-herd footgun at no I/O cost. Passlock=Falseto restore the old behavior where every concurrent miss recomputes. - 💥 Move the backend protocols out of the public
abcsubmodules into private_protocolmodules (clock,config,coordination,task), matchingcacheandresilience.VirtualClocklikewise moves toclock/_virtual.py. The protocols stay exported from each package, so import them from the package (from grelmicro.coordination import LockBackend) instead ofgrelmicro.coordination.abc. - 💥
reconfigure()now raisesCoordinationSettingsValidationError(aSettingsValidationError, still aValueErrorsubclass) instead of a bareValueErrorwhen a new config would change the immutableworker. CatchSettingsValidationErrororGrelmicroErrorto handle it. - 💥 Rename the
ExternalConfigintervalparameter toreload_interval, tying the knob to thereload()verb it controls. UpdateExternalConfig(interval=...)toreload_interval=. - 💥 Rename the log dedup
ttl_secondsfield tottl, matching the bare-noun duration convention used everywhere else (the cachettl, lock durations). UpdateDuplicateFilter(ttl_seconds=...)andDuplicateFilterConfigtottl=. - 💥 Rename the
Tracecomponent symbols to theTracestem so they match the component name.TracingConfig,TracingError,TracingExporterType,TracingProcessorType,TracingSamplerType, andTracingSettingsValidationErrorbecomeTraceConfig,TraceError,TraceExporterType,TraceProcessorType,TraceSamplerType, andTraceSettingsValidationError. Update imports. - 💥 Rename the
Logcomponent symbols to theLogstem so they match the component name.LoggingConfig,LoggingError,LoggingBackendType,LoggingFormatType,LoggingLevelType,LoggingSerializerType,LoggingTimeZoneType, andLoggingSettingsValidationErrorbecomeLogConfig,LogError,LogBackendType,LogFormatType,LogLevelType,LogSerializerType,LogTimeZoneType, andLogSettingsValidationError. Update imports. - 💥 Rename the
TaskLocklease boundaries to lease-anchored names, matchingLockConfigand the KubernetesLeaseDuration.max_lock_secondsbecomeslease_durationandmin_lock_secondsbecomesmin_hold_duration. This covers theTaskLockConfigfields and theTaskLockconstructor. UpdateTaskLock(max_lock_seconds=..., min_lock_seconds=...)tolease_duration=..., min_hold_duration=...and the env varsGREL_TASKLOCK_{NAME_UPPER}_MAX_LOCK_SECONDSand_MIN_LOCK_SECONDSto_LEASE_DURATIONand_MIN_HOLD_DURATION. - 💥 Rename the concrete leader election adapters to the
*Adaptersuffix every other pattern already uses.MemoryLeaderElectionBackend,RedisLeaderElectionBackend,PostgresLeaderElectionBackend, andKubernetesLeaderElectionBackendbecomeMemoryLeaderElectionAdapter,RedisLeaderElectionAdapter,PostgresLeaderElectionAdapter, andKubernetesLeaderElectionAdapter. TheLeaderElectionBackendprotocol keeps its name (protocol stays*Backend, concrete stays*Adapter). Update direct imports and constructions. - 💥 Rename the resilience component wrappers to the singular
*Registryform, matching their singularkindand theCoordinationandCachesiblings.RateLimitersbecomesRateLimiterRegistryandCircuitBreakersbecomesCircuitBreakerRegistry. Updateuses=[...]and imports. - 💥 Drop the redundant
DEFAULTsegment from the default instance env prefix. A default instance (Lock("default"),Retry.exponential("default"), and the like) now reads the bareGREL_{COMPONENT}_{FIELD}instead ofGREL_{COMPONENT}_DEFAULT_{FIELD}. Rename env vars likeGREL_LOCK_DEFAULT_LEASE_DURATIONtoGREL_LOCK_LEASE_DURATION. Named instances are unchanged. The default instance now owns the bareGREL_{COMPONENT}_namespace, so name your other instances to avoid clashing with a field name (aLock("lease")would shareGREL_LOCK_LEASE_DURATIONwith the default instance). - 💥 Raise
OutOfContextErrorwith an actionable message on every ambient backend miss:Lock,TaskLock,LeaderElection,TTLCache,@cached, the cron schedule resolution, andIdempotencynow matchCircuitBreakerandRateLimiter.NoActiveAppErrorstays the low-level error raised byGrelmicro.current()itself. - 💥 Remove the implicit memory fallback on
CircuitBreakerandRateLimiter. Backend resolution is now one rule on every pattern: explicitbackend=wins, else the active app's component, elseOutOfContextError. For a per-process limiter or breaker without an app, passbackend=MemoryRateLimiterAdapter()orbackend=MemoryCircuitBreakerAdapter()(both import fromgrelmicro.resilience). Inside FastAPI handlers, addGrelmicroMiddlewareso ambient resolution works there.RateLimiter.reconfigurenow publishes the config and rebinds the strategy lazily on the next call, matchingCircuitBreaker. - 💥 Add positional argument capture to
grelmicro.testing:Callis nowCall(method, args=..., kwargs=...)andCallLog.countmatches positional arguments too. Update directCall(...)constructions. - 💥 Align the leader election and task lock env var prefixes with their single-token names:
GREL_LEADER_ELECTION_{NAME}_becomesGREL_LEADERELECTION_{NAME}_andGREL_TASK_LOCK_{NAME}_becomesGREL_TASKLOCK_{NAME}_. Update any environment variables set for these components. PR #346. - 💥 Rename the pattern factory methods so each uses the pattern's single-token name:
Provider.breaker()becomescircuitbreaker(),leader_election()becomesleaderelection()on bothProviderandCoordination, andCoordination.task_lock()becomestasklock(). This matches the module names (grelmicro.coordination.leaderelection,grelmicro.coordination.tasklock,grelmicro.resilience.circuitbreaker) and theratelimiter/circuitbreakerkind strings. Updateprovider.circuitbreaker(),micro.coordination.leaderelection(...), andmicro.coordination.tasklock(...)call sites. PR #343, PR #344. - 💥 Make
Log,Trace, andMetricssingletons. Each configures process-global state (the root logger, the OpenTelemetry tracer and meter providers), so registering a second one on the same app now raisesComponentAlreadyRegisteredErrorinstead of silently clobbering the first. PR #343. - 💥 Make the component
namea read-only property everywhere (Coordination,Cache,Log,Trace,Metrics,RateLimiters,CircuitBreakers,HealthChecks,RealClock,VirtualClock), matching the resilience and coordination primitives. Passname=at construction. PR #343.
Features
- ✨ Trace FastStream messages.
micro.install(faststream_app)now wires the broker's OpenTelemetry telemetry middleware against the app's tracer, so consumed and published messages get spans with no per-handler decoration. Selected by the sameTrace(instrument=...)directive under thefaststreamname, and a no-op when the broker's faststream telemetry support is not installed. (#470) - ✨ Trace Valkey commands. grelmicro ships a first-party
ValkeyInstrumentor(valkey-py has no official OpenTelemetry package), registered as a standardopentelemetry_instrumentorentry point soTrace(instrument=...)discovers it like any other library, andValkeyProvideruses it. It reuses the Redis span factories against thevalkey.*classes, so Valkey spans match the Redis ones. (#479) - ✨ Trace any library the app uses, not just grelmicro-managed providers.
Trace(instrument=True)now sweeps every installedopentelemetry-instrumentation-*package and attaches it to the app's tracer, so an app's own SQLAlchemy or asyncpg engine, httpx client, and the like are traced with no grelmicro provider. The set of installed instrumentors defines coverage (no new hard dependency), names follow the OpenTelemetry instrumentor names, and the asyncpg/SQLAlchemy pair is de-duplicated to avoid double spans. (#479) - ✨ Fail fast on a missing ambient binding.
micro.install(app, ambient=False)now warns at startup when ambient-resolving components are registered (it raisesAmbientBindingErrorunderGrelmicro(strict=True)), and the newmicro.check_ambient_binding(app)returns whether the binding middleware is wired so a test can catch a forgottenmicro.install(app)before it 500s on the first request. (#471) - ✨ Auto-disable
Traceuntil an endpoint is configured. The exporter now defaults toTraceExporterType.AUTO, which exports over OTLP HTTP when an endpoint is set (theendpointargument,OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, orOTEL_EXPORTER_OTLP_ENDPOINT) and no-ops otherwise. RegisterTrace()unconditionally and it stays silent in dev, test, and CI instead of falling back tolocalhost:4318. (#476) - ✨ Add first-class HTTP Basic auth to
Trace. Passbasic_auth=(username, password)or setGREL_TRACE_BASIC_AUTH_USERNAMEandGREL_TRACE_BASIC_AUTH_PASSWORD, and theAuthorization: Basicheader is built and attached to the exporter directly, bypassing the fragileOTEL_EXPORTER_OTLP_HEADERSencoding. (#476) - ✨ Add native auto-instrumentation to
Trace.Trace(instrument=...)traces incoming FastAPI requests and Redis and Postgres calls against the app's tracer provider, no per-handler decoration. On by default, a no-op until the newinstrumentationextra is installed. PassFalse, a name or list to select, or a{name: False}map to exclude. Redis attaches per-client, asyncpg is patched process-wide, and Valkey and SQLite stay on@instrument. - ✨ Add
RateLimiter.wait(), a blocking admission verb that waits until tokens are available then consumes them. It polls on the clock seam (soVirtualClockdrives it in tests), waits as long as needed by default, and raisesRateLimitExceededErroronce an optionalmax_waitbudget is exceeded. Acostlarger than the limit raisesValueErrorinstead of waiting forever. - ✨ Add
LeaderElection.lead(func, *, repeat=False), which runs a coroutine only while the worker holds leadership and cancels it the instant leadership is lost, so no stale work outlives the lease. It returns the body's result if it finishes while still leader, orNoneif cancelled. Passrepeat=Trueto re-run after re-acquiring leadership. - ✨ Add
micro.install(app), one call that wires the lifecycle and per-handler ambient binding for Starlette, FastAPI, and FastStream. Passambient=Falseto skip the binding. - ✨ Accept a
timedeltafor the intervalseconds=, like@task.every(seconds=timedelta(minutes=2)). A plain number of seconds still works. - ✨ Add a
key=template to@cachedfor a stable, readable cache key rendered from the arguments, like@cached(key="user:{user_id}"). Passkey_maker=for the fully dynamic case. Passing both raisesTypeError. - ✨ Type
FireInfo.outcomeas the newFireOutcomeStrEnum(SUCCESS,ERROR,SKIPPED). String comparisons likeoutcome == "success"still work. - ✨ Add
Idempotency.run(key, factory), a one-call helper that runs an operation once and replays its response. It takes a sync or async factory and mirrorsTTLCache.get_or_set. - ✨ Every component now raises a typed
*SettingsValidationErrorfor invalid configuration, rooted in the sharedSettingsValidationErrorbase. AddsTraceSettingsValidationError,HealthSettingsValidationError,LogSettingsValidationError, andIdempotencySettingsValidationError. CatchSettingsValidationErrorto handle any of them. - ✨ Cache adapters (
MemoryCacheAdapter,RedisCacheAdapter,PostgresCacheAdapter,SQLiteCacheAdapter) now declare theCacheBackendprotocol explicitly, matching the lock, circuit breaker, and rate limiter adapters. - ✨ Add
Log.from_config,Trace.from_config, andMetrics.from_configto build each component from a pre-built config, matching the declarative path on every other pattern. Theconfig=kwarg still works. - ✨ Add
MemoryProviderso Memory has the same provider-direct surface (memory.lock(),memory.cache(), ...) as Redis, Postgres, and SQLite. - ✨ Add a built-in readiness check per provider. Every connection provider ships a cheap
check()probe (Redis and ValkeyPING, Postgres and SQLiteSELECT 1). Register it withhealth.add_provider(redis)as a criticalprovider:redischeck, or register one for every active provider at once withHealthChecks(auto_health=True).Grelmicro.providerslists the active providers. - ✨ Default the rate limiter
keyto"default"onacquire,acquire_or_raise,allow,peek, andreset, so the single-bucket case isawait limiter.allow(). The limiternamealready namespaces the backend key. - ✨ Add the zero-object
@cached(ttl=30)form for plain memoization: it binds a private process-localTTLCacheat decoration, never resolves the active app, and never shares across replicas. Pass aTTLCachefor shared state. Passing bothcacheandttlraisesTypeError. - ✨ Add the OpenSSF Scorecard workflow and badge.
- ✨ Make
Grelmicro(uses=[...])andmicro.use(...)forgiving: a bare Component class is constructed for you, a bare adapter (class or instance) is wrapped in its matching Component, and a bare Provider with no Components auto-registers one default Component per kind it serves. The explicit form always wins, so any explicit Component turns provider auto-registration off entirely. - ✨ Add
AmbiguousProviderError, raised whenuses=[...]lists two bare Providers with no Components, so the default Component for a shared kind would be ambiguous. Wrap each Provider in the Components it should serve to resolve it. - ✨ Add the Idempotency pattern: a new
grelmicro.idempotencymodule with anIdempotencyprimitive and@idempotentdecorator. A caller-provided key (anIdempotency-Keyheader) executes the operation once, stores the response forttlseconds, and replays it on repeats. Duplicates arriving mid-flight fold into the first execution, across replicas when a Coordination lock backend is configured. A failure stores nothing, so a retry executes fresh. An optionalfingerprint=rejects a reused key with a different payload viaIdempotencyConflictError. Storage rides the cache layer (cache=or the active app'sCachecomponent). - ✨ Add Redis Sentinel and Redis Cluster support:
redis+sentinel://host1:26379,host2:26379/serviceandredis+cluster://host1,host2URL schemes onRedisProvider, plusRedisProvider.sentinel(...)andRedisProvider.cluster(...)factories, so one URL switches topology. On Cluster, the multi-key cache and lock operations require a hash-tagged prefix (prefix="{app}cache"), enforced with a clear error at construction. - ✨ Add Valkey support: a
ValkeyProvideringrelmicro.providers.valkey(extravalkey) serves the full Redis adapter column (Lock, TaskLock, LeaderElection, Schedule, TTLCache, RateLimiter, CircuitBreaker) through thevalkeyclient. - ✨ Add the Externalized Configuration pattern: a new
grelmicro.configmodule with anExternalConfigcomponent that reconfigures live components from a mounted ConfigMap, Secret,.env, JSON, YAML, or TOML file (FileConfigAdapter, nested mappings flatten to env-style keys), polling on an interval with a publicreload()for an immediate pass. Sources are pluggable via theConfigBackendprotocol. Every named pattern built imperatively registers under itsGREL_{PATTERN}_{NAME}_keys, includingCircuitBreaker(GREL_CIRCUITBREAKER_{NAME}_) andRateLimiter(GREL_RATELIMITER_{NAME}_). Instances built from a pre-built config stay static. Validation warnings log field names only, never values. - ✨ Add
GrelmicroMiddlewareingrelmicro.fastapi: a pure ASGI middleware that binds the active app inside request handlers, soLock("cart"),RateLimiter.sliding_window(...), and@cachedresolve ambiently in handlers without explicitbackend=wiring. - ✨ Add a bounded wait to
Lock.acquire(timeout=), raisingTimeoutErrorat the deadline, andLock.extend()to renew the lease of a held lock without releasing it. Both are mirrored on thefrom_threadfacade. - ✨ Add
TaskLock.refresh()so a task body that may outrunmax_lock_secondscan renew its claim, raisingLockNotOwnedErrorwhen the claim was lost. - ✨ Add
retry_jittertoLockandLeaderElection(default 0.1): each retry sleepsretry_interval * uniform(1 - jitter, 1 + jitter), so contending workers spread their attempts instead of retrying in lockstep. - ✨ Add scheduler introspection:
next_fire_timeandlast_fireon interval and cron tasks, withFireInfo(started_at, outcome, duration) exported fromgrelmicro.task. - ✨ Add
Match.explain()returning the human-readable matcher tree, and warn once when a predicate returns a non-bool value. - ✨ Add a shared
AdmissionErrorbase so every gatekeeping rejection is catchable with oneexcept.RateLimitExceededError,BulkheadFullError,CircuitBreakerError, andWouldBlockErrornow inherit it, soexcept AdmissionErrorhandles a rate limiter over budget, a full bulkhead, an open circuit breaker, or a non-blocking lock that would block. It is purely additive: the existing per-primitiveexceptclauses still work. PR #354. - ✨ Add
RateLimiter.allow(key=...)returning aboolfor the common served-or-throttled branch, and makeRateLimitResulttruthy (bool(result)isresult.allowed).if await limiter.acquire(key=...):now reads as the decision whileretry_afterandremainingstay available on the result. PR #354. - ✨ Add serve-stale-on-error to the cache with
stale_ttl=on@cached,get_or_set, andTTLCache.set. Each value keeps a fallback copy forttl + stale_ttlseconds, so a recompute that fails after the TTL serves the last good value instead of raising, up tostale_ttlseconds late. A flaky upstream degrades to slightly stale data instead of an error storm. It composes withlockandearly, an explicit delete or tag invalidation drops the fallback, and each stale serve records thegrelmicro.cache.stale_servesmetric. PR #350. - ✨ Add SQLite cache and circuit breaker backends, completing the SQLite column of the capability matrix (the circuit breaker coordinates single-host multi-process state). PR #349.
- ✨ Add a durable
@tasks.cron(expr, timezone="UTC")decorator that runs a task on a 5-field cron schedule (minute hour day-of-month month day-of-week). The parser is built in, with no external dependency, and supports*, steps, ranges, lists, and the7-as-Sunday alias. It uses standard Vixie day-of-month/day-of-week OR semantics. Each fire is claimed against a durable last-fire state via a newScheduleBackend(Memory, Redis, Postgres, and SQLite), so the task runs at most once across all workers per fire. A fire missed while every worker was down replays once on restart, bounded bymisfire_grace_seconds, and only the most recent missed fire runs. Wire it viaCoordination(provider)orCoordination(schedule=...). PR #348. - ✨ Add a time-based stop to
Retrywithmax_seconds=. Retrying stops as soon as eitherattemptsis reached or the wall-clock budget elapses, whichever comes first (attemptsstill defaults to 3). Available on theRetry.exponential/Retry.constantfactories, the constructor, andRetryConfig(env varGREL_RETRY_{NAME}_MAX_SECONDS). The budget reads the clock seam, soVirtualClockdrives it in tests. PR #347. - ✨ Re-export
FunctionTypeErrorandTaskAddOperationErrorfromgrelmicro.task, so the task errors users catch live next toTaskErrorinstead of only ingrelmicro.task.errors. PR #343. - ✨ Export the catch-all base
GrelmicroErrorand the cross-cuttingDependencyNotFoundError,OutOfContextError, andSettingsValidationErrorfrom the top-levelgrelmicropackage, soexcept GrelmicroErrorcatches any library error from one import. Re-exportWouldBlockErrorandCoordinationBackendErrorfromgrelmicro.coordination(the latter moved intogrelmicro.coordination.errors). PR #343.
Fixed
- 🐛 Name the failing source in the
ExternalConfigreload warning, so a broken config or secrets mount is no longer a generic warning. Each source loads under its own guard, so a config failure no longer hides a working secrets source. Source values are never logged. - 🐛 Raise an actionable
RuntimeErrorfrom a sync@cachedcall when the backend never captured a running loop, instead of an opaqueAttributeError. The message says to open the backend withasync with micro:first. - 🐛 Reconcile cache tags on every Redis
setandset_many, even with no tags. Re-setting a previously tagged key without tags now drops its stale tag membership, so a laterdelete_tagsno longer wrongly removes it. PR #353. - 🐛 Store the cache sidecar entries (the
early=refresh metadata, and the new stale reserve) under a\x1fseparator instead of\x00, so they are valid Postgres text keys.@cached(early=...)previously raised on a Postgres cache backend. PR #350.
Docs
- 📝 Add
docs/architecture/decorators.mddocumenting the bare@decoversus parametrized@deco(...)rule and which decorators wrap sync functions. PR #343.
0.27.0 - 2026-06-07
Breaking
- 💥 Replace
@cached(stampede="local" | "distributed" | None)with@cached(lock=False | True | "local").lock=Truefolds concurrent misses and picks the cross-replica path automatically when the active app has aCoordinationlock backend (in-process otherwise),lock="local"forces the in-process path, and the default is nowlock=False(no protection, opt in explicitly). Migratestampede="local"tolock="local",stampede="distributed"tolock=True, andstampede=Nonetolock=False. Issue #235. - 💥 Move
LeaderElectionout ofgrelmicro.syncinto a newgrelmicro.coordinationpackage. Import it fromgrelmicro.coordination.Sync.leader_election()is removed: register aCoordinationcomponent and callmicro.coordination.leader_election(...). Leader election now runs on a dedicatedLeaderElectionBackend, not the lockSyncBackend, so it can use a different vendor thanLock(Redis forLock, a Kubernetes Lease for leader election). Issue #223. - 💥 Unify
grelmicro.syncintogrelmicro.coordinationand deletegrelmicro.sync. ImportLock,TaskLock,LeaderElection, andCoordinationfromgrelmicro.coordination. TheSynccomponent is gone: use oneCoordinationcomponent, which exposes.lock(...),.task_lock(...), and.leader_election(...), and reach it onmicro.coordination. TheSyncBackendprotocol is nowLockBackend, the*SyncAdapterbackends are now*LockAdapter, and the provider factory.sync()is renamed to.lock(). Issue #223. - 💥 Make the JSON utilities internal. The
grelmicro.jsonmodule is removed. UseJsonSerializerfromgrelmicro.cachefor cache JSON, ororjsondirectly if you need raw fast JSON.
Features
- ✨ Add a
Metricscomponent that installs an OpenTelemetryMeterProviderfor the app's lifetime, with OTLP, Prometheus, console, and none exporters. A@measuredecorator times and counts any function,metrics_router()serves a Prometheus/metricsendpoint, and every built-in component (health, circuit breaker, retry, rate limiter, bulkhead, timeout, cache, tasks) emits its own metrics. All metric calls are no-ops without theopentelemetryextra or an active component. - ✨ Leader election leases carry a Kubernetes-style
LeaderRecord(holder, lease duration, acquire and renew times, leadership transitions, and free-form metadata). Read it fromLeaderElection.record, set the metadata viaLeaderElection(metadata=...). Metadata-storing backends ship for memory, Redis, Postgres, and Kubernetes Lease, resolved throughprovider.leader_election()or passed toCoordination(...)directly. Issue #223. - ✨ Add
grelmicro.testing.record(backend)for protocol-level call assertions. It instruments a backend's public async methods in place and returns aCallLog, so the backend keeps its type and behavior while every call is recorded. Assert withlog.count(method, **kwargs), inspectlog.methods(), or read the rawlog.calls. Works likepytest-mock'smocker.spy. Issue #271. - ✨ Add cache tags,
get_or_set, and batch operations. Tag entries viaset,set_many,get_or_set, or@cached(tags=["users", "user:{user_id}"]), then invalidate a whole group withdelete_tags.get_or_set(key, factory)computes a missing value once under the same stampede protection as@cached(lock=True).get_many,set_many, anddelete_manywork on many keys at once. Tags and batch ops run on Memory, Redis, and Postgres. - 📝 Correct the comparison page and capability matrix to show the Postgres and SQLite cache, rate limiter, and circuit breaker backends as shipped (they were stale-labeled "planned").
- 📝 Add a "what grelmicro is not" line to the README and docs landing for sharper first-read positioning.
- 🔧 Set the PyPI
Development Statusclassifier to4 - Beta. - ✨ Discover Providers and Adapters through entry-point groups. Third-party packages register under
grelmicro.providersandgrelmicro.{kind}.adapters(coordination,cache,ratelimiter,circuitbreaker) and resolve by short name, the same path first-party backends use. Unknown names raiseProviderNotRegisteredErrororAdapterNotRegisteredErrorlisting the installed names. Newdocs/architecture/plugins.mdand anexamples/third-party-adapter/skeleton. Issue #234. - ✨ Add
VirtualClockfor deterministic time in tests. Time-dependent primitives (Retrybackoff,CircuitBreakerhalf-open window,RateLimiterrefill,Shieldadaptive gate) read time through a clock seam (grelmicro.clock.monotonic/sleep). Install aVirtualClock(Grelmicro(uses=[clock, ...])orasync with VirtualClock()) and callclock.advance(seconds)to drive that behavior with no real waiting. With no clock registered, the seam forwards totime.monotonicandasyncio.sleep, so production keeps real time. Issue #272. - ✨ Auto-discover shared Providers in
Grelmicro(uses=[...]). A Provider held by a Component (Coordination(redis),Cache(redis)) no longer has to be listed separately: it is adopted and lifecycled exactly once, opened before the Components that hold it. Listing it explicitly stays valid and keeps control over lifecycle order. Issue #263.
Docs
- 📝 Lead every feature page with the simplest runnable example, then explain, moving deep theory into collapsible sections. Covers the resilience patterns and the cache, coordination, logging, health, tracing, and task guides.
0.26.0 - 2026-06-05
Breaking
- 💥 The
Taskprotocol's__call__now takes astop: asyncio.Event | None = Nonekeyword used for graceful shutdown. CustomTaskimplementations must accept it. The built-inintervaltasks andLeaderElectionare unaffected. Issue #187. - 💥 Replace the
@cached(lock=...)parameter with@cached(stampede="local" | "distributed" | None).lock=Truebecomesstampede="local"(now the default),lock=Falsebecomesstampede=None, and the custom-context-manager form is dropped in favor of the"distributed"cross-replica mode. Issue #235.
Features
- 📝 Add a runnable FastAPI demo under
examples/fastapi-demo/.docker compose up --waitstarts Redis, Postgres, and a FastAPI app that exercises every Pattern (cache, rate limiter, circuit breaker, distributed lock, leader-gated task, health probes), with aDemo SmokeCI job and ajust demoshortcut. Issue #166. - 📝 Add a ConfigMap-watcher example wiring
reconfigure()(docs/configuration/reconfigure-from-configmap.md), with aSIGHUPvariant for non-Kubernetes hosts. Issue #169. - ✨ Accept bare zero-arg classes in
Grelmicro(uses=[...]),micro.use(...), and theSync/Cache/RateLimiters/CircuitBreakersconstructors.uses=[MemorySyncAdapter]andSync(MemorySyncAdapter)now work without the trailing(), in the spirit of FastAPI'sDepends(dep). A class that needs constructor arguments raises a clear error. Issue #263. - ✨ Guard against two overlapping
Grelmicroapps clobbering process-global state. Opening a second app that registersLogorTracewhile another such app is active now raisesMultipleActiveAppsError. Apps without those components overlap freely. PassGrelmicro(allow_multiple=True)to opt out. Newdocs/architecture/multiple-apps.mddocuments the policy. Issue #266. - ✨ Add
Tasks(shutdown_timeout=...)for graceful shutdown. On exit,Taskssignals everyintervaltask to finish its current run and stop, force-cancelling only stragglers that outlast the timeout. The default30.0matches Kubernetes'terminationGracePeriodSeconds, andLeaderElectionreleases leadership on the same signal. Newdocs/architecture/graceful-shutdown.mdcovers signal wiring. Issue #187. - ✨ Add a three-layer cache stampede menu to
@cached.stampede="local"(default) folds concurrent same-key misses to one in-process run,stampede="distributed"coordinates across replicas through theSynccomponent, andearly=(XFetch) refreshes the hottest keys in the background before they expire so no caller blocks. Issue #235. - ✨ Add
LeaderElection.last_confirmation_age()(seconds since the last backend response that confirmed local leadership,Noneuntil first acquisition and after confirmed loss) andLeaderElection.is_leader_confirmed_within(max_age)(stricter variant ofis_leader()that requires a recent backend renewal). Theis_leader()docstring now spells out the advisory uncertainty window during a backend partition. - ✨ Add
Grelmicro(strict=True)to raiseLifecycleOrderErrorinstead of warning when a Component holds a Provider that is missing fromuses=or listed after the dependent Component. The defaultFalsepreserves the lenient warn-only behavior.LifecycleOrderErroris exported fromgrelmicro. - ✨ Add
Shieldresilience pattern: per-attempt timeout, retry-budget-gated retries, CUBIC-style adaptive rate limiter, optional cache and fallback recovery paths. Three profiles (internal,api,slow) cover the common cases. Decorator (@shield,@shield.api(...)), class (Shield.api("name")), and imperative (Shield.api("name").run(fn, ...)) forms supported. Issue #249. - ✨ Add
TTLCacheConfigand expose it viaTTLCache.config. Matches the frozen-config shape used by every other primitive. - ✨ Add
RedisProvider.safe_urlandPostgresProvider.safe_urlreturning the resolved URL with the password replaced by***. The new__repr__on both providers uses the safe form so credentials never leak through logs or tracebacks. - ✨ Add
TracingConfig.shutdown_timeout(default5.0seconds).Trace.__aexit__now runsTracerProvider.shutdown()in a thread with this deadline so a slow or broken exporter no longer hangs application shutdown. - ✨ Add
SQLiteProviderand SQLite rate limiting. UseRateLimiters(SQLiteProvider("app.db"))for file-backed limits on a single host. Each acquire runs a read-modify-write inside aBEGIN IMMEDIATEtransaction. Issue #173. - ✨ Add
PostgresCircuitBreakerAdapterfor fleet-wide circuit breaker state on Postgres, plusPostgresProvider.breaker()soCircuitBreakers(postgres)resolves it. Transitions run in PL/pgSQL functions guarded bypg_advisory_xact_lock. - ✨ Add
Bulkheadresilience pattern to cap concurrent in-flight calls.max_concurrentbounds concurrency,max_waitlets callers queue briefly before aBulkheadFullError(default fails fast), andmax_workersruns blocking work throughbulkhead.to_threadon a dedicated pool. Async context manager and decorator forms. Issue #168. - ✨ Add
Bulkhead(uses=[...])to scope Providers and Components to a bulkhead. Inside the scope, a Pattern resolving its default backend picks up the bulkhead's Component, isolating a business context onto its own pool. Explicitbackend=still wins. Issue #168.
Fixes
- 🐛 The README and
simple_fastapi_app.pyFastAPI examples now pass an explicitbackend=to patterns used inside request handlers. Request handlers run outside the app's ambientGrelmicro.current()scope, so the previous ambient form raisedNoActiveAppError(locks, cache) or silently fell back to an in-memory backend (rate limiter, circuit breaker) at runtime. BackgroundTaskskeep using ambient resolution. Ambient resolution in handlers is tracked in #328. - 🔒
SettingsValidationErrorno longer echoes the offending input value. Env-loaded credentials (DSNs, tokens) no longer surface in error messages. - 🚨
ComponentNotRegisteredErrorfromGrelmicro.get(kind, name)now lists every registered(kind, name)pair (or states that none are registered). Agents and developers see what is available without inspecting the container. - 🚨
HealthChecks.addinvalid-name errors now include valid examples ('redis','db-primary','weather:circuitbreaker') alongside the regex. - 🐛
Log.__aenter__andLog.__aexit__now serialize on a class-levelthreading.Lockso concurrentGrelmicrolifecycles in the same process cannot interleave the stdlib root-logger snapshot / restore sequence. - 🐛 Unexpected exceptions inside a health check now surface as
"TypeName: message"in theCheckResult.errorfield instead of the generic"Health check failed". Operators reading only the/healthzpayload can identify the failing class without grepping logs. - 🔒
Lock("...")now validates the name against^[A-Za-z0-9][A-Za-z0-9._:/-]*$(max 200 chars). Names with whitespace, control characters, or leading separators are rejected with a message that includes valid examples. Existing namespaced names (users:42,payments/eu,weather.svc) keep working. - ⚡
DuplicateFilternow sweeps entries older thanttl_secondsonce per window, so high-cardinality log floods stop evicting still-active keys by size pressure.
Docs
- 📝 Lead
README.mdanddocs/index.mdwith a one-route, one-primitive FastAPI example before the full composition demo. - 📝 Annotate
Grelmicro.use,Grelmicro.get,instrument, andCacheBackendprotocol parameters withAnnotated[..., Doc(...)]. - 📝 Align the
CONTRIBUTING.mddiscriminator rule with the code:kind(nottype). - 📝 Document the per-process scope of
Tasksand point atTaskLock/LeaderElectionfor cluster-wide scheduling. - 📝 Add a Kubernetes operational-assumptions section covering RBAC, API server availability, etcd latency, and single-cluster scope to
docs/architecture/kubernetes.md. - 📝 Fix the
sync.md → task.md#tasksinternal anchor somkdocs --strictno longer reports it. - 📝 Add a lifespan-only example (one provider, one component) between the minimal example and the full composition demo in
README.mdanddocs/index.md. - 📝 Drop unsupported claims and idioms from the landing copy ("Stop reinventing the wheel", "battle-tested in production", "TL;DR").
- 📝 Add
Start here/Common recipeslead lines to every page underdocs/reference/. - 📝 Add an explicit
Running testssection toCONTRIBUTING.mdwith the commands for unit-only, integration-only, and the full local gate. - 📝 Add a
What should I pick?decision tree to the top ofdocs/comparison.mdso readers can map their situation to the right tool (one primitive, two or more, task queue, workflow engine, web framework). - 📝 Add a
Your first contributionsection toCONTRIBUTING.mdwith the expected code, test, and docs shape and a pointer to thegood first issuelabel. - 📝 Add
Annotated[..., Doc(...)]to theSyncBackend,RetryStrategy,RateLimiterStrategy,RateLimiterBackend,CircuitBreakerStrategy, andCircuitBreakerBackendprotocol parameters so IDE and LLM tools surface the same hints on backends as on user-facing primitives. - 📝 Group the
grelmicro.resiliencepackage docstring into front doors, components, adapters, and configs so import-site hover help guides agents and humans to the preferred entry point. - 📝 Document that auto-generated task references (
module:qualname) surface in logs, distributed lock keys, and metric labels. Suggest passing an explicitname=for sensitive workflows invalidate_and_generate_referenceand thedocs/task.mdInterval Task section. - 📝 Add a
Why Python 3.12section todocs/installation.mdlisting the language features (PEP 695,asyncio.timeout) that drive the floor, and note that CI runs the matrix on every advertised classifier (3.12, 3.13, 3.14). - 📝 Add a
Platformscolumn to the Optional extras table indocs/installation.mdcalling out thatuvloopis skipped on Windows and PyPy. - 📝 Document
RateLimitResult.remainingas an estimate for continuous-state algorithms (GCRA-based sliding window). Enforcement still uses exact state, so the nextacquiremay be denied even whenremaining > 0. - 📝 Add a FastStream resilience recipe (
docs/snippets/resilience/faststream.py) that uses a fleet-wide per-keyLockand a sliding-windowRateLimiterinside a Redis-broker subscriber. Linked fromdocs/resilience/index.md. - 📝 Formalize the
test_<component>_<scenario>_<expected_outcome>test-name shape inCONTRIBUTING.mdwith three concrete examples. - 📝 Add
docs/benchmarks.mdwith reproducible request-path benchmarks for the rate limiter, circuit breaker, cache, and lock, plus runnable scripts underbenchmarks/. - 📝 Add a
Choosing a backendguide to the sync, cache, rate limiter, and circuit breaker pages. - 📝 Expand
docs/json.mdwith supported types, the orjson fallback, and serializer boundaries. - 📝 Note that the default OTLP HTTP trace exporter expects a running collector in
docs/tracing.md, withCONSOLEandNONEfor local development.
Internal
- 🔒
@instrumentnow filters arguments whose names match common secret keywords (password,token,secret,api_key,authorization,cookie, ... matched case-insensitively) from both span attributes and log context. Pass extra names viaskip=for custom secret-bearing parameters. Unchanged for non-sensitive args. - 🔧 Replace the optional
orjsonredef-as-Any | Nonepattern ingrelmicro/_json.pywith try/except branches that define the dumps/loads functions in scope. The per-call# type: ignore[union-attr]directives are gone, andorjsonkeeps its real type from the stub package in the available branch. - 🚨
Trace.__aenter__now raisesTracingErrorifopentelemetry.trace._TRACER_PROVIDERis missing instead of silently no-op patching. A future OTel that drops the private global surfaces a clear error pointing at the workaround. An inline comment near the patch documents why the private attribute is required. - 🔒
PickleSerializerdocs upgraded to a Danger callout. Pickle is now framed as trusted in-process backends only, and the@cacheddecorator example leads withJsonSerializer. TheTTLCachedocstring lists Pydantic and JSON before Pickle. - 🔧 Comment why
_env_prefix=env_prefixneeds a type-ignore inRedisProviderandPostgresProvider(pydantic-settings runtime kwarg the stubs do not expose). - ⚡ Snapshot hot config fields (
cost,allowed_repetitions,ttl_seconds,cache_size) ontoRateLimitFilterandDuplicateFilterinstances during setup so the per-recordfilter()path reads plain attrs instead of walking the Pydantic config. - 🔧 Drop three unused
ty: ignoredirectives ingrelmicro/_json.py. - ⚡
@cached(lock=True)per-key lock dictionaries now bound their size with LRU eviction (1024 entries). High-cardinality miss-heavy workloads no longer accumulateasyncio.Lock/threading.Lockobjects indefinitely. Held locks are never evicted, so in-flight stampede protection is preserved. - 🔒
PostgresRateLimiterAdapteradvisory locks now usepg_advisory_xact_lock(hashtextextended(key, namespace)). The grelmicro-specific seed gives rate-limiter keys their own 64-bit lock-id space, isolating them from any other advisory lock in the same database and reducing intra-rate-limiter collisions from a 32-bit birthday risk to a 64-bit one. - ✅ Add a
tests/typing/sample (test_cache_generics.py) that usestyping.assert_typeto lock inTTLCache[T],PickleSerializer[T], andPydanticSerializer[T]inference end-to-end. A regression that widens inference back toAnyfailsuv run ty check. - ✅ Add a guard test that every
_LAZYkey ingrelmicro/resilience/__init__.pyis exported in__all__and actually resolves at runtime. - ✅ Add Hypothesis property tests for token-bucket and sliding-window math and for exponential backoff jitter bounds.
- ✅ Enable branch coverage (
--cov-branch). The 100% gate now covers both lines and branches. Defensive guards against impossible state are marked with# pragma: no branch. - 🔧 Document why every
type: ignoreandty: ignoreingrelmicro/_config.pyis required (Pydantic dynamic-subclass boundary). - 🔧 Explain the double-checked
pragma: no coverinReconfigurable.reconfigureso future contributors see the concurrent-caller intent. - 🔧 Add inline attribution cues to
grelmicro/task/_utils.pyandgrelmicro/resilience/_protocol.py/grelmicro/cache/_protocol.pyso readers immediately see where third-party adaptations live and that protocol bodies live in concrete adapters. - 🔧 Fix the
THIRD_PARTY_NOTICES.mdpath togrelmicro/resilience/ratelimiter/redis.py.
0.25.0 - 2026-05-21
Features
- ✨ Add
Timeoutreconfigurable resilience pattern.Timeout("db", seconds=2.0)wrapsasyncio.timeout, usable as an async context manager (async with db_timeout:) or decorator on async functions.TimeoutConfigis a frozen three-paths Pydantic config withseconds: PositiveFloat. Env prefixGREL_TIMEOUT_{NAME_UPPER}_. InheritsReconfigurable[TimeoutConfig]for live deadline swaps. Issue #176. - ✨ Add
Fallbackprimitive with decorator, block, and class forms.@fallback(when=..., default=...)/@fallback(when=..., factory=...)swap a matched exception for a safe value.async with falling_back(when=..., default=...) as result:covers inline blocks.Fallback("name", when=..., default=...)is the named, reconfigurable class form.FallbackConfigis a frozen three-paths Pydantic config withdefault/factorymutually exclusive.when=matches Retry's keyword so theMatchDSL stays universal. Composition order documented in Composing patterns. Issue #199. - ✨ Add
PostgresCacheAdapterfor Postgres-backed cache storage. Register viaGrelmicro(uses=[postgres, Cache(postgres)]). Entries land in a singlegrelmicro_cachetable keyed onkey TEXT PRIMARY KEYwithvalue BYTEAandexpires_at TIMESTAMPTZ.getfilters expired rows withWHERE expires_at > NOW(),setis oneINSERT ... ON CONFLICT DO UPDATE. Schema auto-migrates on first connect, opt out withauto_migrate=False. Optional janitor reclaims storage whencleanup_interval=is set (off by default). Issue #167. - ✨ Add
PostgresRateLimiterAdapterfor fleet-wide rate limiting on Postgres. Register viaGrelmicro(uses=[postgres, RateLimiters(postgres)])andRateLimiter.token_bucket(...)orRateLimiter.sliding_window(...)runs against a singlegrelmicro_rate_limitertable.acquireandpeekeach run one round-trip to a PL/pgSQL function. Concurrent writes for the same key are serialized withpg_advisory_xact_lock. Schema and functions auto-migrate on first connect, opt out withauto_migrate=False. Issue #164.
0.24.0 - 2026-05-18
Features
- ✨ Add
CircuitBreakerStrategyProtocol andCircuitBreakerBackend.bind(name, config) -> Strategy. Mirrors the RateLimiter shape so a second algorithm plugs in without breaking changes.CircuitBreakerConfiggains akind: Literal["consecutive_count"]discriminator. Issue #163. - ✨ Add
RedisCircuitBreakerAdapterfor fleet-wide breaker state. Register viaGrelmicro(uses=[redis, CircuitBreakers(redis)])andCircuitBreaker("name")consults Redis for admission, counters, and transitions. Half-open admission cap is enforced globally via atomic Lua scripts.last_errorandlast_error_timestay per-replica. Issue #163. - ✨ Add
CircuitBreaker.consecutive_count(name, ...)factory classmethod, mirroringRateLimiter.token_bucket(...)andRateLimiter.sliding_window(...). Each algorithm of every Pattern lands as a classmethod on the Pattern class. The algorithm-config module loads lazily on first call. Issue #163. - ✨
grelmicro.resilienceis now a PEP 562 lazy package:from grelmicro.resilience import CircuitBreakerno longer loadsRateLimiter, its algorithm configs, or memory/redis adapters. Same in the other direction. Top-level__getattr__dispatches to the right subpackage on first access. Issue #163.
Breaking
- 💥
CircuitBreaker.transition_to_closed,transition_to_open,transition_to_half_open,transition_to_forced_open,transition_to_forced_closed, andrestartare nowasync def. Addawaitat every call site. Issue #163. - 💥
CircuitBreakerBackendProtocol is now lifecycle +bind(name, config). Custom backends should return aCircuitBreakerStrategyinstance frombind.register(breaker)and the local fast-path are dropped: every backend (includingMemoryCircuitBreakerAdapter) goes through the Strategy. Memory state lives in adapter-owned dicts keyed by breaker name.CircuitBreakerSharedStaterenamed toCircuitBreakerSnapshot. Issue #163. - 💥
CircuitBreakerConfigis now aDiscriminator("kind")-tagged union (matchesRateLimiterConfig). InstantiateConsecutiveCountConfig(...)directly. The algorithm config lives atgrelmicro.resilience.circuitbreaker.consecutive_count. Issue #163. - 💥
FORCED_OPENandFORCED_CLOSEDno longer incrementconsecutive_error_count/consecutive_success_count. Per-replicatotal_error_count/total_success_countstill tick. Dashboards keying off consecutive counts during forced states need updating. Issue #163. - 💥 Resilience layout: each Pattern is now a subpackage with its algorithm configs and adapters as siblings.
grelmicro.resilience.memory→grelmicro.resilience.circuitbreaker.memoryandgrelmicro.resilience.ratelimiter.memory.grelmicro.resilience.redis→grelmicro.resilience.circuitbreaker.redisandgrelmicro.resilience.ratelimiter.redis.grelmicro.resilience.algorithmsis gone: rate-limiter configs live atgrelmicro.resilience.ratelimiter.{token_bucket,sliding_window}, circuit-breaker configs atgrelmicro.resilience.circuitbreaker.consecutive_count. Top-levelfrom grelmicro.resilience import ...shortcuts are unchanged. Issue #163. - 💥 Rename Components:
Breaker→CircuitBreakers,RateLimit→RateLimiters. Plural matches existing Component convention (Tasks,HealthChecks). Mechanical migration: replaceBreaker(...)withCircuitBreakers(...)andRateLimit(...)withRateLimiters(...)at every call site. Issue #163. - 💥
CircuitBreaker.__init__drops the algorithm kwargs path (error_threshold=,success_threshold=,reset_timeout=,half_open_capacity=,log_level=,ignore_exceptions=,env_prefix=,env_load=). Signature is nowCircuitBreaker(name, config=None, *, backend=None), matchingRateLimiter. UseCircuitBreaker.consecutive_count("name", error_threshold=5, ...)for the simple case,CircuitBreaker("name", ConsecutiveCountConfig(...))for the declarative case, or bareCircuitBreaker("name")for defaults. Env loading viaGREL_CIRCUIT_BREAKER_*is gone: build the config frompydantic-settingsif you need that. Issue #163. - ✨
CircuitBreakerandRateLimiterfall back to a process-global implicitMemoryCircuitBreakerAdapter/MemoryRateLimiterAdapterwhen noCircuitBreakers/RateLimitersComponent is registered.CircuitBreaker("payments")andRateLimiter.token_bucket("api", capacity=10, refill_rate=1)work without anyGrelmicro(uses=[...])setup. Fleet-wide opt-in stays explicit (Grelmicro(uses=[redis, CircuitBreakers(redis), RateLimiters(redis)])). Issue #163.
0.23.0 - 2026-05-17
Breaking
- 💥 Rename the discriminator field from
typetokindon every tagged union. AffectsRateLimiterConfig(TokenBucketConfig,SlidingWindowConfig) andRetryBackoffConfig(ExponentialBackoff,ConstantBackoff,LinearBackoff,FibonacciBackoff,RandomBackoff). Serialized YAML and JSON configs must replacetype:withkind:(for exampleGREL_RETRY_FOO_BACKOFF={"kind":"exponential",...}). Frees the Pythontypebuiltin from being shadowed on every config object. Issue #268. - 💥 Rename
GCRAConfigtoSlidingWindowConfigandRateLimiter.gcra(...)toRateLimiter.sliding_window(...). The discriminator value also moves from"gcra"to"sliding_window". Modulegrelmicro.resilience.algorithms.gcrabecomesgrelmicro.resilience.algorithms.sliding_window. Internal strategy classes (_RedisGCRA,_MemoryGCRA) keep their names since they describe the underlying algorithm. Issue #259.
Features
- ✨ Add
LogandTracecomponents. RegisterLog()andTrace()inGrelmicro(uses=[...])to wire observability through the same verb asSync,Cache,RateLimit,Breaker, andTasks.Log()wrapsgrelmicro.log.configure(...)and snapshots stdlib root handlers on enter so sequential apps in tests do not stack handlers.Trace()owns an OTelTracerProvider: builds it fromTracingConfig(env prefixGREL_TRACE_), installs it on enter, shuts it down and restores the prior global provider on exit. OTLP HTTP and gRPC exporters are lazy-imported. Issue #224.
Docs
- 📝 Add Testing page documenting
micro.override(...)and the conftest recipe. Issue #236. - 📝 Add Capability matrix page covering Pattern × Adapter pairs for
1.0.0. Issue #161.
0.22.0 - 2026-05-16
Features
- ✨ Add
Grelmicroapp object andComponentprotocol. The user composes everything attached to the app into one container and opens it withasync with micro:. SingleGrelmicro.use(item)registration verb (anduses=constructor kwarg) acceptsComponentinstances (registered with(kind, name)lookup, exposed onmicro.<kind>), first-party backends (auto-wrapped into their matching Component:RedisCacheAdapter→Cache,RedisSyncAdapter→Sync), and any other async context manager (lifecycled only, caller keeps the reference). Typed accessorsmicro.syncandmicro.cacheprovide IDE completion.Grelmicro.componentsreturns the registered Components in order for/healthz-style introspection. Issue #208, epic #201, unified in #219,Componentrename and.componentsaccessor in #233. - ✨ Add
Synccomponent. Wraps aSyncBackendand exposeslock(...),task_lock(...),leader_election(...)factories. Use it viaGrelmicro(uses=[redis, Sync(redis)])(Provider-direct) orSync(MemorySyncAdapter())(Backend-direct). Reach it onmicro.sync. Issue #210. - ✨ Add
Cachecomponent. Wraps aCacheBackendand exposes attl(...)factory that builds aTTLCachebound to the wrapped backend. Use it viaGrelmicro(uses=[redis, Cache(redis)])(Provider-direct) orCache(MemoryCacheAdapter())(Backend-direct). Reach it onmicro.cache. Issue #212. - ✨ Add Component-direct Provider API.
Sync,Cache,RateLimit, andBreakeraccept aProvideror aBackendinstance. When given a Provider, the Component callsprovider.sync(),provider.cache(),provider.ratelimiter(), orprovider.breaker()to build the matching adapter. AddProviderbase class ingrelmicro.providers._basewith the four factory methods. AddRedisProvider.ratelimiter()returning aRedisRateLimiterAdapter. The Adapter classes stay public as escape hatches for custom Providers, but the recommended user code usesSync(redis)instead ofSync(RedisSyncAdapter(provider=redis)). - ✨ Add
Grelmicro.current()classmethod for ambient lookup. Insideasync with micro:it returns the active app for the current asyncio task. - ✨ Add
Retryprimitive with decorator, block, and class forms. Five backoff algorithms ship:ExponentialBackoff(default, with full jitter),ConstantBackoff,LinearBackoff,FibonacciBackoff, andRandomBackoff.when=is required and accepts aMatch(or shorthand). Live reconfiguration viaReconfigurable[RetryConfig]. Three-paths configuration. Underlying exception is re-raised with a PEP 678 note on exhaustion. Issue #165. - ✨ Add
MatchandOutcometogrelmicro.resilience.Matchis the resilience-wide outcome filter DSL:Match.exception(...),Match.result(...),Match.exception_message(...),Match.exception_cause(...),Match.predicate(...),Match.always(),Match.never()plus theirnot_*twins, composed with the|and&operators.Outcome[T]is the dataclass passed to custom predicates (exception,result,raised). Issue #242. - ✨ Add
grelmicro.providers.redis.RedisProvider. First-class Redis connection holder shared across components:RedisProvider("redis://..."),RedisProvider(host="...", port=...),RedisProvider()(env-driven viaREDIS_*),RedisProvider.from_config(RedisConfig(...)), andRedisProvider.from_client(client, own=False)for bring-your-own clients.Grelmicrodedupes implicit providers by(provider_class, env_prefix), so two adapters with the same prefix share one connection pool. Issue #226. - ✨ Add
grelmicro.providers.postgres.PostgresProvider. First-class Postgres connection holder wrapping anasyncpg.Pool:PostgresProvider("postgresql://..."),PostgresProvider(host=..., database=..., user=..., password=...),PostgresProvider()(env-driven viaPOSTGRES_*),PostgresProvider.from_config(PostgresConfig(...)), andPostgresProvider.from_client(pool, own=False)for bring-your-own pools. Shares the same(provider_class, env_prefix)dedupe asRedisProvider. Issue #255.
Breaking
- 💥 Patterns
RateLimiter,CircuitBreaker, and the FastAPI health router resolve through the activeGrelmicroapp. Add the two new ComponentsRateLimit(wrapsRateLimiterBackend, kind"ratelimiter") andBreaker(wrapsCircuitBreakerBackend, kind"circuitbreaker").Grelmicro.use(...)auto-wraps aRateLimiterBackendorCircuitBreakerBackendinstance into its matching Component.HealthChecksbecomes a Component (kind = "health", defaultname = "default"). Pass it toGrelmicro(uses=[...])and the FastAPIhealth_router()resolves it viaGrelmicro.current(). Delete therate_limiter_backend_registry,circuit_breaker_backend_registry,health_checksregistries plusgrelmicro/_backends.py(BackendRegistry,BackendNotLoadedError,BackendAlreadyRegisteredError). Closes out #201. Issue #261. - 💥 Redis adapters now take
provider=orenv_prefix=, not a positionalurl=.RedisSyncAdapter,RedisCacheAdapter, andRedisRateLimiterAdapterlose theirurl=argument. Passprovider=RedisProvider(...)to share a pool, or rely onenv_prefix=(defaultREDIS_) to build one. Issue #226. - 💥
PostgresSyncAdapternow takesprovider=orenv_prefix=, not a positionalurl=. Passprovider=PostgresProvider(...)to share a pool, or rely onenv_prefix=(defaultPOSTGRES_) to build one. Issue #255. - 💥 Rename
TaskManagertoTasks. Class still extendsTaskRouter, mirroring FastAPI'sAPIRouter←FastAPIshape. Update imports tofrom grelmicro.task import Tasks. Issue #218. - 💥 Rename
HealthRegistrytoHealthChecks(andHealthRegistryConfigtoHealthChecksConfig). Update imports tofrom grelmicro.health import HealthChecks. Issue #201. - 💥 Rename concrete backends to
*Adapter.MemorySyncBackend,RedisSyncBackend,PostgresSyncBackend,SQLiteSyncBackend,KubernetesSyncBackend,MemoryCacheBackend,RedisCacheBackend,MemoryRateLimiterBackend,RedisRateLimiterBackend, andMemoryCircuitBreakerBackendbecome*Adapter. TheSyncBackend,CacheBackend,RateLimiterBackend, andCircuitBreakerBackendProtocols stay as-is. Issue #201. - 💥 Rename nested backoff classes to drop the redundant
Configsuffix.ExponentialBackoffConfig,ConstantBackoffConfig,LinearBackoffConfig,FibonacciBackoffConfig, andRandomBackoffConfigbecomeExponentialBackoff,ConstantBackoff,LinearBackoff,FibonacciBackoff, andRandomBackoff. TheRetryBackoffConfigdiscriminated-union alias and the JSON discriminator (type: "exponential") are unchanged. Issue #239. - 💥 Rename the
Moduleprotocol toComponentto avoid clashing with Python's own "module".ModuleAlreadyRegisteredErrorbecomesComponentAlreadyRegisteredErrorandModuleNotRegisteredErrorbecomesComponentNotRegisteredError. TheSyncandCacheclasses keep their names. No deprecation shim. Issue #233. - 💥 Rename the env-loading flag to align the per-call kwarg and the global env var.
read_env=becomesenv_load=on every component,GREL_CONFIG_FROM_ENVbecomesGREL_ENV_LOAD, and thegrelmicro._config.env_opt_in_enabled()helper becomesenv_load_default(). No deprecation shim. Issue #232. - 💥 Remove the module-level registry and lifespan API.
grelmicro.lifespan()is gone. Theregister/unregister/use/use_backend/use_registryhelpers acrossgrelmicro.{sync,cache,health,resilience}(plus the resilience circuit-breaker variants) are gone. Patterns (Lock,TaskLock,LeaderElection,TTLCache) now resolve their backend viaGrelmicro.current()at every call. Build aGrelmicro(uses=[...])and open it withasync with micro:. Thegrelmicro.sync._backendsandgrelmicro.cache._backendsmodules are removed (sync and cache resolve through the app). The internalrate_limiter_backend_registry,circuit_breaker_backend_registry, andhealth_checksregistries stay private until follow-up issues introduce their Component wrappers. Issue #207. - 💥
Retryoutcome filter is nowwhen=accepting aMatch. The oldon=parameter is gone. TheMatchDSL (Match.exception(...),Match.result(...),Match.exception_message(...),Match.exception_cause(...),Match.always(),Match.never(),Match.predicate(...)) plus theirnot_*twins and the|/&operators cover the common retry-filter surface. Bare-class shorthand is still accepted (when=httpx.HTTPError). Result-based retry lands in the same change:when=Match.result(None)retries until the function stops returningNone. Env var renamedGREL_RETRY_{NAME}_ON→GREL_RETRY_{NAME}_WHEN. Issue #242.
Internal
- ⚡ Defer the
opentelemetryimport ingrelmicro.trace.import grelmicro.traceno longer loadsopentelemetry(was 16 modules). The package is resolved lazily on first call toinstrument,span, oradd_contextand cached. Issue #189.
0.21.0 - 2026-05-06
Breaking
- 💥 Drop Python 3.11. The new floor is
requires-python = ">=3.12". RHEL 9 (App Streampython3.12) and RHEL 10 (default) ship 3.12 and the UBI images are available, so enterprise users are covered. Issue #66. - 💥 Drop AnyIO. grelmicro now targets
asynciodirectly. Issue #183. - 💥
CircuitBreakernow takes a backend (CircuitBreakerBackend). The in-memory backend (MemoryCircuitBreakerBackend) is the default. A future Redis-backed implementation will share state across replicas (issue #188). The async API stays primary, sync code goes throughcb.from_thread. - 💥 The sync adapters on
Lock,TaskLock,TTLCache, andCircuitBreakernow require the backend to be opened (async with backend:orgrelmicro.lifespan()). The backend captures the running loop and the sync adapter dispatches through it. Zero hot-path overhead. - 💥 Resilience registries are now namespaced. The rate limiter registry name moves from
"resilience"to"resilience.ratelimiter"and the circuit breaker registry is"resilience.circuitbreaker".grelmicro.lifespan(exclude=...)now matches by dotted prefix, soexclude={"resilience"}still skips both registries.
Features
- ✨ Add
uvloopto thestandardextra (Linux and macOS). Activate withuvloop.run(main()).
Internal
- ✅ Migrate the test suite from
pytest.mark.anyiotopytest-asynciowithasyncio_mode = "auto". AnyIO is no longer a direct dependency of grelmicro (it may still arrive transitively, for example throughfast-depends). - ♻️ Adopt PEP 695 generic syntax (
class Foo[T]:,def f[T](...),type X = ...) across_backends.py,_config.py,_types.py,health/_types.py,trace/_instrument.py, andtests/task/conftest.py. Two files keep the older form: the recursive aliases in_json.py(ty cannot expand recursive PEP 695 aliases) and the decorator factory incache/cached.py(PEP 695 binds the inner decorator to the outer scope's type parameters, breaking per-decoration-site inference). Issue #65. - 🔨 Bump
tool.ruff.target-versiontopy312and the CI matrix to["3.12","3.13","3.14"].
0.20.0 - 2026-05-03
Live reconfiguration is complete. Every stateful primitive now exposes reconfigure(new_config), so you can hot-reload from a ConfigMap or SIGHUP without restarting the process. See Live reconfiguration for the contract.
Features
- ✨ Add
RateLimiter.reconfigure(new_config). Swap algorithm config without rebuilding the limiter. PR #153. - ✨ Add
reconfigure(new_config)toLock,TaskLock, andLeaderElection. Swap timing fields without restarting. Theworkerfield cannot change. PR #159. - ✨ Add
CircuitBreaker.reconfigure(new_config). Swap thresholds andignore_exceptionswithout restarting. Runtime state andlast_errorare kept.log_levelis applied to the logger. PR #160. - ✨ Add
HealthRegistry.reconfigure(new_config). Swapcache_ttland the defaulttimeoutwithout restarting. Per-check timeouts stay as registered. PR #180.
Docs
- 📝 Reframe README and docs landing as a microservice patterns toolkit. PR #155.
- 📝 Replace "Production-ready" with "Railguarded": 100% pytest coverage, ty-checked, ruff-linted, Pydantic-validated. PR #181.
Internal
- 🔨 Switch build backend to Hatch. PR #155.
- 🎨 Supersample favicon PNGs with Lanczos downscaling for smoother anti-aliasing. PR #156.
0.19.0 - 2026-05-01
Cleans out the long-deprecated APIs (ResilienceException, Synchronization, scheduled(), the token= kwarg) ahead of the 1.0.0 design work, ships a 3.4× speedup on env-driven config construction, and brings the test suite under 20s for contributors.
Breaking
- 💥 The Environmental config path is now opt-in. Set
GREL_CONFIG_FROM_ENV=trueonce at startup to enable env reads across every component, or passread_env=Trueper call. The per-call value (True/False) always wins over the global flag. This stops grelmicro from silently picking up ambient env vars in unit tests or scripts. Issue #142. - 💥 The
read_envkwarg default flips fromTruetoNoneon every component.Nonefollows the global flag.TrueandFalsekeep their meaning as explicit per-call overrides. - 💥 Remove obsolete deprecation shims that were marked for removal in 0.7.0. Replace
ResilienceExceptionwithResilienceError,SynchronizationwithSyncPrimitive, and thescheduled()decorator onTaskRouter/TaskManagerwithinterval(seconds=N, max_lock_seconds=N*5). Thetoken=kwarg onLockAcquireError,LockReleaseError, andLockNotOwnedErroris removed (drop it from your code). Thesync=parameter oninterval()no longer warns when used with non-Lockprimitives.
Internal
- ♻️ Add
grelmicro._config.env_opt_in_enabled()helper that exposes the truthyGREL_CONFIG_FROM_ENVcheck (1,true,yes,on, case-insensitive). Issue #142. - 📝 Document the "no field-mirroring" decision in
docs/architecture/config.mdwith the benchmark numbers frombenchmarks/config_attr_benchmark.py. Closes Issue #113 without code changes: hot-path config reads cost <1% of a real call (~2 ns out of ~250 ns), so we keepself._configas the single source of truth instead of copying frozen fields onto the component. - 🔇 Silence the upstream
testcontainers@wait_container_is_readydeprecation banner via a scopedfilterwarningsentry inpyproject.toml. Replace the unawaitedlambda: sleep(math.inf)mock side-effect intests/sync/test_leaderelection.py::test_leadership_abandon_on_renew_deadline_reachedwith an explicit async helper. The full suite now reports zero warnings. - ⚡ Speed up the test suite from ~73s to ~19s by adding
pytest-xdist(-n autoinaddopts) and shrinking expiration sleeps intests/sync/test_backends.py. Fix--durationsreporting by removing the autousefreeze_timefixture:@freeze_time()decorator stays on the two tests that comparedatetime.now(), the two tests that previously calledfrozen_time.tick(...)switch tomonkeypatch.setattr(circuitbreaker, "monotonic", ...). Issue #125. - ⚡ Cache the dynamic
BaseSettingssubclass built bygrelmicro._config._build_settings_clswith@functools.lru_cache(maxsize=256). The env path ofresolve_confignow reuses the same_<Config>Settingssubclass across calls instead of rebuilding it every time, which makesLock("cart")-style construction ~3.4× faster (232 µs/op → 68 µs/op). The bound is a safety net for long-running processes that might derive prefixes from runtime inputs. Issue #119. - ♻️ Rename the local
parent_configtomerged_configin_build_settings_clsand document why the existing# type: ignorecomments are needed. Issue #127.
0.18.0 - 2026-04-30
M2 milestone closed: backend wiring is now fully explicit. Construction is pure (no global writes), registration is named (<module>.register(backend, "name")), and grelmicro.lifespan(*ad_hoc, exclude=...) walks every registry that has been imported and opens its registered backends in one call. Task-scoped overrides via with <module>.use(...): swap backends per request or per test through contextvars.
Breaking
- 💥 Backend constructors are now pure:
__init__performs no registry writes. Theauto_registerkwarg is removed from every backend and fromHealthRegistry. PR #138. - 💥
BackendRegistry.setis renamedregisterandBackendRegistry.unregisteris added with an identity check.resetremains for test fixtures. PR #138. - 💥
async with backendopens the connection but no longer registers. Callregister(backend)(oruse_backend(backend)) to register, or open everything at once withgrelmicro.lifespan(). PR #139. - 💥
BackendRegistryis now multi-name:register(backend, name="default"),unregister(name, backend=None),get(name="default"). PR #139. - 💥 The sync registry name changed from
"lock"to"sync"(used in error messages andlifespan()exclude keys). The rate limiter registry changed from"rate_limiter"to"resilience". PR #139. - 💥 Overwriting a registered name with a different instance now raises
BackendAlreadyRegisteredError(was: warning + replace). Re-registering the same instance stays a no-op. PR #139.
Features
- ✨ Add
grelmicro.sync.use_backend,grelmicro.cache.use_backend,grelmicro.resilience.use_backend, andgrelmicro.health.use_registryfor explicit, idempotent process-lifetime registration. PR #138. - ✨
grelmicro.lifespan(*ad_hoc, exclude=...)opens every registered backend across every imported module in one call, with reverse-order shutdown. PR #139. - ✨ Per-module helpers
register,unregister,use_backend,useongrelmicro.sync,grelmicro.cache,grelmicro.resilience(anduse_registry,useongrelmicro.health). PR #139. - ✨ Task-scoped overrides via
<module>.use(backend)or<module>.use(default=a, analytics=b). Stacks LIFO viacontextvarsfor per-test and per-request substitution. PR #139. - ✨ Primitives accept
backend=as either a backend instance or a registered name (Lock("audit", backend="analytics")). The registry is consulted on each call so<module>.use(...)overrides apply. PR #139. - ✨ Registries subscribe themselves on import:
lifespan()walks only modules that are actually imported, so unused components have zero RAM cost and zero startup work. PR #139. - ✨ Lookup falls back to the sole registered entry when no
"default"is named, so the single-backend case stays one-call. PR #139.
0.17.0 - 2026-04-29
Breaking
- 💥
CircuitBreakerconfig moves to a frozenCircuitBreakerConfig. Read it viacb.config. PR #132. - 💥 The mutable attributes
cb.error_threshold,cb.success_threshold,cb.reset_timeout,cb.half_open_capacity,cb.ignore_exceptions,cb.log_levelare removed. Construct a newCircuitBreakerto change config. PR #132. - 💥 Rename
grelmicro.loggingtogrelmicro.logandgrelmicro.tracingtogrelmicro.trace. Avoids shadowing stdlibloggingand aligns with the OpenTelemetry /ddtracetrace(singular) convention. Update imports:from grelmicro import log, trace. PR #135. - 💥
configure_logging()is renamedlog.configure(). Uselog.configure_with(config)for the declarative path. Both return the appliedLoggingConfig. PR #135. - 💥
LoggingSettings(theBaseSettingsshadow class) is removed.LoggingConfigis the config class. Env reading happens insidelog.configure(). PR #135. - 💥
LoggingConfigfield names move to lowercase:LOG_BACKEND→backend,LOG_LEVEL→level,LOG_FORMAT→format,LOG_TIMEZONE→timezone,LOG_JSON_SERIALIZER→json_serializer,LOG_CALLER_ENABLED→caller_enabled,LOG_OTEL_ENABLED→otel_enabled. PR #135. - 💥 Env vars move from
LOG_*toGREL_LOG_*to align with the rest of the library. PR #135. - 💥
LoggingSettingsValidationErroris removed.pydantic.ValidationErrorpropagates fromlog.configure()like every other component. PR #135.
Features
- ✨ Add
CircuitBreakerConfigandCircuitBreaker.from_config(name, config). PR #132. - ✨
CircuitBreakerreadsGREL_CIRCUIT_BREAKER_<NAME>_*env vars and acceptsenv_prefix=/read_env=. PR #132. - ✨
ignore_exceptionsaccepts fully-qualified import strings ("builtins.ValueError") so YAML and env loaders can specify exception types. PR #132. - ✨ Env vars for tuple/list fields accept comma-separated values in addition to JSON arrays. PR #132.
- ✨
log.configure(**kwargs)accepts everyLoggingConfigfield as a kwarg, mirroring the three-paths contract used by other components. PR #135. - ✨
log.configure_with(config)is the declarative entry point. Returns the appliedLoggingConfig. PR #135.
Internal
- ♻️ Add
grelmicro/_types.pyfor shared lightweight type aliases (LogLevel). PR #132. - ♻️ Add
grelmicro/_config.py::parse_csv_or_jsonshared utility for env var list parsing. PR #132.
Docs
- 🎨 Switch logo typeface from Funnel Sans to Funnel Display.
0.16.1 - 2026-04-29
Internal
- ✅ "No registry call at construction" tests now patch the registry source instead of the per-module import alias, so a future refactor that bypasses the local alias can no longer silently pass the check. PR #130.
- ⬆️ Bump
tyfrom 0.0.29 to 0.0.30. PR #111. - ⬆️ Pre-commit autoupdate. PR #114.
0.16.0 - 2026-04-29
Breaking
- 💥
LockConfig,TaskLockConfig,LeaderElectionConfig, andRateLimiterConfigno longer carry anamefield. Pass the name positionally:Lock("cart", LockConfig(lease_duration=30)). PR #123. - 💥 Rename
TokenBuckettoTokenBucketConfigandGCRAtoGCRAConfig.RateLimiterConfigbecomes the discriminated union of algorithm configs. PR #123. - 💥
RateLimitertakes the algorithm config positionally:RateLimiter("api", GCRAConfig(limit=100, window=60)). Thealgorithm=,limit=,window=kwargs are removed. PR #123. - 💥
fail_openmoves fromRateLimiter(...)to the algorithm config. PR #123.
Features
- ✨ Add
Component.from_config(name, config)to every primitive (Lock,TaskLock,LeaderElection,RateLimiter,HealthRegistry,RateLimitFilter,DuplicateFilter). PR #123. - ✨ Read environment variables under
GREL_<COMPONENT>_<NAME>_*for every component that supports the environmental path. PR #123. - ✨ Add
RateLimiter.token_bucket(name, ...)andRateLimiter.gcra(name, ...)factory classmethods. PR #123. - ✨ Add
env_prefix=andread_env=kwargs to every component that exposes the environmental path. PR #123. - ✨ Normalise instance names like
payments-eu,cart.v2, orweather/svcinto POSIX env var segments. PR #123.
Changed
- ♻️
Lock,TaskLock,LeaderElection, andRateLimiternow resolve the backend lazily on first use instead of at construction.BackendNotLoadedErrorsurfaces on the firstacquire/peek/resetcall rather than in__init__. Each component exposes a publicbackendproperty. PR #128.
Fixed
- 🐛 Auto-registered backends now identity-check before clearing the registry on
__aexit__, so a replacement instance is left alone. PR #122. - 🐛
Lock.releaseclears local ownership only after the backend confirms the release. PR #122.
0.15.0 - 2026-04-29
Breaking
- 💥 Redesign the
healthmodule:@health.check("name")decorator, binaryok/errorstatus, empty probe bodies, per-check caching. PR #112. - 💥 Endpoint renames:
/health/live→/livez,/health/ready→/readyz. New/healthzreturns the full check JSON. PR #112. - 💥
HealthRegistry.check()renamed torun(). Thecheckname is now the decorator. PR #112. - 💥
HealthCheckerProtocol removed. Use plaindeforasync deffunctions. PR #112. - 💥
HealthReport.components: listbecomesHealthReport.checks: dict[name, ...]. PR #112. - 💥
HealthCheckTimeoutErrorand the three-stateHealthStatusremoved. PR #112.
Docs
- 📝 Restate the versioning policy: pre-1.0
MINORmay break,PATCHnever. Post-1.0 deprecations get twoMINORreleases.
0.14.3 - 2026-04-22
Docs
- 🐛 Fix the wordmark duplicating on PyPI and other renderers that don't understand GitHub's theme-only URL fragments. PR #109.
0.14.2 - 2026-04-22
Docs
- 🐛 Fix the landing-page wordmark disappearing when the docs site is toggled into dark mode. PR #108.
- 📝 Centre the badges row under the tagline. PR #108.
0.14.1 - 2026-04-22
Docs
- 🎨 Ship the grelmicro brand identity: wordmark, favicon, and social-preview card. PR #106.
- 🎨 Refresh the docs theme with the brand palette. PR #106.
- 📝 Rewrite the "Why grelmicro" pillars. PR #106.
- 📝 Split the resilience docs into per-pattern pages.
- 📝 Add an Installation guide with
pip,uv, andpoetrytabs. - 📝 Render PEP 727
Annotated[..., Doc(...)]parameter docs viagriffe-typingdoc. - 📝 Plain-English pass on docs and docstrings for non-native readers.
- 📝 Add a Mermaid state diagram to the Circuit Breaker page.
- 📝 Document every
__all__symbol in the API reference. - 📝 Add a plain-English style guide to
CONTRIBUTING.md.
Internal
- 🐛 De-flake
test_lock_reentrant_from_threadon Python 3.12. Fixes #105. - 🔧 Add keywords to
pyproject.tomlfor PyPI discovery.
0.14.0 - 2026-04-21
Features
- ✨ Add pluggable
RateLimiteralgorithms via thealgorithm=parameter:TokenBucketandGCRA. PR #102. - ✨ Add
MemoryTokenBucket, a standalone synchronous token-bucket primitive. PR #102. - ✨ Add
RateLimitFilter, alogging.Filterwith configurablekey_mode. PR #102. - ✨ Add
DuplicateFilter, alogging.Filterthat caps repeated records per key with optional TTL. PR #94. - ✨
HealthRegistrynow logs every unhealthy path atWARNING(ERRORfor unexpected exceptions). PR #92.
Deprecations
- 🗑️
RateLimiter(name, limit=..., window=...)is deprecated. UseRateLimiter(name, algorithm=GCRA(limit=..., window=...))instead. Will be removed in 0.15.0. PR #102.
Docs
- 📝 Add
CONTRIBUTING.mdwith repo conventions. PR #102. - 📝 Add a "Choosing an algorithm" guide for
TokenBucketvsGCRAin the Rate Limiter docs. PR #102. - 📝 Surface
THIRD_PARTY_NOTICES.mdin the docs site. PR #102.
Security
- 🔒️ Harden CI supply chain: pin all Actions to SHAs, close
run:injection vectors, add zizmor workflow-lint, restrict Dependabot auto-merge to uv patch/minor updates. PRs #95, #100, #101.
Internal
- ⬆️ Bump
pydanticto 2.13.0,opentelemetry-api/opentelemetry-sdkto 1.41.0,pytestto 9.0.3,ruffto 0.15.10,tyto 0.0.29,fastapito 0.135.3,uvicornto 0.44.0. PR #99. - ⬆️ Bump
pydantic-extra-typesfrom 2.11.1 to 2.11.2. PR #89. - ⬆️ Pre-commit
ruffautoupdate (v0.15.9 → v0.15.11). PR #91. - ⬆️ Bump
codecov/codecov-actionto v6. PR #96. - ⬆️ Bump
astral-sh/setup-uvto v8. PR #97. - ⬆️ Bump
dependabot/fetch-metadatato v3. PR #98.
0.13.0 - 2026-04-08
Features
- ✨ Add
RateLimiter.peek(key): check rate limit state without consuming tokens. PR #90. - ✨ Add
RateLimiter.reset(key): delete rate limit state for a key, restoring full quota. PR #90. - ✨ Add
fail_openparameter toRateLimiter: return allowed result on backend errors instead of propagating exceptions. PR #90.
0.12.0 - 2026-04-07
Features
- ✨ Add
healthmodule with health check registry, concurrent checker execution, and FastAPI integration for liveness/readiness probes. PR #84.
Internal
- ⬆️ Bump orjson from 3.11.7 to 3.11.8. PR #72.
- ⬆️ Bump ty from 0.0.26 to 0.0.27. PR #74.
- ⬆️ Update uv-build requirement from
<0.10.0to<0.12.0. PR #75. - 👷 Add build provenance attestations and wheel verification to release pipeline.
- ♻️ Pre-release cleanup: add health/json to overview, fix style inconsistencies, remove stale branches.
0.11.0 - 2026-04-03
Breaking Changes
- 💥 Logging: split
callerinto separatelogger(logger name) andcaller(function:line) fields.calleris now opt-in viaGREL_LOG_CALLER_ENABLED(default:False), following common structured-logging conventions. Uvicorn formatter never includescaller. - 💥 Cache: replace
TTLCacheserializer/deserializercallable pair with a singleserializeraccepting aCacheSerializerprotocol object. UseJsonSerializer(),PydanticSerializer(Model), orPickleSerializer()instead.
Features
- ✨ Add
GREL_LOG_CALLER_ENABLEDsetting to opt in to caller info (function:line) in log records. Disabled by default for cleaner logs and better performance. - ✨ Add
loggerfield (logger name, e.g.,myapp.api) to all log records across all backends and formats. - ✨ Add
grelmicro.jsonmodule with fast JSON serialization usingorjsonwhen available, with automatic fallback to stdlibjson.
0.10.0 - 2026-04-02
Features
- ✨ Add
RateLimiterto theresiliencemodule: Redis-backed sliding-window rate limiting using the GCRA algorithm. IncludesRateLimitResultwith fields mapping to IETF rate limit headers, weighted requests viacostparameter, andRateLimitExceededError.
Removals
- 🗑️ Remove deprecated
UvicornJSONFormatterandUvicornAccessJSONFormatter. UseUvicornFormatterandUvicornAccessFormatterinstead (deprecated since 0.9.1).
CI
- ⚡ Migrate PyPI publishing from API token to OIDC trusted publishing.
0.9.1 - 2026-04-01
Deprecations
- 🗑️
UvicornJSONFormatterandUvicornAccessJSONFormatterare deprecated. UseUvicornFormatterandUvicornAccessFormatterinstead. The new formatters respectGREL_LOG_FORMATinstead of always producing JSON. Old names kept as aliases withDeprecationWarning.
0.9.0 - 2026-04-01
Breaking Changes
- 💥
GREL_LOG_FORMATdefault changed fromJSONtoAUTO. In production (non-TTY), behavior is identical (JSON output). In local dev (TTY), output switches to human-readableTEXTwith colors. SetGREL_LOG_FORMAT=JSONexplicitly to restore the previous default.
Features
- ✨ Add
AUTOlog format (new default): detects TTY and selectsTEXT(terminal) orJSON(piped/CI). - ✨ Add
LOGFMTlog format: key-value pairs following the logfmt convention, 30-40% smaller than JSON. - ✨ Add
PRETTYlog format: multi-line indented output with structured error rendering. - ✨ Enhanced
TEXTformat: now includes extra context fields askey=valuepairs and supports ANSI colors. - ✨ Add
NO_COLOR/FORCE_COLORenvironment variable support following no-color.org standard.
0.8.0 - 2026-04-01
Breaking Changes
- 💥 Backend imports moved to submodules. Use
from grelmicro.sync.redis import RedisSyncBackendinstead offrom grelmicro.sync import RedisSyncBackend. Same for all sync, cache, and logging backends. See Import Strategy.
Features
- ✨ Add Uvicorn JSON formatters (
UvicornJSONFormatter,UvicornAccessJSONFormatter) for structured logging viadictConfig.
0.7.0 - 2026-03-31
Breaking Changes
- 💥 Logging JSON format redesigned to follow industry standards:
loggerrenamed tocallerthreadremovedctxremoved: extra fields are now flat at the top levelexceptionreplaced by structurederrorobject (type,message,stack)
Features
- ✨ Add
tracingmodule with@instrumentdecorator,span()context manager, andadd_context()for unified logging and OTel instrumentation.
Performance
- ⚡ Logging: Up to +23% throughput across all backends.
- ⚡ Use
OrderedDictfor O(1) LRU operations inTTLCache.
Refactors
- ♻️ Extract shared Redis config into
grelmicro/_redis.py. - ♻️ Make
TTLCachegeneric and addDocannotations. - ♻️ Extract context stack into
grelmicro/_context.pyto decouple logging from tracing. - ♻️ Filter private (
_-prefixed) attributes from stdlib JSON log output. - ♻️ Widen
@instrument(skip=...)type fromset[str]toAbstractSet[str].
Removals
- 🗑️
Synchronizationprotocol removed. UseSyncPrimitiveinstead (deprecated since 0.6.0). - 🗑️
ResilienceExceptionremoved. UseResilienceErrorinstead (deprecated since 0.6.0). - 🗑️ The
tokenparameter on lock errors removed (deprecated since 0.6.0). - 🗑️ The
syncparameter oninterval()removed (deprecated since 0.6.0). - 🗑️ The
scheduled()decorator removed (deprecated since 0.6.0).
0.6.0 - 2026-03-30
Deprecations
- 🗑️
Synchronizationprotocol renamed toSyncPrimitive. The old name still works but emits aDeprecationWarning. Will be removed in 0.7.0. - 🗑️
ResilienceExceptionrenamed toResilienceError. The old name still works but emits aDeprecationWarning. Will be removed in 0.7.0. - 🗑️ The
tokenparameter onLockAcquireError,LockReleaseError, andLockNotOwnedErroris deprecated. Tokens are no longer included in error messages for security. Will be removed in 0.7.0. - 🗑️ The
syncparameter oninterval()forTaskLockandLeaderElectionis deprecated. Usemax_lock_secondsandleaderparameters instead. Will be removed in 0.7.0. - 🗑️ The
scheduled()decorator is deprecated. Useinterval()withmax_lock_secondsorleaderinstead. Will be removed in 0.7.0.
Features
- ✨ Add in-memory TTL cache with LRU eviction, per-key stampede protection, and
@cacheddecorator. - ✨ Add
RedisCacheBackendfor distributed cache storage. - ✨ Add cache statistics via
CacheInfo(hits, misses, evictions, stampedes). - ✨ Add Kubernetes sync backend using Lease resources (
pip install grelmicro[kubernetes]). - ✨ Add SQLite sync backend for home lab and local testing (
pip install grelmicro[sqlite]).
Security
- 🔒️ Remove token values from lock error messages to prevent leaking in logs.
- 🔒️ Upgrade
requeststo 2.33.0 (CVE fix in transitive dependency).
Refactors
- ♻️ Unify error hierarchy under
GrelmicroErrorbase class. All module errors (SyncError,ResilienceError,LoggingError,TaskError,CacheError) now share a common base. - ♻️ Use server-side timestamps and native Lease fields in sync backends.
- ♻️ Simplify token generation from UUID-based to string concatenation.
- ♻️ Harden TaskLock token nonce and error handling.
Internal
- ✅ Achieve 100% library code coverage.
- 💚 Fix flaky integration test timeout in CI.
- ⬆️ Bump dependencies and fix ty v0.0.26 type errors.
Docs
- 📝 Add cache module documentation with usage guide and API reference.
- 📝 Add Kubernetes Backend Architecture page.
- 📝 Add SQLite Backend Architecture page.
- 📝 Add backend comparison matrix to Coordination guide.
- 📝 Rewrite README with project vision.
0.5.0 - 2026-03-17
Breaking Changes
- 💥 Add namespace prefix to sync primitive backend keys (
lock:,tasklock:,leader:). See Migration Guide below.
Features
- ✨ Add
TaskLock.from_threadthread-safe adapter. PR #57. - ✨ Add specific lock error classes (
LockAcquireError,LockReleaseError,LockLockedCheckError,LockOwnedCheckError,LockReentrantError). PR #57.
Refactors
- ♻️ Consolidate distributed lock and leader gating into the
interval()decorator viamax_lock_secondsandleaderparameters. PR #54.
Docs
- 📝 Add Coordination Architecture page. PR #57.
Internal
- ⬆️ Bump redis, fastapi, pydantic, and pydantic-settings. PR #55.
- ⬆️ Update pre-commit hooks. PR #50.
Migration Guide
Namespace-Prefixed Backend Keys
Prior versions used the name parameter directly as the backend key. Now each primitive adds a type-specific prefix:
| Primitive | Name | Backend Key |
|---|---|---|
Lock("my-resource") |
my-resource |
lock:my-resource |
TaskLock("cleanup") |
cleanup |
tasklock:cleanup |
LeaderElection("main") |
main |
leader:main |
Existing locks stored in Redis or PostgreSQL will no longer match after upgrading. A running instance on the old version and one on the new version will not see each other's locks.
Upgrade all running instances together so they use the same key format. Old keys expire automatically via their lease duration (Redis PEXPIRE / PostgreSQL expire_at).
0.4.1 - 2026-03-13
Docs
- 📝 Add Task Lock to synchronization primitives guide.
Internal
- ⬆️ Bump actions/checkout to v6 and astral-sh/setup-uv to v7.
0.4.0 - 2026-03-13
Features
- ✨ Add
TaskLockfor distributed task locking with auto-renewal. - ✨ Add
GREL_LOG_TIMEZONEsupport for configurable timezone in logging output. - ✨ Add OpenTelemetry trace context injection into log records.
- ✨ Add
structlogas alternative logging backend. - ✨ Add configurable JSON serializer (
json/orjson) for logging.
Docs
- 📝 Add logging benchmark and performance documentation.
Internal
0.3.2 - 2026-01-27
Internal
- 👷 Migrate from mypy to Astral ty for type checking. PR #45.
- 🔧 Add Python 3.14 support. PR #47.
- 🔧 Switch build system to
uv_build. PR #49. - 💚 Simplify CI and release workflow. PR #24.
0.3.1 - 2025-06-05
Docs
- 📝 Add resilience patterns section and update links in README and index.
Internal
- 💚 Fix release pipeline and GitHub Pages deployment permissions.
0.3.0 - 2025-06-05
Features
- ✨ Add Circuit Breaker resilience pattern. PR #18.
Docs
- 📝 Refactor code examples to use snippets.
Internal
0.2.3 - 2024-12-04
Features
- ✨ Add Redis key prefix support to avoid conflicts in shared instances.
- ✨ Add Redis and PostgreSQL settings management from environment variables.
0.2.2 - 2024-11-28
Features
- ✨ Add PostgreSQL backend configuration from environment variables.
Internal
0.2.1 - 2024-11-26
Internal
- 💚 Set up release workflow with version tagging.
0.2.0 - 2024-11-26
First public release.
Features
- ✨ Add distributed
Lockwith lease-based expiration. - ✨ Add
LeaderElectionfor single-leader task execution. - ✨ Add
IntervalTaskscheduler for periodic tasks with synchronization support. - ✨ Add Redis, PostgreSQL, and in-memory synchronization backends.
- ✨ Add logging module with JSON and TEXT formatting via
GREL_LOG_LEVELandGREL_LOG_FORMATenvironment variables.
Docs
- 📝 Add MkDocs documentation site with Material theme.
Internal
- 👷 Add unified CI workflow with linting, testing, and coverage.