feat(failover): package-level default observer for transparently-built chains
CI / Build, Test & Lint (push) Successful in 10m43s

The transparent comma-Parse path builds failover chains via NewFailoverModel
with no options, so defaultFailoverConfig() left the observer nil and observers
only fired when a caller passed WithFailoverObserver explicitly. Add a
package-level default observer (SetFailoverObserver / DefaultFailoverObserver),
guarded by the existing defaultsMu, and seed it in defaultFailoverConfig() so
chains built transparently still notify it. An explicit WithFailoverObserver
still overrides the default per-chain. mort sets this at boot to persist
failover events.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 00:43:24 +02:00
parent 361999550e
commit 1206261e6a
3 changed files with 170 additions and 0 deletions
+36
View File
@@ -27,6 +27,12 @@ var (
// DefaultFailoverBackoff is the default exponential-with-jitter backoff.
DefaultFailoverBackoff = defaultBackoff
// defaultFailoverObserver is the package-level observer applied to chains
// built without an explicit WithFailoverObserver (e.g. the transparent
// comma-Parse path). Kept unexported and behind defaultsMu so reads/writes
// are race-safe under -race. mort sets this at boot to persist failover events.
defaultFailoverObserver FailoverObserver
defaultsMu sync.Mutex
)
@@ -62,6 +68,31 @@ func SetFailoverDefaults(maxRetries int, cooldown time.Duration) {
DefaultFailoverCooldown = cooldown
}
// SetFailoverObserver sets the package-level default observer notified on
// failover decisions for chains built without an explicit WithFailoverObserver.
//
// Why: the transparent comma-Parse path builds chains via NewFailoverModel with
// no options, so without a package default no observer ever fires; mort sets
// this once at boot to persist failover events from every chain.
// What: stores the observer under defaultsMu; pass nil to disable.
// Test: set an observer, build a no-option chain, assert it fires on failover.
func SetFailoverObserver(obs FailoverObserver) {
defaultsMu.Lock()
defer defaultsMu.Unlock()
defaultFailoverObserver = obs
}
// DefaultFailoverObserver returns the current package-level default observer.
//
// Why: lets tests assert/restore the default without reaching into the unexported var.
// What: reads defaultFailoverObserver under defaultsMu.
// Test: set via SetFailoverObserver, assert this returns a non-nil func.
func DefaultFailoverObserver() FailoverObserver {
defaultsMu.Lock()
defer defaultsMu.Unlock()
return defaultFailoverObserver
}
// ---------------------------------------------------------------------------
// Global model health (process-wide bench registry)
// ---------------------------------------------------------------------------
@@ -285,6 +316,11 @@ func defaultFailoverConfig() failoverConfig {
maxRetries: DefaultFailoverMaxRetries,
cooldown: DefaultFailoverCooldown,
backoff: DefaultFailoverBackoff,
// Seed the package-level default observer. An explicit
// WithFailoverObserver applied after this in NewFailoverModel/ParseChain
// overrides it for that chain. Read under the same defaultsMu we already
// hold (a single Lock above), so no re-lock / deadlock.
observer: defaultFailoverObserver,
}
}