diff --git a/README.md b/README.md index 89007d60..64900ffc 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,15 @@ create an equality condition (using `ovsdb.ConditionEqual`) on the `_uuid` field The table is inferred from the type that the function accepts as only argument. +**WhereCacheTyped()**: `WhereCacheTyped()` is a high-performance, type-safe alternative to `WhereCache()` that uses Go generics. It provides the same functionality as `WhereCache()` but with significantly better performance and zero memory allocations for predicate evaluation. This is the recommended approach for performance-critical applications. Example: + + lsList := []LogicalSwitch{} + client.WhereCacheTyped(ovs, func(ls *MyLogicalSwitch) bool { + return strings.HasPrefix(ls.Name, "ext_") + }).List(&lsList) + +The generic type parameter is automatically inferred from the predicate function signature, eliminating the need for runtime type assertions and reflection. This API is available for all operations that `WhereCache()` supports, including `List()`, `Update()`, `Delete()`, and `Mutate()`. + ### Client indexes The client will track schema indexes and use them when appropriate in `Get`, `Where`, and `WhereAll` as explained above. @@ -180,6 +189,17 @@ Search the cache for elements that match a certain predicate: fmt.Printf("%+v\n", ls) } +Search the cache using the high-performance typed API: + + var lsList *[]MyLogicalSwitch + client.WhereCacheTyped(ovs, func(ls *MyLogicalSwitch) bool { + return strings.HasPrefix(ls.Name, "ext_") + }).List(&lsList) + + for _, ls := range lsList { + fmt.Printf("%+v\n", ls) + } + Create a new element ops, _ := ovs.Create(&MyLogicalSwitch{ diff --git a/client/api_test.go b/client/api_test.go index 1ee2a48c..d40bf834 100644 --- a/client/api_test.go +++ b/client/api_test.go @@ -1807,6 +1807,110 @@ func BenchmarkAPIList(b *testing.B) { } } +func BenchmarkWhereCacheTypedList(b *testing.B) { + const numRows = 10000 + + lscacheList := make([]*testBridge, 0, numRows) + + for i := 0; i < numRows; i++ { + lscacheList = append(lscacheList, + &testBridge{ + UUID: uuid.New().String(), + Name: fmt.Sprintf("ls%d", i), + ExternalIDs: map[string]string{"foo": "bar"}, + }) + } + lscache := map[string]model.Model{} + for i := range lscacheList { + lscache[lscacheList[i].UUID] = lscacheList[i] + } + testData := cache.Data{ + "Bridge": lscache, + } + tcache := apiTestCache(b, testData) + ovs, err := newOVSDBClient(defDB) + require.NoError(b, err) + ovs.primaryDB().cache = tcache + ovs.primaryDB().api = newAPI(tcache, &discardLogger, false) + + r := rand.New(rand.NewSource(int64(b.N))) + var index int + + test := []struct { + name string + predicate any + predicateTyped func(*testBridge) bool + }{ + { + name: "predicate returns none", + predicate: func(_ *testBridge) bool { + return false + }, + }, + { + name: "predicate returns all", + predicate: func(_ *testBridge) bool { + return true + }, + }, + { + name: "predicate on an arbitrary condition", + predicate: func(t *testBridge) bool { + return strings.HasPrefix(t.Name, "ls1") + }, + }, + { + name: "predicate matches name", + predicate: func(t *testBridge) bool { + return t.Name == lscacheList[index].Name + }, + }, + // Typed versions for comparison + { + name: "typed: predicate returns none", + predicateTyped: func(_ *testBridge) bool { + return false + }, + }, + { + name: "typed: predicate returns all", + predicateTyped: func(_ *testBridge) bool { + return true + }, + }, + { + name: "typed: predicate on an arbitrary condition", + predicateTyped: func(t *testBridge) bool { + return strings.HasPrefix(t.Name, "ls1") + }, + }, + { + name: "typed: predicate matches name", + predicateTyped: func(t *testBridge) bool { + return t.Name == lscacheList[index].Name + }, + }, + } + + for _, tt := range test { + b.Run(tt.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + index = r.Intn(numRows) + var result []*testBridge + var cond ConditionalAPI + + if tt.predicateTyped != nil { + cond = WhereCacheTyped(ovs, tt.predicateTyped) + } else if tt.predicate != nil { + cond = ovs.WhereCache(tt.predicate) + } + err := cond.List(context.Background(), &result) + assert.NoError(b, err) + } + }) + } +} + func BenchmarkAPIListMultiple(b *testing.B) { const numRows = 500 diff --git a/client/client.go b/client/client.go index 9d05e773..7f22be97 100644 --- a/client/client.go +++ b/client/client.go @@ -64,6 +64,8 @@ type Client interface { MonitorCancel(ctx context.Context, cookie MonitorCookie) error NewMonitor(...MonitorOption) *Monitor CurrentEndpoint() string + Logger() *logr.Logger + Option() *options API // GetSelectResultsByIndex parses the result of the select operation indicated by // the 0-based index from the transaction results of the provided operations. @@ -1576,3 +1578,11 @@ func (o *ovsdbClient) GetSelectResultsByIndex(ops []ovsdb.Operation, results []o func (o *ovsdbClient) GetSelectResults(ops []ovsdb.Operation, results []ovsdb.OperationResult, target interface{}) error { return o.GetSelectResultsByIndex(ops, results, target, 0) } + +func (o *ovsdbClient) Logger() *logr.Logger { + return o.logger +} + +func (o *ovsdbClient) Option() *options { + return o.options +} diff --git a/client/condition.go b/client/condition.go index 2e217c78..fcb12518 100644 --- a/client/condition.go +++ b/client/condition.go @@ -223,6 +223,82 @@ func newPredicateConditional(table string, cache *cache.TableCache, predicate an }, nil } +// predicateConditionalTyped is a type-safe version using generics +type predicateConditionalTyped[T model.Model] struct { + tableName string + predicate func(T) bool + cache *cache.TableCache +} + +// Matches returns the models that match the predicate without reflection +func (c *predicateConditionalTyped[T]) Matches() (map[string]model.Model, error) { + tableCache := c.cache.Table(c.tableName) + if tableCache == nil { + return nil, ErrNotFound + } + found := map[string]model.Model{} + // run the predicate on a shallow copy of the models for speed and only + // clone the matches + for u, m := range tableCache.RowsShallow() { + // Type assertion instead of reflection - much faster + if typedModel, ok := m.(T); ok { + if c.predicate(typedModel) { + found[u] = model.Clone(m) + } + } + } + return found, nil +} + +func (c *predicateConditionalTyped[T]) Table() string { + return c.tableName +} + +// Generate returns a list of conditions that match, by _uuid equality, all the objects that +// match the predicate +func (c *predicateConditionalTyped[T]) Generate() ([][]ovsdb.Condition, error) { + models, err := c.Matches() + if err != nil { + return nil, err + } + return generateConditionsFromModels(c.cache.DatabaseModel(), models) +} + +// newPredicateConditionalTyped creates a type-safe predicate conditional +// This is the recommended high-performance alternative to newPredicateConditional +func newPredicateConditionalTyped[T model.Model](table string, cache *cache.TableCache, predicate func(T) bool) (Conditional, error) { + return &predicateConditionalTyped[T]{ + tableName: table, + predicate: predicate, + cache: cache, + }, nil +} + +// WhereCacheTyped creates a high-performance ConditionalAPI using generics +// Usage: WhereCacheTyped(client, func(m *MyModel) bool { return m.Field > value }) +func WhereCacheTyped[T model.Model](client Client, predicate func(T) bool) ConditionalAPI { + clientCache := client.Cache() + logger := client.Logger() + options := client.Option() + + // Get table name from the generic type T + var zero T + table := clientCache.DatabaseModel().FindTable(reflect.TypeOf(zero)) + if table == "" { + // Return an error conditional if table not found + errorCond := newErrorConditional(fmt.Errorf("model %T not found in Database Model", zero)) + return newConditionalAPI(clientCache, errorCond, logger, options.validateModel) + } + + condition, err := newPredicateConditionalTyped(table, clientCache, predicate) + if err != nil { + errorCond := newErrorConditional(err) + return newConditionalAPI(clientCache, errorCond, logger, options.validateModel) + } + + return newConditionalAPI(clientCache, condition, logger, options.validateModel) +} + // errorConditional is a conditional that encapsulates an error // It is used to delay the reporting of errors from conditional creation to API method call type errorConditional struct { diff --git a/test/ovs/ovs_integration_test.go b/test/ovs/ovs_integration_test.go index 347693bf..1b76b8bf 100644 --- a/test/ovs/ovs_integration_test.go +++ b/test/ovs/ovs_integration_test.go @@ -2030,3 +2030,19 @@ func (suite *OVSIntegrationSuite) TestSelectIntegrity() { // Cleanup is handled by TearDownTest } +func (suite *OVSIntegrationSuite) TestWhereCache() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + var ovs []*ovsType + err := suite.clientWithoutInactvityCheck.WhereCache(func(*ovsType) bool { return true }).List(ctx, &ovs) + suite.Require().NoError(err) + suite.Len(ovs, 1) + + var ovs1 []*ovsType + err = client.WhereCacheTyped(suite.clientWithoutInactvityCheck, func(*ovsType) bool { return true }).List(ctx, &ovs1) + suite.Require().NoError(err) + suite.Len(ovs1, 1) + + suite.Equal(ovs[0].UUID, ovs1[0].UUID) +}