github.com/cilium/statedb@v0.3.2/reconciler/multi_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package reconciler_test 5 6 import ( 7 "context" 8 "errors" 9 "iter" 10 "log/slog" 11 "sync/atomic" 12 "testing" 13 "time" 14 15 "github.com/cilium/hive" 16 "github.com/cilium/hive/cell" 17 "github.com/cilium/hive/hivetest" 18 "github.com/cilium/hive/job" 19 "github.com/cilium/statedb" 20 "github.com/cilium/statedb/index" 21 "github.com/cilium/statedb/reconciler" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 ) 25 26 type multiStatusObject struct { 27 ID uint64 28 Statuses reconciler.StatusSet 29 } 30 31 func (m *multiStatusObject) Clone() *multiStatusObject { 32 m2 := *m 33 return &m2 34 } 35 36 var multiStatusIndex = statedb.Index[*multiStatusObject, uint64]{ 37 Name: "id", 38 FromObject: func(t *multiStatusObject) index.KeySet { 39 return index.NewKeySet(index.Uint64(t.ID)) 40 }, 41 FromKey: index.Uint64, 42 Unique: true, 43 } 44 45 type multiMockOps struct { 46 numUpdates int 47 faulty atomic.Bool 48 } 49 50 // Delete implements reconciler.Operations. 51 func (m *multiMockOps) Delete(context.Context, statedb.ReadTxn, *multiStatusObject) error { 52 return nil 53 } 54 55 // Prune implements reconciler.Operations. 56 func (m *multiMockOps) Prune(context.Context, statedb.ReadTxn, iter.Seq2[*multiStatusObject, statedb.Revision]) error { 57 return nil 58 } 59 60 // Update implements reconciler.Operations. 61 func (m *multiMockOps) Update(ctx context.Context, txn statedb.ReadTxn, obj *multiStatusObject) error { 62 m.numUpdates++ 63 if m.faulty.Load() { 64 return errors.New("fail") 65 } 66 return nil 67 } 68 69 var _ reconciler.Operations[*multiStatusObject] = &multiMockOps{} 70 71 // TestMultipleReconcilers tests use of multiple reconcilers against 72 // a single object. 73 func TestMultipleReconcilers(t *testing.T) { 74 table, err := statedb.NewTable("objects", multiStatusIndex) 75 require.NoError(t, err, "NewTable") 76 77 var ops1, ops2 multiMockOps 78 var db *statedb.DB 79 80 hive := hive.New( 81 statedb.Cell, 82 job.Cell, 83 cell.Provide( 84 cell.NewSimpleHealth, 85 reconciler.NewExpVarMetrics, 86 func(r job.Registry, h cell.Health, lc cell.Lifecycle) job.Group { 87 g := r.NewGroup(h) 88 lc.Append(g) 89 return g 90 }, 91 ), 92 cell.Invoke(func(db_ *statedb.DB) error { 93 db = db_ 94 return db.RegisterTable(table) 95 }), 96 97 cell.Module("test1", "First reconciler", 98 cell.Invoke(func(params reconciler.Params) error { 99 _, err := reconciler.Register( 100 params, 101 table, 102 (*multiStatusObject).Clone, 103 func(obj *multiStatusObject, s reconciler.Status) *multiStatusObject { 104 obj.Statuses = obj.Statuses.Set("test1", s) 105 return obj 106 }, 107 func(obj *multiStatusObject) reconciler.Status { 108 return obj.Statuses.Get("test1") 109 }, 110 &ops1, 111 nil, 112 reconciler.WithRetry(time.Hour, time.Hour), 113 ) 114 return err 115 }), 116 ), 117 118 cell.Module("test2", "Second reconciler", 119 cell.Invoke(func(params reconciler.Params) error { 120 _, err := reconciler.Register( 121 params, 122 table, 123 (*multiStatusObject).Clone, 124 func(obj *multiStatusObject, s reconciler.Status) *multiStatusObject { 125 obj.Statuses = obj.Statuses.Set("test2", s) 126 return obj 127 }, 128 func(obj *multiStatusObject) reconciler.Status { 129 return obj.Statuses.Get("test2") 130 }, 131 &ops2, 132 nil, 133 reconciler.WithRetry(time.Hour, time.Hour), 134 ) 135 return err 136 }), 137 ), 138 ) 139 140 log := hivetest.Logger(t, hivetest.LogLevel(slog.LevelError)) 141 require.NoError(t, hive.Start(log, context.TODO()), "Start") 142 143 wtxn := db.WriteTxn(table) 144 table.Insert(wtxn, &multiStatusObject{ 145 ID: 1, 146 Statuses: reconciler.NewStatusSet(), 147 }) 148 wtxn.Commit() 149 150 var obj1 *multiStatusObject 151 for { 152 obj, _, watch, found := table.GetWatch(db.ReadTxn(), multiStatusIndex.Query(1)) 153 if found && 154 obj.Statuses.Get("test1").Kind == reconciler.StatusKindDone && 155 obj.Statuses.Get("test2").Kind == reconciler.StatusKindDone { 156 157 // Check that both reconcilers performed the update only once. 158 assert.Equal(t, 1, ops1.numUpdates) 159 assert.Equal(t, 1, ops2.numUpdates) 160 assert.Regexp(t, "^Done: test[12] test[12] \\(.* ago\\)", obj.Statuses.String()) 161 162 obj1 = obj 163 break 164 } 165 <-watch 166 } 167 168 // Make the second reconciler faulty. 169 ops2.faulty.Store(true) 170 171 // Mark the object pending again. Reuse the StatusSet. 172 wtxn = db.WriteTxn(table) 173 obj1 = obj1.Clone() 174 obj1.Statuses = obj1.Statuses.Pending() 175 assert.Regexp(t, "^Pending: test[12] test[12] \\(.* ago\\)", obj1.Statuses.String()) 176 table.Insert(wtxn, obj1) 177 wtxn.Commit() 178 179 // Wait for it to reconcile. 180 for { 181 obj, _, watch, found := table.GetWatch(db.ReadTxn(), multiStatusIndex.Query(1)) 182 if found && 183 obj.Statuses.Get("test1").Kind == reconciler.StatusKindDone && 184 obj.Statuses.Get("test2").Kind == reconciler.StatusKindError { 185 186 assert.Equal(t, 2, ops1.numUpdates) 187 assert.Equal(t, 2, ops2.numUpdates) 188 assert.Regexp(t, "^Errored: test2 \\(fail\\), Done: test1 \\(.* ago\\)", obj.Statuses.String()) 189 190 break 191 } 192 <-watch 193 } 194 195 require.NoError(t, hive.Stop(log, context.TODO()), "Stop") 196 }