Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
135 lines
3.4 KiB
Go
135 lines
3.4 KiB
Go
package service
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Group availability is derived from recent real request outcomes instead of an
|
|
// active probe. A group whose attempts keep failing is skipped during group
|
|
// selection until a cooldown elapses, after which it is eligible again so the
|
|
// next request acts as a probe.
|
|
const (
|
|
groupAvailabilityMaxSamples = 20
|
|
groupAvailabilityMinSamples = 4
|
|
groupAvailabilityFailRatio = 0.6
|
|
groupAvailabilityFailureBurst = 3
|
|
groupAvailabilitySampleLifetime = 120 * time.Second
|
|
groupAvailabilityCooldown = 20 * time.Second
|
|
)
|
|
|
|
type groupOutcome struct {
|
|
at time.Time
|
|
success bool
|
|
}
|
|
|
|
type groupAvailabilityState struct {
|
|
mu sync.Mutex
|
|
outcomes []groupOutcome
|
|
}
|
|
|
|
var (
|
|
groupAvailabilityMu sync.RWMutex
|
|
groupAvailabilityStates = make(map[string]*groupAvailabilityState)
|
|
)
|
|
|
|
func groupAvailabilityStateFor(group string) *groupAvailabilityState {
|
|
groupAvailabilityMu.RLock()
|
|
state, ok := groupAvailabilityStates[group]
|
|
groupAvailabilityMu.RUnlock()
|
|
if ok {
|
|
return state
|
|
}
|
|
groupAvailabilityMu.Lock()
|
|
defer groupAvailabilityMu.Unlock()
|
|
if state, ok := groupAvailabilityStates[group]; ok {
|
|
return state
|
|
}
|
|
state = &groupAvailabilityState{}
|
|
groupAvailabilityStates[group] = state
|
|
return state
|
|
}
|
|
|
|
// RecordGroupOutcome records the result of one upstream attempt for a group.
|
|
func RecordGroupOutcome(group string, success bool) {
|
|
if group == "" {
|
|
return
|
|
}
|
|
state := groupAvailabilityStateFor(group)
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
now := time.Now()
|
|
state.outcomes = append(state.outcomes, groupOutcome{at: now, success: success})
|
|
trimmed := state.outcomes[:0]
|
|
for _, outcome := range state.outcomes {
|
|
if now.Sub(outcome.at) <= groupAvailabilitySampleLifetime {
|
|
trimmed = append(trimmed, outcome)
|
|
}
|
|
}
|
|
state.outcomes = trimmed
|
|
if len(state.outcomes) > groupAvailabilityMaxSamples {
|
|
state.outcomes = state.outcomes[len(state.outcomes)-groupAvailabilityMaxSamples:]
|
|
}
|
|
}
|
|
|
|
// IsGroupAvailable reports whether a group should be preferred by selection.
|
|
// A group without recorded traffic is available, so a fresh group is never
|
|
// skipped before it has been tried.
|
|
func IsGroupAvailable(group string) bool {
|
|
if group == "" {
|
|
return true
|
|
}
|
|
groupAvailabilityMu.RLock()
|
|
state, ok := groupAvailabilityStates[group]
|
|
groupAvailabilityMu.RUnlock()
|
|
if !ok {
|
|
return true
|
|
}
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
now := time.Now()
|
|
samples := make([]groupOutcome, 0, len(state.outcomes))
|
|
for _, outcome := range state.outcomes {
|
|
if now.Sub(outcome.at) <= groupAvailabilitySampleLifetime {
|
|
samples = append(samples, outcome)
|
|
}
|
|
}
|
|
if len(samples) == 0 {
|
|
return true
|
|
}
|
|
last := samples[len(samples)-1]
|
|
if last.success {
|
|
return true
|
|
}
|
|
if now.Sub(last.at) >= groupAvailabilityCooldown {
|
|
return true
|
|
}
|
|
burst := 0
|
|
for i := len(samples) - 1; i >= 0; i-- {
|
|
if samples[i].success {
|
|
break
|
|
}
|
|
burst++
|
|
}
|
|
if burst >= groupAvailabilityFailureBurst {
|
|
return false
|
|
}
|
|
if len(samples) < groupAvailabilityMinSamples {
|
|
return true
|
|
}
|
|
failures := 0
|
|
for _, sample := range samples {
|
|
if !sample.success {
|
|
failures++
|
|
}
|
|
}
|
|
return float64(failures)/float64(len(samples)) < groupAvailabilityFailRatio
|
|
}
|
|
|
|
// ResetGroupAvailability clears all tracked state. Intended for tests.
|
|
func ResetGroupAvailability() {
|
|
groupAvailabilityMu.Lock()
|
|
defer groupAvailabilityMu.Unlock()
|
|
groupAvailabilityStates = make(map[string]*groupAvailabilityState)
|
|
}
|