ops_runtime_snapshot_test.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "sync"
  6. "sync/atomic"
  7. "testing"
  8. "time"
  9. )
  10. var (
  11. benchmarkOpsMonitoringEnabled bool
  12. benchmarkOpsAdvancedSettings OpsAdvancedSettings
  13. )
  14. type opsRuntimeRefreshRepo struct {
  15. SettingRepository
  16. mu sync.RWMutex
  17. values map[string]string
  18. fail atomic.Bool
  19. calls atomic.Int64
  20. }
  21. func (r *opsRuntimeRefreshRepo) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
  22. r.calls.Add(1)
  23. select {
  24. case <-ctx.Done():
  25. return nil, ctx.Err()
  26. default:
  27. }
  28. if r.fail.Load() {
  29. return nil, errors.New("settings unavailable")
  30. }
  31. r.mu.RLock()
  32. defer r.mu.RUnlock()
  33. out := make(map[string]string, len(keys))
  34. for _, key := range keys {
  35. if value, ok := r.values[key]; ok {
  36. out[key] = value
  37. }
  38. }
  39. return out, nil
  40. }
  41. func (r *opsRuntimeRefreshRepo) set(key, value string) {
  42. r.mu.Lock()
  43. r.values[key] = value
  44. r.mu.Unlock()
  45. }
  46. func waitForOpsRefresh(t *testing.T, timeout time.Duration, condition func() bool) {
  47. t.Helper()
  48. deadline := time.Now().Add(timeout)
  49. for time.Now().Before(deadline) {
  50. if condition() {
  51. return
  52. }
  53. time.Sleep(time.Millisecond)
  54. }
  55. t.Fatal("condition not satisfied before timeout")
  56. }
  57. func TestOpsRuntimeSettingsSnapshotLoadsOnceAndServesHotPath(t *testing.T) {
  58. repo := newRuntimeSettingRepoStub()
  59. repo.values[SettingKeyOpsMonitoringEnabled] = "false"
  60. repo.values[SettingKeyOpsAdvancedSettings] = `{"ignore_context_canceled":false,"auto_refresh_interval_seconds":45}`
  61. svc := &OpsService{settingRepo: repo}
  62. svc.initRuntimeSettings(context.Background())
  63. if repo.getMultipleCalls != 1 {
  64. t.Fatalf("startup GetMultiple calls = %d, want 1", repo.getMultipleCalls)
  65. }
  66. for range 1000 {
  67. if svc.IsMonitoringEnabled(context.Background()) {
  68. t.Fatal("monitoring enabled, want false")
  69. }
  70. cfg, err := svc.GetOpsAdvancedSettings(context.Background())
  71. if err != nil {
  72. t.Fatalf("GetOpsAdvancedSettings() error = %v", err)
  73. }
  74. if cfg.AutoRefreshIntervalSec != 45 {
  75. t.Fatalf("AutoRefreshIntervalSec = %d, want 45", cfg.AutoRefreshIntervalSec)
  76. }
  77. }
  78. if repo.getValueCalls != 0 || repo.getMultipleCalls != 1 {
  79. t.Fatalf("hot path touched repository: get=%d get_multiple=%d", repo.getValueCalls, repo.getMultipleCalls)
  80. }
  81. }
  82. func TestOpsRuntimeSettingsAdministrativeUpdatesAreImmediatelyVisible(t *testing.T) {
  83. svc := &OpsService{}
  84. svc.initRuntimeSettings(context.Background())
  85. svc.SetMonitoringEnabled(false)
  86. if svc.IsMonitoringEnabled(context.Background()) {
  87. t.Fatal("monitoring update was not visible")
  88. }
  89. cfg := defaultOpsAdvancedSettings()
  90. cfg.IgnoreNoAvailableAccounts = true
  91. svc.storeAdvancedSettingsSnapshot(cfg)
  92. got, err := svc.GetOpsAdvancedSettings(context.Background())
  93. if err != nil {
  94. t.Fatalf("GetOpsAdvancedSettings() error = %v", err)
  95. }
  96. if !got.IgnoreNoAvailableAccounts {
  97. t.Fatal("advanced settings update was not visible")
  98. }
  99. if svc.IsMonitoringEnabled(context.Background()) {
  100. t.Fatal("advanced update overwrote monitoring setting")
  101. }
  102. }
  103. func TestOpsRuntimeSettingsBackgroundRefreshConverges(t *testing.T) {
  104. repo := &opsRuntimeRefreshRepo{values: map[string]string{
  105. SettingKeyOpsMonitoringEnabled: "false",
  106. SettingKeyOpsRuntimeLogConfig: `{"persist_access_logs":false}`,
  107. }}
  108. sink := &OpsSystemLogSink{}
  109. svc := &OpsService{settingRepo: repo, systemLogSink: sink}
  110. svc.initRuntimeSettings(context.Background())
  111. if svc.IsMonitoringEnabled(context.Background()) {
  112. t.Fatal("initial monitoring state = true, want false")
  113. }
  114. repo.set(SettingKeyOpsMonitoringEnabled, "true")
  115. repo.set(SettingKeyOpsRuntimeLogConfig, `{"persist_access_logs":true}`)
  116. svc.startRuntimeSettingsRefresh(context.Background(), 5*time.Millisecond, 0, 50*time.Millisecond)
  117. t.Cleanup(svc.StopRuntimeSettingsRefresh)
  118. waitForOpsRefresh(t, time.Second, func() bool {
  119. return svc.IsMonitoringEnabled(context.Background()) &&
  120. sink.persistAccessLogs.Load() &&
  121. svc.RuntimeSettingsRefreshHealth().SuccessTotal > 0
  122. })
  123. }
  124. func TestOpsRuntimeSettingsRefreshUsesSafeAccessLogDefault(t *testing.T) {
  125. repo := &opsRuntimeRefreshRepo{values: map[string]string{
  126. SettingKeyOpsRuntimeLogConfig: `{"persist_access_logs":true}`,
  127. }}
  128. sink := &OpsSystemLogSink{}
  129. svc := &OpsService{settingRepo: repo, systemLogSink: sink}
  130. svc.initRuntimeSettings(context.Background())
  131. if !sink.persistAccessLogs.Load() {
  132. t.Fatal("valid runtime config did not enable access-log persistence")
  133. }
  134. repo.set(SettingKeyOpsRuntimeLogConfig, `{invalid-json}`)
  135. if err := svc.RefreshRuntimeSettings(context.Background()); err != nil {
  136. t.Fatalf("RefreshRuntimeSettings() error = %v", err)
  137. }
  138. if sink.persistAccessLogs.Load() {
  139. t.Fatal("invalid runtime config should fail closed for access-log persistence")
  140. }
  141. }
  142. func TestOpsRuntimeSettingsRefreshFailuresKeepLastKnownGoodSnapshot(t *testing.T) {
  143. repo := &opsRuntimeRefreshRepo{values: map[string]string{
  144. SettingKeyOpsMonitoringEnabled: "false",
  145. SettingKeyOpsAdvancedSettings: `{"ignore_no_available_accounts":true}`,
  146. SettingKeyOpsRuntimeLogConfig: `{"persist_access_logs":true}`,
  147. }}
  148. sink := &OpsSystemLogSink{}
  149. svc := &OpsService{settingRepo: repo, systemLogSink: sink}
  150. svc.initRuntimeSettings(context.Background())
  151. if !sink.persistAccessLogs.Load() {
  152. t.Fatal("initial access-log setting was not applied")
  153. }
  154. repo.fail.Store(true)
  155. svc.startRuntimeSettingsRefresh(context.Background(), 5*time.Millisecond, 0, 50*time.Millisecond)
  156. t.Cleanup(svc.StopRuntimeSettingsRefresh)
  157. waitForOpsRefresh(t, time.Second, func() bool {
  158. return svc.RuntimeSettingsRefreshHealth().FailureTotal >= 3
  159. })
  160. if svc.IsMonitoringEnabled(context.Background()) {
  161. t.Fatal("failed refresh overwrote last known monitoring state")
  162. }
  163. if !svc.OpsAdvancedSettingsSnapshot().IgnoreNoAvailableAccounts {
  164. t.Fatal("failed refresh overwrote last known advanced settings")
  165. }
  166. if !sink.persistAccessLogs.Load() {
  167. t.Fatal("failed refresh overwrote last known access-log setting")
  168. }
  169. }
  170. func TestOpsRuntimeSettingsRefreshStopEndsLifecycle(t *testing.T) {
  171. repo := &opsRuntimeRefreshRepo{values: map[string]string{}}
  172. svc := &OpsService{settingRepo: repo}
  173. svc.initRuntimeSettings(context.Background())
  174. svc.startRuntimeSettingsRefresh(context.Background(), 5*time.Millisecond, 0, 50*time.Millisecond)
  175. waitForOpsRefresh(t, time.Second, func() bool {
  176. return svc.RuntimeSettingsRefreshHealth().SuccessTotal > 0
  177. })
  178. svc.StopRuntimeSettingsRefresh()
  179. callsAfterStop := repo.calls.Load()
  180. time.Sleep(20 * time.Millisecond)
  181. if got := repo.calls.Load(); got != callsAfterStop {
  182. t.Fatalf("refresh continued after Stop: before=%d after=%d", callsAfterStop, got)
  183. }
  184. if svc.RuntimeSettingsRefreshHealth().Running {
  185. t.Fatal("refresh health still reports running after Stop")
  186. }
  187. // Idempotence is part of the cleanup contract.
  188. svc.StopRuntimeSettingsRefresh()
  189. }
  190. func BenchmarkOpsRuntimeSettingsSnapshotRead(b *testing.B) {
  191. svc := &OpsService{}
  192. svc.initRuntimeSettings(context.Background())
  193. ctx := context.Background()
  194. b.ReportAllocs()
  195. b.ResetTimer()
  196. for range b.N {
  197. benchmarkOpsMonitoringEnabled = svc.IsMonitoringEnabled(ctx)
  198. benchmarkOpsAdvancedSettings = svc.OpsAdvancedSettingsSnapshot()
  199. }
  200. }