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  }