Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
187 lines
5.7 KiB
Go
187 lines
5.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
benchmarkOpsMonitoringEnabled bool
|
|
benchmarkOpsAdvancedSettings OpsAdvancedSettings
|
|
)
|
|
|
|
type opsRuntimeRefreshRepo struct {
|
|
SettingRepository
|
|
mu sync.RWMutex
|
|
values map[string]string
|
|
fail atomic.Bool
|
|
calls atomic.Int64
|
|
}
|
|
|
|
func (r *opsRuntimeRefreshRepo) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
|
r.calls.Add(1)
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
if r.fail.Load() {
|
|
return nil, errors.New("settings unavailable")
|
|
}
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make(map[string]string, len(keys))
|
|
for _, key := range keys {
|
|
if value, ok := r.values[key]; ok {
|
|
out[key] = value
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *opsRuntimeRefreshRepo) set(key, value string) {
|
|
r.mu.Lock()
|
|
r.values[key] = value
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
func waitForOpsRefresh(t *testing.T, timeout time.Duration, condition func() bool) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if condition() {
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatal("condition not satisfied before timeout")
|
|
}
|
|
|
|
func TestOpsRuntimeSettingsSnapshotLoadsOnceAndServesHotPath(t *testing.T) {
|
|
repo := newRuntimeSettingRepoStub()
|
|
repo.values[SettingKeyOpsMonitoringEnabled] = "false"
|
|
repo.values[SettingKeyOpsAdvancedSettings] = `{"ignore_context_canceled":false,"auto_refresh_interval_seconds":45}`
|
|
|
|
svc := &OpsService{settingRepo: repo}
|
|
svc.initRuntimeSettings(context.Background())
|
|
if repo.getMultipleCalls != 1 {
|
|
t.Fatalf("startup GetMultiple calls = %d, want 1", repo.getMultipleCalls)
|
|
}
|
|
|
|
for range 1000 {
|
|
if svc.IsMonitoringEnabled(context.Background()) {
|
|
t.Fatal("monitoring enabled, want false")
|
|
}
|
|
cfg, err := svc.GetOpsAdvancedSettings(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("GetOpsAdvancedSettings() error = %v", err)
|
|
}
|
|
if cfg.AutoRefreshIntervalSec != 45 {
|
|
t.Fatalf("AutoRefreshIntervalSec = %d, want 45", cfg.AutoRefreshIntervalSec)
|
|
}
|
|
}
|
|
if repo.getValueCalls != 0 || repo.getMultipleCalls != 1 {
|
|
t.Fatalf("hot path touched repository: get=%d get_multiple=%d", repo.getValueCalls, repo.getMultipleCalls)
|
|
}
|
|
}
|
|
|
|
func TestOpsRuntimeSettingsAdministrativeUpdatesAreImmediatelyVisible(t *testing.T) {
|
|
svc := &OpsService{}
|
|
svc.initRuntimeSettings(context.Background())
|
|
|
|
svc.SetMonitoringEnabled(false)
|
|
if svc.IsMonitoringEnabled(context.Background()) {
|
|
t.Fatal("monitoring update was not visible")
|
|
}
|
|
|
|
cfg := defaultOpsAdvancedSettings()
|
|
cfg.IgnoreNoAvailableAccounts = true
|
|
svc.storeAdvancedSettingsSnapshot(cfg)
|
|
got, err := svc.GetOpsAdvancedSettings(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("GetOpsAdvancedSettings() error = %v", err)
|
|
}
|
|
if !got.IgnoreNoAvailableAccounts {
|
|
t.Fatal("advanced settings update was not visible")
|
|
}
|
|
if svc.IsMonitoringEnabled(context.Background()) {
|
|
t.Fatal("advanced update overwrote monitoring setting")
|
|
}
|
|
}
|
|
|
|
func TestOpsRuntimeSettingsBackgroundRefreshConverges(t *testing.T) {
|
|
repo := &opsRuntimeRefreshRepo{values: map[string]string{SettingKeyOpsMonitoringEnabled: "false"}}
|
|
svc := &OpsService{settingRepo: repo}
|
|
svc.initRuntimeSettings(context.Background())
|
|
if svc.IsMonitoringEnabled(context.Background()) {
|
|
t.Fatal("initial monitoring state = true, want false")
|
|
}
|
|
|
|
repo.set(SettingKeyOpsMonitoringEnabled, "true")
|
|
svc.startRuntimeSettingsRefresh(context.Background(), 5*time.Millisecond, 0, 50*time.Millisecond)
|
|
t.Cleanup(svc.StopRuntimeSettingsRefresh)
|
|
waitForOpsRefresh(t, time.Second, func() bool {
|
|
return svc.IsMonitoringEnabled(context.Background()) && svc.RuntimeSettingsRefreshHealth().SuccessTotal > 0
|
|
})
|
|
}
|
|
|
|
func TestOpsRuntimeSettingsRefreshFailuresKeepLastKnownGoodSnapshot(t *testing.T) {
|
|
repo := &opsRuntimeRefreshRepo{values: map[string]string{
|
|
SettingKeyOpsMonitoringEnabled: "false",
|
|
SettingKeyOpsAdvancedSettings: `{"ignore_no_available_accounts":true}`,
|
|
}}
|
|
svc := &OpsService{settingRepo: repo}
|
|
svc.initRuntimeSettings(context.Background())
|
|
repo.fail.Store(true)
|
|
svc.startRuntimeSettingsRefresh(context.Background(), 5*time.Millisecond, 0, 50*time.Millisecond)
|
|
t.Cleanup(svc.StopRuntimeSettingsRefresh)
|
|
waitForOpsRefresh(t, time.Second, func() bool {
|
|
return svc.RuntimeSettingsRefreshHealth().FailureTotal >= 3
|
|
})
|
|
|
|
if svc.IsMonitoringEnabled(context.Background()) {
|
|
t.Fatal("failed refresh overwrote last known monitoring state")
|
|
}
|
|
if !svc.OpsAdvancedSettingsSnapshot().IgnoreNoAvailableAccounts {
|
|
t.Fatal("failed refresh overwrote last known advanced settings")
|
|
}
|
|
}
|
|
|
|
func TestOpsRuntimeSettingsRefreshStopEndsLifecycle(t *testing.T) {
|
|
repo := &opsRuntimeRefreshRepo{values: map[string]string{}}
|
|
svc := &OpsService{settingRepo: repo}
|
|
svc.initRuntimeSettings(context.Background())
|
|
svc.startRuntimeSettingsRefresh(context.Background(), 5*time.Millisecond, 0, 50*time.Millisecond)
|
|
waitForOpsRefresh(t, time.Second, func() bool {
|
|
return svc.RuntimeSettingsRefreshHealth().SuccessTotal > 0
|
|
})
|
|
|
|
svc.StopRuntimeSettingsRefresh()
|
|
callsAfterStop := repo.calls.Load()
|
|
time.Sleep(20 * time.Millisecond)
|
|
if got := repo.calls.Load(); got != callsAfterStop {
|
|
t.Fatalf("refresh continued after Stop: before=%d after=%d", callsAfterStop, got)
|
|
}
|
|
if svc.RuntimeSettingsRefreshHealth().Running {
|
|
t.Fatal("refresh health still reports running after Stop")
|
|
}
|
|
// Idempotence is part of the cleanup contract.
|
|
svc.StopRuntimeSettingsRefresh()
|
|
}
|
|
|
|
func BenchmarkOpsRuntimeSettingsSnapshotRead(b *testing.B) {
|
|
svc := &OpsService{}
|
|
svc.initRuntimeSettings(context.Background())
|
|
ctx := context.Background()
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for range b.N {
|
|
benchmarkOpsMonitoringEnabled = svc.IsMonitoringEnabled(ctx)
|
|
benchmarkOpsAdvancedSettings = svc.OpsAdvancedSettingsSnapshot()
|
|
}
|
|
}
|