Skip to content
This repository was archived by the owner on Jul 24, 2026. It is now read-only.

Commit ff54632

Browse files
authored
Merge pull request altaidevorg#57 from efecnc/feat/resettable-fallback-providers
feat(agent): make fallback providers re-settable (RwLock, not set-once)
2 parents cae2974 + 90d338a commit ff54632

1 file changed

Lines changed: 25 additions & 11 deletions

File tree

src/agent/mod.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,19 +2176,32 @@ pub fn build_fallback_specs(
21762176
.collect()
21772177
}
21782178

2179-
/// Process-wide fallback providers, set once at startup (like the primary, these are config).
2180-
/// Empty until set, so failover is simply inert when unconfigured or in tests.
2181-
static FALLBACK_PROVIDERS: std::sync::OnceLock<Vec<FallbackProviderSpec>> = std::sync::OnceLock::new();
2182-
2183-
/// Install the fallback provider list. Call once at startup, after the primary is built; a second
2184-
/// call is ignored (OnceLock). **Do not call from tests** — the `OnceLock` can't be reset, so it
2185-
/// would pollute every other test in the binary. Tests drive [`try_fallbacks`] directly instead.
2179+
/// Process-wide fallback providers (config, like the primary). Empty until set, so failover is
2180+
/// simply inert when unconfigured. Stored behind an `RwLock` rather than a `OnceLock` so an embedder
2181+
/// that rebuilds its runtime — e.g. a desktop app that re-bootstraps on a provider/model switch —
2182+
/// can refresh the list, keeping the fallbacks consistent with the current primary instead of
2183+
/// frozen at the first call.
2184+
static FALLBACK_PROVIDERS: std::sync::RwLock<Vec<FallbackProviderSpec>> =
2185+
std::sync::RwLock::new(Vec::new());
2186+
2187+
/// Install (or replace) the fallback provider list. Safe to call repeatedly — each call replaces the
2188+
/// previous list; pass an empty vec to disable failover.
2189+
///
2190+
/// **Don't mutate this from tests.** It's process-global and the reasoning-loop tests read it (via
2191+
/// `chat_with_retry` on primary exhaustion), so setting it from a test races the rest of the binary
2192+
/// under Cargo's parallel runner. Drive [`try_fallbacks`] directly instead, as the failover tests do.
21862193
pub fn set_fallback_providers(specs: Vec<FallbackProviderSpec>) {
2187-
let _ = FALLBACK_PROVIDERS.set(specs);
2194+
// Recover from a poisoned lock: we replace the list wholesale, so any state a panicking writer
2195+
// left behind is irrelevant — honoring the poison would instead wedge failover config for the
2196+
// rest of the process.
2197+
let mut guard = FALLBACK_PROVIDERS.write().unwrap_or_else(|e| e.into_inner());
2198+
*guard = specs;
21882199
}
21892200

2190-
fn fallback_providers() -> &'static [FallbackProviderSpec] {
2191-
FALLBACK_PROVIDERS.get().map(Vec::as_slice).unwrap_or(&[])
2201+
/// Snapshot of the current fallback list. Returns an owned clone so the lock isn't held across the
2202+
/// (async) failover chat calls. Recovers a poisoned read lock so failover keeps working.
2203+
fn fallback_providers() -> Vec<FallbackProviderSpec> {
2204+
FALLBACK_PROVIDERS.read().unwrap_or_else(|e| e.into_inner()).clone()
21922205
}
21932206

21942207
/// Result of attempting the configured fallback providers.
@@ -2355,8 +2368,9 @@ async fn chat_with_retry(
23552368
// Primary exhausted. Before surfacing a failure, try each configured fallback provider once, so
23562369
// a transient outage / key rotation / model deprecation on the primary doesn't drop a long
23572370
// unattended turn. The primary stays the active provider — failover is per-call.
2371+
let fallbacks = fallback_providers();
23582372
match try_fallbacks(
2359-
fallback_providers(),
2373+
&fallbacks,
23602374
|s| {
23612375
crate::provider::create_provider(&s.provider_name, &s.base_url, &s.api_key, &s.model_name)
23622376
},

0 commit comments

Comments
 (0)