mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
* fix(redis): share one native circuit breaker between RedisCache and the Rust stores RedisCache and the native Redis client each kept their own breaker, so once the cache learned the coordination Redis was wedged, a spend-counter write through the native store still paid a full socket timeout. Every Redis target now has one Rust breaker, looked up by NativeRedisBreaker.shared and injected into both the RedisCache guard and the native client. The Python FSM is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(rust): cargo fmt the shared breaker Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(redis): close the breaker when the recovery probe gets a Redis reply error A command error such as WRONGTYPE means Redis answered, but the probe was left unsettled and its drop reopened the breaker for another recovery timeout. Both the RedisCache guard and the native client now settle such a probe as a success. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(redis): pass a NativeRedisBreaker and isolate the shared breaker per test The sensitive routing test still passed RedisBreakerSettings as the store breaker, and the session affinity fixture reused one Redis URL, so a breaker an earlier test opened leaked into the next. Also make the RUF043 match pattern raw. * fix(redis): key the shared breaker by server and close the three audit gaps The breaker registry now keys on the Redis server (host and port, unix socket, cluster node set or sentinel service) instead of the full connection target, so caches that differ only in db, credentials or url spelling share one breaker. A READONLY reply from a replica mid-failover no longer counts as Redis answering, so it can't reset a run of failures. A new RedisCache refreshes the breaker state gauge, which the deleted Python breaker did at construction. A shared native client is only reused by a store that passes the same breaker. * refactor(redis): split redis-types into modules and drop redundant pyo3 attribute annotations lib.rs now only declares modules and re-exports, with resolution, target, tls, auth and endpoint each in their own file. Derived FromPyObject already reads a struct field as the attribute of the same name, so #[pyo3(attribute)] on the breaker and store settings did nothing. * refactor(redis): drop redundant pyo3 attribute annotations in the stores and let PyO3 build the cooldown snapshot Derived FromPyObject already reads each field as the attribute of the same name. The cooldown snapshot returns an IndexMap, which PyO3 turns into the same dict in the same order. * refactor(redis): return a HashMap from the three memory-tier snapshots The snapshots read cache-memory's HashMap, so there is no order to keep. PyO3 builds the same dict from a std HashMap, which replaces the hand-written PyDict loops in the spend counter and router usage stores and the IndexMap in the cooldown store. The rate-limit slot registry keeps IndexMap because it hands back a Python dict in the order Python wrote it. * refactor(redis): share Redis breakers and clients through one RedisHosts per host redis-connect's RedisHosts now owns the per-server breaker and the shared clients that python-bridge kept in two process-wide statics. A client takes breaker settings and admits through its server's breaker, so no caller can pair a client with a breaker of its own, and clients are keyed by Resolution alone. This drops RedisClient::guarded_by and Breaker::same. python-bridge keeps one RedisHosts per process and calls it with the GIL released. The gateway builds one per connect, so its coordination and response cache on the same server now share a breaker. RedisStoreSettings.breaker is back to breaker settings, as on main. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2 KiB
2 KiB
fsmis a generic state machine runtime and knows nothing about breakers.breakeris a generic circuit breaker and knows nothing about Redis. Deciding which errors count as failures belongs to the consumerMachine::stepstays pure.&selfis config and time comes in on the event, never read inside the machine. The only clock read isAdmission's drop, at the call-site boundary- Transition rules are one
match (state, event)per machine, with no macro DSL and no_arm that would hide a new state from the compiler Sharedcounts a transition, bumps the generation and calls the observer only when the state's enum variant changes. Data inside a variant (failure counters,since) changes without a transition- The observer runs under the lock so transitions are seen in generation order. Keep it to logging or a metric
- A disabled breaker is
Option<Breaker>::Noneat the consumer, not a flag inside the machine.Configcomes in already resolved, readingREDIS_CIRCUIT_BREAKER_*env is the consumer's job
The only Redis breaker
- Python's
RedisCacheadmits through this breaker viaNativeRedisBreaker(python-bridge/src/redis/breaker.rs), sharing one per Redis server with the native client throughredis-connect'sRedisHosts, so the PythonRedisCircuitBreakerit replaced is gone - The rstest table
matches_the_python_redis_circuit_breakerwas checked case by case against that Python class and now pins its behavior. A behavior change here changes what every Redis caller in the proxy sees - Divergences from the old Python class, each on purpose
- monotonic
Instantinstead oftime.time(), so a wall clock jump can't open or close the breaker - a probe
Admissiondropped without an outcome records a hard failure and reopens. Python's probe that dies onCancelledError(aBaseException) records nothing and leaves the breaker HALF_OPEN for good, and a dropped Rust future is common _failure_countis dropped, it only fed a log line- no
enabledflag, see above
- monotonic