github.com/cilium/statedb@v0.3.2/reconciler/script_test.go (about) 1 package reconciler_test 2 3 import ( 4 "context" 5 "errors" 6 "expvar" 7 "fmt" 8 "iter" 9 "maps" 10 "slices" 11 "sort" 12 "strconv" 13 "strings" 14 "sync" 15 "sync/atomic" 16 "testing" 17 "time" 18 19 "github.com/cilium/hive" 20 "github.com/cilium/hive/cell" 21 "github.com/cilium/hive/hivetest" 22 "github.com/cilium/hive/job" 23 "github.com/cilium/hive/script" 24 "github.com/cilium/hive/script/scripttest" 25 "github.com/cilium/statedb" 26 "github.com/cilium/statedb/index" 27 "github.com/cilium/statedb/reconciler" 28 "github.com/stretchr/testify/assert" 29 "github.com/stretchr/testify/require" 30 "golang.org/x/time/rate" 31 ) 32 33 func newScriptTest(t *testing.T) *script.Engine { 34 log := hivetest.Logger(t) 35 36 var ( 37 ops = &mockOps{} 38 db *statedb.DB 39 r reconciler.Reconciler[*testObject] 40 reconcilerParams reconciler.Params 41 reconcilerLifecycle = &cell.DefaultLifecycle{} 42 markInit func() 43 ) 44 45 expVarMetrics := reconciler.NewUnpublishedExpVarMetrics() 46 47 testObjects, err := statedb.NewTable("test-objects", idIndex) 48 require.NoError(t, err, "NewTable") 49 50 hive := hive.New( 51 statedb.Cell, 52 job.Cell, 53 54 cell.Provide( 55 cell.NewSimpleHealth, 56 func(h *cell.SimpleHealth) hive.ScriptCmdOut { 57 return hive.NewScriptCmd( 58 "health", 59 cell.SimpleHealthCmd(h)) 60 }, 61 ), 62 63 cell.Module( 64 "test", 65 "Test", 66 67 cell.Provide( 68 func() reconciler.Metrics { 69 return expVarMetrics 70 }), 71 72 cell.Invoke( 73 func(db_ *statedb.DB, p_ reconciler.Params) error { 74 db = db_ 75 reconcilerParams = p_ 76 return db.RegisterTable(testObjects) 77 }, 78 79 func(lc cell.Lifecycle) { 80 lc.Append(cell.Hook{ 81 OnStop: func(ctx cell.HookContext) error { return reconcilerLifecycle.Stop(log, ctx) }, 82 }) 83 }, 84 85 func(h *cell.SimpleHealth) { 86 wtxn := db.WriteTxn(testObjects) 87 done := testObjects.RegisterInitializer(wtxn, "test") 88 wtxn.Commit() 89 markInit = func() { 90 wtxn := db.WriteTxn(testObjects) 91 done(wtxn) 92 wtxn.Commit() 93 } 94 }), 95 ), 96 ) 97 98 cmds, err := hive.ScriptCommands(log) 99 require.NoError(t, err) 100 101 cmds["mark-init"] = script.Command( 102 script.CmdUsage{Summary: "Mark table as initialized"}, 103 func(s *script.State, args ...string) (script.WaitFunc, error) { 104 markInit() 105 return nil, nil 106 }, 107 ) 108 109 cmds["start-reconciler"] = script.Command( 110 script.CmdUsage{Summary: "Mark table as initialized"}, 111 func(s *script.State, args ...string) (script.WaitFunc, error) { 112 opts := []reconciler.Option{ 113 // Speed things up a bit. Quick retry interval does mean we can't 114 // assert the metrics exactly (e.g. error count depends on how 115 // many retries happened). 116 reconciler.WithRetry(50*time.Millisecond, 50*time.Millisecond), 117 reconciler.WithRoundLimits(1000, rate.NewLimiter(1000.0, 10)), 118 } 119 var bops reconciler.BatchOperations[*testObject] 120 for _, arg := range args { 121 switch arg { 122 case "with-prune": 123 opts = append(opts, reconciler.WithPruning(time.Hour)) 124 case "with-refresh": 125 opts = append(opts, reconciler.WithRefreshing(50*time.Millisecond, rate.NewLimiter(100.0, 1))) 126 case "with-batchops": 127 bops = ops 128 default: 129 return nil, fmt.Errorf("unexpected arg, expected 'with-prune', 'with-batchops' or 'with-refresh'") 130 } 131 } 132 reconcilerParams.Lifecycle = reconcilerLifecycle 133 r, err = reconciler.Register( 134 reconcilerParams, 135 testObjects, 136 (*testObject).Clone, 137 (*testObject).SetStatus, 138 (*testObject).GetStatus, 139 ops, 140 bops, 141 opts...) 142 if err != nil { 143 return nil, err 144 } 145 return nil, reconcilerLifecycle.Start(log, context.TODO()) 146 }, 147 ) 148 149 cmds["prune"] = script.Command( 150 script.CmdUsage{Summary: "Trigger pruning"}, 151 func(s *script.State, args ...string) (script.WaitFunc, error) { 152 r.Prune() 153 return nil, nil 154 }, 155 ) 156 157 cmds["set-faulty"] = script.Command( 158 script.CmdUsage{Summary: "Mark target faulty or not"}, 159 func(s *script.State, args ...string) (script.WaitFunc, error) { 160 if args[0] == "true" { 161 t.Logf("Marked target faulty") 162 ops.faulty.Store(true) 163 } else { 164 t.Logf("Marked target healthy") 165 ops.faulty.Store(false) 166 } 167 return nil, nil 168 }, 169 ) 170 171 cmds["expect-ops"] = script.Command( 172 script.CmdUsage{Summary: "Assert ops"}, 173 func(s *script.State, args ...string) (script.WaitFunc, error) { 174 sort.Strings(args) 175 var actual []string 176 cond := func() bool { 177 actual = ops.history.take(len(args)) 178 sort.Strings(actual) 179 return slices.Equal(args, actual) 180 } 181 for s.Context().Err() == nil { 182 if cond() { 183 return nil, nil 184 } 185 } 186 return nil, fmt.Errorf("operations mismatch, expected %v, got %v", args, actual) 187 }, 188 ) 189 190 cmds["expvar"] = script.Command( 191 script.CmdUsage{Summary: "Print expvars to stdout"}, 192 func(s *script.State, args ...string) (script.WaitFunc, error) { 193 return func(*script.State) (stdout, stderr string, err error) { 194 var buf strings.Builder 195 expVarMetrics.Map().Do(func(kv expvar.KeyValue) { 196 switch v := kv.Value.(type) { 197 case expvar.Func: 198 // skip 199 case *expvar.Map: 200 v.Do(func(kv2 expvar.KeyValue) { 201 fmt.Fprintf(&buf, "%s.%s: %s\n", kv.Key, kv2.Key, kv2.Value) 202 }) 203 default: 204 fmt.Fprintf(&buf, "%s: %s\n", kv.Key, kv.Value) 205 } 206 }) 207 return buf.String(), "", nil 208 }, nil 209 }, 210 ) 211 212 require.NoError(t, err, "ScriptCommands") 213 maps.Insert(cmds, maps.All(script.DefaultCmds())) 214 215 t.Cleanup(func() { 216 assert.NoError(t, hive.Stop(log, context.TODO())) 217 }) 218 219 return &script.Engine{ 220 Cmds: cmds, 221 } 222 } 223 224 func TestScript(t *testing.T) { 225 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 226 t.Cleanup(cancel) 227 scripttest.Test(t, 228 ctx, func() *script.Engine { 229 return newScriptTest(t) 230 }, []string{}, "testdata/*.txtar") 231 } 232 233 type testObject struct { 234 ID uint64 235 Faulty bool 236 Updates int 237 Status reconciler.Status 238 } 239 240 var idIndex = statedb.Index[*testObject, uint64]{ 241 Name: "id", 242 FromObject: func(t *testObject) index.KeySet { 243 return index.NewKeySet(index.Uint64(t.ID)) 244 }, 245 FromKey: index.Uint64, 246 Unique: true, 247 } 248 249 func (t *testObject) GetStatus() reconciler.Status { 250 return t.Status 251 } 252 253 func (t *testObject) SetStatus(status reconciler.Status) *testObject { 254 t.Status = status 255 return t 256 } 257 258 func (t *testObject) Clone() *testObject { 259 t2 := *t 260 return &t2 261 } 262 263 func (t *testObject) TableHeader() []string { 264 return []string{ 265 "ID", 266 "Faulty", 267 "StatusKind", 268 "StatusError", 269 } 270 } 271 272 func (t *testObject) TableRow() []string { 273 return []string{ 274 strconv.FormatUint(t.ID, 10), 275 strconv.FormatBool(t.Faulty), 276 string(t.Status.Kind), 277 t.Status.Error, 278 } 279 } 280 281 type opHistory struct { 282 mu sync.Mutex 283 history []opHistoryItem 284 } 285 286 type opHistoryItem = string 287 288 func opUpdate(id uint64) opHistoryItem { 289 return opHistoryItem(fmt.Sprintf("update(%d)", id)) 290 } 291 func opUpdateRefresh(id uint64) opHistoryItem { 292 return opHistoryItem(fmt.Sprintf("update-refresh(%d)", id)) 293 } 294 func opDelete(id uint64) opHistoryItem { 295 return opHistoryItem(fmt.Sprintf("delete(%d)", id)) 296 } 297 func opPrune(numDesiredObjects int) opHistoryItem { 298 return opHistoryItem(fmt.Sprintf("prune(n=%d)", numDesiredObjects)) 299 } 300 func opFail(item opHistoryItem) opHistoryItem { 301 return item + " fail" 302 } 303 304 func (o *opHistory) add(item opHistoryItem) { 305 o.mu.Lock() 306 o.history = append(o.history, item) 307 o.mu.Unlock() 308 } 309 310 func (o *opHistory) take(n int) []opHistoryItem { 311 o.mu.Lock() 312 defer o.mu.Unlock() 313 314 out := []opHistoryItem{} 315 for n > 0 { 316 idx := len(o.history) - n 317 if idx >= 0 { 318 out = append(out, o.history[idx]) 319 } 320 n-- 321 } 322 return out 323 } 324 325 type intMap struct { 326 sync.Map 327 } 328 329 func (m *intMap) incr(key uint64) { 330 if n, ok := m.Load(key); ok { 331 m.Store(key, n.(int)+1) 332 } else { 333 m.Store(key, 1) 334 } 335 } 336 337 type mockOps struct { 338 history opHistory 339 faulty atomic.Bool 340 updates intMap 341 } 342 343 // DeleteBatch implements recogciler.BatchOperations. 344 func (mt *mockOps) DeleteBatch(ctx context.Context, txn statedb.ReadTxn, batch []reconciler.BatchEntry[*testObject]) { 345 for i := range batch { 346 batch[i].Result = mt.Delete(ctx, txn, batch[i].Object) 347 } 348 } 349 350 // UpdateBatch implements reconciler.BatchOperations. 351 func (mt *mockOps) UpdateBatch(ctx context.Context, txn statedb.ReadTxn, batch []reconciler.BatchEntry[*testObject]) { 352 for i := range batch { 353 batch[i].Result = mt.Update(ctx, txn, batch[i].Object) 354 } 355 } 356 357 // Delete implements reconciler.Operations. 358 func (mt *mockOps) Delete(ctx context.Context, txn statedb.ReadTxn, obj *testObject) error { 359 if mt.faulty.Load() || obj.Faulty { 360 mt.history.add(opFail(opDelete(obj.ID))) 361 return errors.New("delete fail") 362 } 363 mt.history.add(opDelete(obj.ID)) 364 365 return nil 366 } 367 368 // Prune implements reconciler.Operations. 369 func (mt *mockOps) Prune(ctx context.Context, txn statedb.ReadTxn, objects iter.Seq2[*testObject, statedb.Revision]) error { 370 objs := statedb.Collect(objects) 371 if mt.faulty.Load() { 372 mt.history.add(opFail(opPrune(len(objs)))) 373 return errors.New("prune fail") 374 } 375 mt.history.add(opPrune(len(objs))) 376 return nil 377 } 378 379 // Update implements reconciler.Operations. 380 func (mt *mockOps) Update(ctx context.Context, txn statedb.ReadTxn, obj *testObject) error { 381 mt.updates.incr(obj.ID) 382 383 op := opUpdate(obj.ID) 384 if obj.Status.Kind == reconciler.StatusKindRefreshing { 385 op = opUpdateRefresh(obj.ID) 386 } 387 if mt.faulty.Load() || obj.Faulty { 388 mt.history.add(opFail(op)) 389 return errors.New("update fail") 390 } 391 mt.history.add(op) 392 obj.Updates += 1 393 394 return nil 395 } 396 397 var _ reconciler.Operations[*testObject] = &mockOps{} 398 var _ reconciler.BatchOperations[*testObject] = &mockOps{}