This is an automated email from the ASF dual-hosted git repository.
robocanic pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-admin.git
The following commit(s) were added to refs/heads/develop by this push:
new b809b14c feat: extend indexer with prefix matching and db persistence
(#1422)
b809b14c is described below
commit b809b14cea8c163c5ec83bb387b132f7b561e522
Author: ThunGuo <[email protected]>
AuthorDate: Sun Mar 29 19:27:19 2026 +0800
feat: extend indexer with prefix matching and db persistence (#1422)
* feat: extend indexer with prefix matching and db persistence
* refactor: remove in-memory index from GormStore and add operator field
* fix: copilot review suggestion and service panic
* update
---
go.mod | 1 +
go.sum | 1 +
pkg/console/service/application.go | 51 +--
pkg/console/service/condition_rule.go | 4 +-
pkg/console/service/configurator_rule.go | 4 +-
pkg/console/service/instance.go | 28 +-
pkg/console/service/service.go | 32 +-
pkg/console/service/service_provider_instances.go | 6 +-
pkg/console/service/tag_rule.go | 4 +-
pkg/core/bootstrap/init.go | 1 +
pkg/core/discovery/subscriber/instance.go | 6 +-
pkg/core/discovery/subscriber/nacos_service.go | 24 +-
pkg/core/discovery/subscriber/rpc_instance.go | 4 +-
pkg/core/engine/subscriber/runtime_instance.go | 8 +-
pkg/core/manager/manager.go | 21 +-
pkg/core/manager/manager_helper.go | 35 +-
pkg/core/store/index/condition.go | 38 ++
pkg/core/store/store.go | 9 +-
pkg/governor/mock/factory.go | 50 +++
pkg/store/dbcommon/gorm_store.go | 383 +++++++++++++------
pkg/store/dbcommon/gorm_store_test.go | 377 +++++++++++++++----
pkg/store/dbcommon/index.go | 254 -------------
pkg/store/dbcommon/model.go | 19 +
pkg/store/memory/store.go | 185 ++++++++-
pkg/store/memory/store_test.go | 433 +++++++++++++++++++++-
25 files changed, 1391 insertions(+), 587 deletions(-)
diff --git a/go.mod b/go.mod
index 7548c170..c8d33c25 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ toolchain go1.24.11
require (
dubbo.apache.org/dubbo-go/v3 v3.0.0-20260210015753-35ea886421f9
github.com/apache/dubbo-go-hessian2 v1.12.5
+ github.com/armon/go-radix v1.0.0
github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
github.com/duke-git/lancet/v2 v2.3.6
github.com/envoyproxy/go-control-plane/envoy v1.32.4
diff --git a/go.sum b/go.sum
index b288f3a0..414edca8 100644
--- a/go.sum
+++ b/go.sum
@@ -151,6 +151,7 @@ github.com/apolloconfig/agollo/v4 v4.4.0/go.mod
h1:6WjI68IzqMk/Y6ghMtrj5AX6Uewo2
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod
h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod
h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod
h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0
h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod
h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod
h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod
h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
diff --git a/pkg/console/service/application.go
b/pkg/console/service/application.go
index 80e7ee68..cd295e2b 100644
--- a/pkg/console/service/application.go
+++ b/pkg/console/service/application.go
@@ -42,9 +42,9 @@ func GetApplicationDetail(ctx consolectx.Context, req
*model.ApplicationDetailRe
instanceResources, err :=
manager.ListByIndexes[*meshresource.InstanceResource](
ctx.ResourceManager(),
meshresource.InstanceKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByInstanceAppNameIndex: req.AppName,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByInstanceAppNameIndex, Value:
req.AppName, Operator: index.Equals},
},
)
if err != nil {
@@ -68,9 +68,9 @@ func GetAppInstanceInfo(ctx consolectx.Context, req
*model.ApplicationTabInstanc
pageData, err :=
manager.PageListByIndexes[*meshresource.InstanceResource](
ctx.ResourceManager(),
meshresource.InstanceKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByInstanceAppNameIndex: req.AppName,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByInstanceAppNameIndex, Value:
req.AppName, Operator: index.Equals},
},
req.PageReq,
)
@@ -120,23 +120,28 @@ func GetAppServiceInfo(ctx consolectx.Context, req
*model.ApplicationServiceForm
}
func getAppProvideServiceInfo(ctx consolectx.Context, req
*model.ApplicationServiceFormReq) (*model.SearchPaginationResult, error) {
- var indexes map[string]string
+ var conditions []index.IndexCondition
+ conditions = append(conditions, index.IndexCondition{
+ IndexName: index.ByMeshIndex,
+ Value: req.Mesh,
+ Operator: index.Equals,
+ })
+ conditions = append(conditions, index.IndexCondition{
+ IndexName: index.ByServiceProviderAppName,
+ Value: req.AppName,
+ Operator: index.Equals,
+ })
if strutil.IsNotBlank(req.ServiceName) {
- indexes = map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByServiceProviderAppName: req.AppName,
- index.ByServiceProviderServiceName: req.ServiceName,
- }
- } else {
- indexes = map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByServiceProviderAppName: req.AppName,
- }
+ conditions = append(conditions, index.IndexCondition{
+ IndexName: index.ByServiceProviderServiceName,
+ Value: req.ServiceName,
+ Operator: index.Equals,
+ })
}
pageData, err :=
manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource](
ctx.ResourceManager(),
meshresource.ServiceProviderMetadataKind,
- indexes,
+ conditions,
req.PageReq,
)
if err != nil {
@@ -167,9 +172,9 @@ func getAppConsumeServiceInfo(ctx consolectx.Context, req
*model.ApplicationServ
pageData, err :=
manager.PageListByIndexes[*meshresource.ServiceConsumerMetadataResource](
ctx.ResourceManager(),
meshresource.ServiceConsumerMetadataKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByServiceConsumerAppName: req.AppName,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByServiceConsumerAppName, Value:
req.AppName, Operator: index.Equals},
},
req.PageReq,
)
@@ -220,8 +225,8 @@ func SearchApplications(ctx consolectx.Context, req
*model.ApplicationSearchReq)
pageData, err :=
manager.PageListByIndexes[*meshresource.ApplicationResource](
ctx.ResourceManager(),
meshresource.ApplicationKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
},
req.PageReq,
)
diff --git a/pkg/console/service/condition_rule.go
b/pkg/console/service/condition_rule.go
index de0ebfc8..9fae1594 100644
--- a/pkg/console/service/condition_rule.go
+++ b/pkg/console/service/condition_rule.go
@@ -40,8 +40,8 @@ func SearchConditionRules(ctx context.Context, req
*model.SearchConditionRuleReq
pageData, err :=
manager.PageListByIndexes[*meshresource.ConditionRouteResource](
ctx.ResourceManager(),
meshresource.ConditionRouteKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
},
req.PageReq)
if err != nil {
diff --git a/pkg/console/service/configurator_rule.go
b/pkg/console/service/configurator_rule.go
index 16aa59e4..13dd2284 100644
--- a/pkg/console/service/configurator_rule.go
+++ b/pkg/console/service/configurator_rule.go
@@ -36,8 +36,8 @@ func PageListConfiguratorRule(ctx consolectx.Context, req
*model.SearchReq) (*mo
pageData, err :=
manager.PageListByIndexes[*meshresource.DynamicConfigResource](
ctx.ResourceManager(),
meshresource.DynamicConfigKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
},
req.PageReq)
if err != nil {
diff --git a/pkg/console/service/instance.go b/pkg/console/service/instance.go
index ed0f822d..85a04eb2 100644
--- a/pkg/console/service/instance.go
+++ b/pkg/console/service/instance.go
@@ -44,9 +44,9 @@ func SearchInstanceByIp(ctx consolectx.Context, req
*model.SearchReq) (*model.Se
pageData, err :=
manager.PageListByIndexes[*meshresource.InstanceResource](
ctx.ResourceManager(),
meshresource.InstanceKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByInstanceIpIndex: req.Keywords,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByInstanceIpIndex, Value:
req.Keywords, Operator: index.HasPrefix},
},
req.PageReq)
if err != nil {
@@ -54,7 +54,7 @@ func SearchInstanceByIp(ctx consolectx.Context, req
*model.SearchReq) (*model.Se
}
if pageData.Data == nil || len(pageData.Data) == 0 {
return &model.SearchPaginationResult{
- List:
[]*meshresource.ServiceProviderMetadataResourceList{},
+ List: []*model.SearchInstanceResp{},
PageInfo: coremodel.Pagination{
Total: 0,
PageSize: req.PageReq.PageSize,
@@ -63,8 +63,8 @@ func SearchInstanceByIp(ctx consolectx.Context, req
*model.SearchReq) (*model.Se
}, nil
}
return &model.SearchPaginationResult{
- List: slice.Map(pageData.Data, func(_ int, item
*meshresource.InstanceResource) *model.AppInstanceInfoResp {
- return buildAppInstanceInfoResp(item, ctx.Config())
+ List: slice.Map(pageData.Data, func(_ int, item
*meshresource.InstanceResource) *model.SearchInstanceResp {
+ return
model.NewSearchInstanceResp().FromInstanceResource(item, ctx.Config())
}),
PageInfo: pageData.Pagination,
}, nil
@@ -75,9 +75,9 @@ func SearchInstanceByName(ctx consolectx.Context, req
*model.SearchReq) (*model.
pageData, err :=
manager.PageListByIndexes[*meshresource.InstanceResource](
ctx.ResourceManager(),
meshresource.InstanceKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByInstanceNameIndex: req.Keywords,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByInstanceNameIndex, Value:
req.Keywords, Operator: index.HasPrefix},
},
req.PageReq)
if err != nil {
@@ -85,7 +85,7 @@ func SearchInstanceByName(ctx consolectx.Context, req
*model.SearchReq) (*model.
}
if pageData.Data == nil || len(pageData.Data) == 0 {
return &model.SearchPaginationResult{
- List:
[]*meshresource.ServiceProviderMetadataResourceList{},
+ List: []*model.SearchInstanceResp{},
PageInfo: coremodel.Pagination{
Total: 0,
PageSize: req.PageReq.PageSize,
@@ -94,8 +94,8 @@ func SearchInstanceByName(ctx consolectx.Context, req
*model.SearchReq) (*model.
}, nil
}
return &model.SearchPaginationResult{
- List: slice.Map(pageData.Data, func(_ int, item
*meshresource.InstanceResource) *model.AppInstanceInfoResp {
- return buildAppInstanceInfoResp(item, ctx.Config())
+ List: slice.Map(pageData.Data, func(_ int, item
*meshresource.InstanceResource) *model.SearchInstanceResp {
+ return
model.NewSearchInstanceResp().FromInstanceResource(item, ctx.Config())
}),
PageInfo: pageData.Pagination,
}, nil
@@ -113,8 +113,8 @@ func SearchInstances(ctx consolectx.Context, req
*model.SearchInstanceReq) (*mod
pageData, err :=
manager.PageListByIndexes[*meshresource.InstanceResource](
ctx.ResourceManager(),
meshresource.InstanceKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
},
req.PageReq)
if err != nil {
diff --git a/pkg/console/service/service.go b/pkg/console/service/service.go
index 9217a606..0178fe2c 100644
--- a/pkg/console/service/service.go
+++ b/pkg/console/service/service.go
@@ -41,17 +41,21 @@ import (
// GetServiceTabDistribution get service distribution
func GetServiceTabDistribution(ctx consolectx.Context, req
*model.ServiceTabDistributionReq) (*model.SearchPaginationResult, error) {
- indexes := map[string]string{
- index.ByServiceConsumerServiceName: req.ServiceName,
+ conditions := []index.IndexCondition{
+ {IndexName: index.ByServiceConsumerServiceName, Value:
req.ServiceName, Operator: index.Equals},
}
// for now, only support accurate name match
if strutil.IsNotBlank(req.Keywords) {
- indexes[index.ByServiceConsumerAppName] = req.Keywords
+ conditions = append(conditions, index.IndexCondition{
+ IndexName: index.ByServiceConsumerAppName,
+ Value: req.Keywords,
+ Operator: index.Equals,
+ })
}
pageData, err :=
manager.PageListByIndexes[*meshresource.ServiceConsumerMetadataResource](
ctx.ResourceManager(),
meshresource.ServiceConsumerMetadataKind,
- indexes,
+ conditions,
req.PageReq)
if err != nil {
logger.Errorf("get service consumer %s failed, cause: %v",
req.ServiceName, err)
@@ -98,8 +102,8 @@ func SearchServices(ctx consolectx.Context, req
*model.ServiceSearchReq) (*model
pageData, err :=
manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource](
ctx.ResourceManager(),
meshresource.ServiceProviderMetadataKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
},
req.PageReq,
)
@@ -120,14 +124,14 @@ func SearchServices(ctx consolectx.Context, req
*model.ServiceSearchReq) (*model
}, nil
}
-// SearchServicesByKeywords search services by keywords, for now only support
accurate search
+// SearchServicesByKeywords search services by keywords with prefix matching
func SearchServicesByKeywords(ctx consolectx.Context, req
*model.ServiceSearchReq) (*model.SearchPaginationResult, error) {
pageData, err :=
manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource](
ctx.ResourceManager(),
meshresource.ServiceProviderMetadataKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByServiceProviderServiceName: req.Keywords,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByServiceProviderServiceName, Value:
req.Keywords, Operator: index.HasPrefix},
},
req.PageReq,
)
@@ -190,10 +194,10 @@ func GetServiceMethodDetail(ctx consolectx.Context, req
model.ServiceMethodDetai
}
// providerIndexes defines the canonical indexes for provider metadata
-func providerIndexes(req model.BaseServiceReq) map[string]string {
- return map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByServiceProviderServiceKey: req.ServiceKey(),
+func providerIndexes(req model.BaseServiceReq) []index.IndexCondition {
+ return []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator:
index.Equals},
+ {IndexName: index.ByServiceProviderServiceKey, Value:
req.ServiceKey(), Operator: index.Equals},
}
}
diff --git a/pkg/console/service/service_provider_instances.go
b/pkg/console/service/service_provider_instances.go
index 934c21fc..3ea90e81 100644
--- a/pkg/console/service/service_provider_instances.go
+++ b/pkg/console/service/service_provider_instances.go
@@ -50,9 +50,9 @@ func GetServiceProviderInstances(ctx consolectx.Context, req
model.BaseServiceRe
instanceList, err :=
manager.ListByIndexes[*meshresource.InstanceResource](
ctx.ResourceManager(),
meshresource.InstanceKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
- index.ByInstanceAppNameIndex: providerAppName,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByInstanceAppNameIndex,
Value: providerAppName, Operator: index.Equals},
},
)
if err != nil {
diff --git a/pkg/console/service/tag_rule.go b/pkg/console/service/tag_rule.go
index 972cbf10..a051117c 100644
--- a/pkg/console/service/tag_rule.go
+++ b/pkg/console/service/tag_rule.go
@@ -36,8 +36,8 @@ func PageListTagRule(ctx consolectx.Context, req
*model.SearchReq) (*model.Searc
pageData, err :=
manager.PageListByIndexes[*meshresource.TagRouteResource](
ctx.ResourceManager(),
meshresource.TagRouteKind,
- map[string]string{
- index.ByMeshIndex: req.Mesh,
+ []index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: req.Mesh,
Operator: index.Equals},
},
req.PageReq)
if err != nil {
diff --git a/pkg/core/bootstrap/init.go b/pkg/core/bootstrap/init.go
index 21bf1133..c590b84d 100644
--- a/pkg/core/bootstrap/init.go
+++ b/pkg/core/bootstrap/init.go
@@ -32,6 +32,7 @@ import (
_ "github.com/apache/dubbo-admin/pkg/discovery/zk"
_ "github.com/apache/dubbo-admin/pkg/engine/kubernetes"
_ "github.com/apache/dubbo-admin/pkg/engine/mock"
+ _ "github.com/apache/dubbo-admin/pkg/governor/mock"
_ "github.com/apache/dubbo-admin/pkg/governor/nacos2"
_ "github.com/apache/dubbo-admin/pkg/governor/zk"
_ "github.com/apache/dubbo-admin/pkg/store/memory"
diff --git a/pkg/core/discovery/subscriber/instance.go
b/pkg/core/discovery/subscriber/instance.go
index ded8a67d..bb60fb8e 100644
--- a/pkg/core/discovery/subscriber/instance.go
+++ b/pkg/core/discovery/subscriber/instance.go
@@ -70,9 +70,9 @@ func (s *InstanceEventSubscriber) ProcessEvent(event
events.Event) error {
} else {
instanceRes = oldObj
}
- instanceResList, err := s.instanceStore.ListByIndexes(map[string]string{
- index.ByMeshIndex: instanceRes.Mesh,
- index.ByInstanceAppNameIndex: instanceRes.Spec.AppName,
+ instanceResList, err :=
s.instanceStore.ListByIndexes([]index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: instanceRes.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByInstanceAppNameIndex, Value:
instanceRes.Spec.AppName, Operator: index.Equals},
})
appResKey := coremodel.BuildResourceKey(instanceRes.Mesh,
instanceRes.Spec.AppName)
diff --git a/pkg/core/discovery/subscriber/nacos_service.go
b/pkg/core/discovery/subscriber/nacos_service.go
index e5c9c631..07c23f10 100644
--- a/pkg/core/discovery/subscriber/nacos_service.go
+++ b/pkg/core/discovery/subscriber/nacos_service.go
@@ -127,9 +127,9 @@ func (n *NacosServiceEventSubscriber)
processConsumerMetadataUpsert(serviceRes *
logger.Errorf("process service consumer metadata upsert event,
but cannot route to service consumer metadata resource, cause: %v", err)
return err
}
- resources, err := st.ListByIndexes(map[string]string{
- index.ByMeshIndex: serviceRes.Mesh,
- index.ByServiceConsumerServiceName: serviceName,
+ resources, err := st.ListByIndexes([]index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByServiceConsumerServiceName, Value:
serviceName, Operator: index.Equals},
})
if err != nil {
logger.Errorf("process service consumer metadata upsert event,
but cannot list service consumer metadata resource of %s, cause: %v",
serviceRes.Name, err)
@@ -212,9 +212,9 @@ func (n *NacosServiceEventSubscriber)
processRPCInstanceUpsert(serviceRes *meshr
logger.Errorf("process rpc instance upsert event, but cannot
route to rpc instance resource, cause: %v", err)
return err
}
- resources, err := st.ListByIndexes(map[string]string{
- index.ByMeshIndex: serviceRes.Mesh,
- index.ByRPCInstanceAppName: serviceRes.Name,
+ resources, err := st.ListByIndexes([]index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByRPCInstanceAppName, Value: serviceRes.Name,
Operator: index.Equals},
})
if err != nil {
logger.Errorf("process rpc instance upsert event, but cannot
list rpc instance resource of %s, cause: %v", serviceRes.Name, err)
@@ -287,9 +287,9 @@ func (n *NacosServiceEventSubscriber)
processServiceConsumerDelete(serviceRes *m
logger.Errorf("process service consumer delete event, but
cannot route to service consumer metadata resource, cause: %v", err)
return err
}
- resources, err := st.ListByIndexes(map[string]string{
- index.ByMeshIndex: serviceRes.Mesh,
- index.ByServiceConsumerServiceName: serviceRes.Name,
+ resources, err := st.ListByIndexes([]index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByServiceConsumerServiceName, Value:
serviceRes.Name, Operator: index.Equals},
})
if err != nil {
logger.Errorf("process service consumer delete event, but
cannot list service consumer metadata resource of %s, cause: %v",
serviceRes.Name, err)
@@ -311,9 +311,9 @@ func (n *NacosServiceEventSubscriber)
processRPCInstanceDelete(serviceRes *meshr
logger.Errorf("process rpc instance delete event, but cannot
route to rpc instance resource, cause: %v", err)
return err
}
- resources, err := st.ListByIndexes(map[string]string{
- index.ByMeshIndex: serviceRes.Mesh,
- index.ByRPCInstanceAppName: serviceRes.Name,
+ resources, err := st.ListByIndexes([]index.IndexCondition{
+ {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh,
Operator: index.Equals},
+ {IndexName: index.ByRPCInstanceAppName, Value: serviceRes.Name,
Operator: index.Equals},
})
if err != nil {
logger.Errorf("process rpc instance delete event, but cannot
list rpc instance resource of %s, cause: %v", serviceRes.Name, err)
diff --git a/pkg/core/discovery/subscriber/rpc_instance.go
b/pkg/core/discovery/subscriber/rpc_instance.go
index e970239d..0343e5cf 100644
--- a/pkg/core/discovery/subscriber/rpc_instance.go
+++ b/pkg/core/discovery/subscriber/rpc_instance.go
@@ -185,8 +185,8 @@ func (s *RPCInstanceEventSubscriber)
findRelatedRuntimeInstanceAndMerge(instance
}
func (s *RPCInstanceEventSubscriber) getRuntimeInstanceByIp(ip string)
*meshresource.RuntimeInstanceResource {
- resources, err := s.rtInstanceStore.ListByIndexes(map[string]string{
- index.ByRuntimeInstanceIPIndex: ip,
+ resources, err :=
s.rtInstanceStore.ListByIndexes([]index.IndexCondition{
+ {IndexName: index.ByRuntimeInstanceIPIndex, Value: ip,
Operator: index.Equals},
})
if err != nil {
logger.Errorf("list runtime instance by ip index failed, ip:
%s, err: %s", ip, err.Error())
diff --git a/pkg/core/engine/subscriber/runtime_instance.go
b/pkg/core/engine/subscriber/runtime_instance.go
index 6038462c..f7d38709 100644
--- a/pkg/core/engine/subscriber/runtime_instance.go
+++ b/pkg/core/engine/subscriber/runtime_instance.go
@@ -161,8 +161,8 @@ func (s *RuntimeInstanceEventSubscriber)
getRelatedInstanceByName(
return nil, nil
}
instanceResName :=
meshresource.BuildInstanceResName(rtInstanceRes.Spec.AppName,
rtInstanceRes.Spec.Ip, rtInstanceRes.Spec.RpcPort)
- resources, err := s.instanceStore.ListByIndexes(map[string]string{
- index.ByInstanceNameIndex: instanceResName,
+ resources, err := s.instanceStore.ListByIndexes([]index.IndexCondition{
+ {IndexName: index.ByInstanceNameIndex, Value: instanceResName,
Operator: index.Equals},
})
if err != nil {
return nil, err
@@ -190,8 +190,8 @@ func (s *RuntimeInstanceEventSubscriber)
getRelatedInstanceByName(
func (s *RuntimeInstanceEventSubscriber) getRelatedInstanceByIP(
rtInstanceRes *meshresource.RuntimeInstanceResource)
(*meshresource.InstanceResource, error) {
- resources, err := s.instanceStore.ListByIndexes(map[string]string{
- index.ByInstanceIpIndex: rtInstanceRes.Spec.Ip,
+ resources, err := s.instanceStore.ListByIndexes([]index.IndexCondition{
+ {IndexName: index.ByInstanceIpIndex, Value:
rtInstanceRes.Spec.Ip, Operator: index.Equals},
})
if err != nil {
return nil, err
diff --git a/pkg/core/manager/manager.go b/pkg/core/manager/manager.go
index 44861ca7..3e4ce094 100644
--- a/pkg/core/manager/manager.go
+++ b/pkg/core/manager/manager.go
@@ -25,6 +25,7 @@ import (
"github.com/apache/dubbo-admin/pkg/core/governor"
"github.com/apache/dubbo-admin/pkg/core/resource/model"
"github.com/apache/dubbo-admin/pkg/core/store"
+ "github.com/apache/dubbo-admin/pkg/core/store/index"
)
type ReadOnlyResourceManager interface {
@@ -32,13 +33,10 @@ type ReadOnlyResourceManager interface {
GetByKey(rk model.ResourceKind, key string) (r model.Resource, exist
bool, err error)
// GetByKeys returns the resources with the given resource keys
GetByKeys(rk model.ResourceKind, keys []string) ([]model.Resource,
error)
- // ListByIndexes returns the resources with the given indexes, indexes
is a map of index name and index value
- ListByIndexes(rk model.ResourceKind, indexes map[string]string)
([]model.Resource, error)
- // PageListByIndexes page list the resources with the given indexes,
indexes is a map of index name and index value
- PageListByIndexes(rk model.ResourceKind, indexes map[string]string, pr
model.PageReq) (*model.PageData[model.Resource], error)
- // PageSearchResourceByConditions page fuzzy search resource by
conditions, conditions cannot be empty
- // TODO support multiple conditions
- PageSearchResourceByConditions(rk model.ResourceKind, conditions
[]string, pr model.PageReq) (*model.PageData[model.Resource], error)
+ // ListByIndexes returns the resources with the given index conditions
+ ListByIndexes(rk model.ResourceKind, indexes []index.IndexCondition)
([]model.Resource, error)
+ // PageListByIndexes page list the resources with the given index
conditions
+ PageListByIndexes(rk model.ResourceKind, indexes
[]index.IndexCondition, pr model.PageReq) (*model.PageData[model.Resource],
error)
}
type WriteOnlyResourceManager interface {
@@ -100,7 +98,7 @@ func (rm *resourcesManager) GetByKeys(rk model.ResourceKind,
keys []string) ([]m
return resources, nil
}
-func (rm *resourcesManager) ListByIndexes(rk model.ResourceKind, indexes
map[string]string) ([]model.Resource, error) {
+func (rm *resourcesManager) ListByIndexes(rk model.ResourceKind, indexes
[]index.IndexCondition) ([]model.Resource, error) {
rs, err := rm.storeRouter.ResourceKindRoute(rk)
if err != nil {
return nil, err
@@ -114,7 +112,7 @@ func (rm *resourcesManager) ListByIndexes(rk
model.ResourceKind, indexes map[str
func (rm *resourcesManager) PageListByIndexes(
rk model.ResourceKind,
- indexes map[string]string,
+ indexes []index.IndexCondition,
pr model.PageReq) (*model.PageData[model.Resource], error) {
rs, err := rm.storeRouter.ResourceKindRoute(rk)
@@ -128,11 +126,6 @@ func (rm *resourcesManager) PageListByIndexes(
return pageData, nil
}
-func (rm *resourcesManager) PageSearchResourceByConditions(rk
model.ResourceKind, conditions []string, pr model.PageReq)
(*model.PageData[model.Resource], error) {
- //TODO implement me
- panic("implement me")
-}
-
func (rm *resourcesManager) Add(r model.Resource) error {
if !governor.RuleResourceKinds.Contain(r.ResourceKind()) {
return bizerror.New(bizerror.InvalidArgument, "invalid resource
kind")
diff --git a/pkg/core/manager/manager_helper.go
b/pkg/core/manager/manager_helper.go
index 80e40ba8..48a33363 100644
--- a/pkg/core/manager/manager_helper.go
+++ b/pkg/core/manager/manager_helper.go
@@ -22,6 +22,7 @@ import (
"github.com/apache/dubbo-admin/pkg/common/bizerror"
"github.com/apache/dubbo-admin/pkg/core/resource/model"
+ "github.com/apache/dubbo-admin/pkg/core/store/index"
)
// GetByKey is a helper function of ResourceManager.GeyByKey
@@ -59,7 +60,7 @@ func GetByKeys[T model.Resource](rm ReadOnlyResourceManager,
rk model.ResourceKi
}
// ListByIndexes is a helper function of ResourceManager.ListByIndexes
-func ListByIndexes[T model.Resource](rm ReadOnlyResourceManager, rk
model.ResourceKind, indexes map[string]string) ([]T, error) {
+func ListByIndexes[T model.Resource](rm ReadOnlyResourceManager, rk
model.ResourceKind, indexes []index.IndexCondition) ([]T, error) {
resources, err := rm.ListByIndexes(rk, indexes)
if err != nil {
return nil, err
@@ -81,7 +82,7 @@ func ListByIndexes[T model.Resource](rm
ReadOnlyResourceManager, rk model.Resour
func PageListByIndexes[T model.Resource](
rm ReadOnlyResourceManager,
rk model.ResourceKind,
- indexes map[string]string,
+ indexes []index.IndexCondition,
pr model.PageReq) (*model.PageData[T], error) {
pageData, err := rm.PageListByIndexes(rk, indexes, pr)
@@ -107,33 +108,3 @@ func PageListByIndexes[T model.Resource](
}
return newPageData, nil
}
-
-// PageSearchResourceByConditions is a helper function of
ResourceManager.PageSearchResourceByConditions
-func PageSearchResourceByConditions[T model.Resource](
- rm ReadOnlyResourceManager,
- rk model.ResourceKind,
- conditions []string,
- pr model.PageReq) (*model.PageData[T], error) {
- pageData, err := rm.PageSearchResourceByConditions(rk, conditions, pr)
- if err != nil {
- return nil, err
- }
-
- typedResources := make([]T, len(pageData.Data))
- for i, resource := range pageData.Data {
- typedResource, ok := resource.(T)
- if !ok {
- return nil, bizerror.NewAssertionError(rk,
reflect.TypeOf(typedResource).Name())
- }
- typedResources[i] = typedResource
- }
- newPageData := &model.PageData[T]{
- Pagination: model.Pagination{
- Total: pageData.Total,
- PageOffset: pageData.PageOffset,
- PageSize: pageData.PageSize,
- },
- Data: typedResources,
- }
- return newPageData, nil
-}
diff --git a/pkg/core/store/index/condition.go
b/pkg/core/store/index/condition.go
new file mode 100644
index 00000000..4bdcee2f
--- /dev/null
+++ b/pkg/core/store/index/condition.go
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package index
+
+// IndexOperator defines the comparison operator for index queries
+type IndexOperator string
+
+const (
+ // Equals performs exact match on the index value
+ Equals IndexOperator = "Equals"
+ // HasPrefix performs prefix match on the index value
+ HasPrefix IndexOperator = "HasPrefix"
+)
+
+// IndexCondition represents a single index query condition
+type IndexCondition struct {
+ // IndexName is the name of the index to query
+ IndexName string
+ // Value is the value to match against the index
+ Value string
+ // Operator is the comparison operator to use (Equals, HasPrefix, etc.)
+ Operator IndexOperator
+}
diff --git a/pkg/core/store/store.go b/pkg/core/store/store.go
index 15d2117e..8923f657 100644
--- a/pkg/core/store/store.go
+++ b/pkg/core/store/store.go
@@ -27,6 +27,7 @@ import (
"github.com/apache/dubbo-admin/pkg/core/resource/model"
"github.com/apache/dubbo-admin/pkg/core/runtime"
+ "github.com/apache/dubbo-admin/pkg/core/store/index"
)
// ResourceStore defines the interface for the persistance of a resource
@@ -36,10 +37,10 @@ type ResourceStore interface {
// GetByKeys get resources by keys, return list of resource.
// if a resource of specified key doesn't exist in the store, resource
list will not include it
GetByKeys(keys []string) ([]model.Resource, error)
- // ListByIndexes list resources by indexes, indexes is map of index
name and index value
- ListByIndexes(indexes map[string]string) ([]model.Resource, error)
- // PageListByIndexes list resources by indexes pageable, indexes is map
of index name and index value
- PageListByIndexes(indexes map[string]string, pq model.PageReq)
(*model.PageData[model.Resource], error)
+ // ListByIndexes list resources by index conditions
+ ListByIndexes(indexes []index.IndexCondition) ([]model.Resource, error)
+ // PageListByIndexes list resources by index conditions pageable
+ PageListByIndexes(indexes []index.IndexCondition, pq model.PageReq)
(*model.PageData[model.Resource], error)
}
// ManagedResourceStore includes both functional interfaces and lifecycle
interfaces
diff --git a/pkg/governor/mock/factory.go b/pkg/governor/mock/factory.go
new file mode 100644
index 00000000..4c7b282d
--- /dev/null
+++ b/pkg/governor/mock/factory.go
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mock
+
+import (
+ discoverycfg "github.com/apache/dubbo-admin/pkg/config/discovery"
+ "github.com/apache/dubbo-admin/pkg/core/events"
+ "github.com/apache/dubbo-admin/pkg/core/governor"
+ coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
+ "github.com/apache/dubbo-admin/pkg/core/store"
+)
+
+func init() {
+ governor.RegisterFactory(&mockGovernorFactory{})
+}
+
+type mockGovernorFactory struct{}
+
+var _ governor.Factory = &mockGovernorFactory{}
+
+func (f *mockGovernorFactory) Support(t discoverycfg.Type) bool {
+ return t == discoverycfg.Mock
+}
+
+func (f *mockGovernorFactory) New(_ string, _ *discoverycfg.Config, _
store.Router, _ events.Emitter) (governor.RuleGovernor, error) {
+ return &mockGovernor{}, nil
+}
+
+type mockGovernor struct{}
+
+var _ governor.RuleGovernor = &mockGovernor{}
+
+func (g *mockGovernor) CreateRule(_ coremodel.Resource) error { return nil }
+func (g *mockGovernor) UpdateRule(_ coremodel.Resource) error { return nil }
+func (g *mockGovernor) DeleteRule(_ coremodel.Resource) error { return nil }
diff --git a/pkg/store/dbcommon/gorm_store.go b/pkg/store/dbcommon/gorm_store.go
index f72b4376..cedc2014 100644
--- a/pkg/store/dbcommon/gorm_store.go
+++ b/pkg/store/dbcommon/gorm_store.go
@@ -22,6 +22,7 @@ import (
"fmt"
"reflect"
"sort"
+ "sync"
"gorm.io/gorm"
"k8s.io/client-go/tools/cache"
@@ -35,14 +36,15 @@ import (
)
// GormStore is a GORM-backed store implementation for Dubbo resources
-// It uses GORM for database operations and maintains in-memory indices for
fast lookups
+// It uses GORM for database operations and persists all indices to the
resource_indices table
// This implementation is database-agnostic and works with any GORM-supported
database
type GormStore struct {
- pool *ConnectionPool // Shared connection pool with reference
counting
- kind model.ResourceKind
- address string
- indices *Index // In-memory index with thread-safe operations
- stopCh chan struct{}
+ pool *ConnectionPool // Shared connection pool with reference
counting
+ kind model.ResourceKind
+ address string
+ indexers cache.Indexers // Index functions for creating indices
+ mu sync.RWMutex // Protects indexers
+ stopCh chan struct{}
}
var _ store.ManagedResourceStore = &GormStore{}
@@ -50,15 +52,15 @@ var _ store.ManagedResourceStore = &GormStore{}
// NewGormStore creates a new GORM store for the specified resource kind
func NewGormStore(kind model.ResourceKind, address string, pool
*ConnectionPool) *GormStore {
return &GormStore{
- kind: kind,
- address: address,
- pool: pool,
- indices: NewIndex(),
- stopCh: make(chan struct{}),
+ kind: kind,
+ address: address,
+ pool: pool,
+ indexers: make(cache.Indexers),
+ stopCh: make(chan struct{}),
}
}
-// Init initializes the GORM store by migrating the schema and rebuilding
indices
+// Init initializes the GORM store by migrating the schema and registering
indexers
func (gs *GormStore) Init(_ runtime.BuilderContext) error {
// Perform table migration
db := gs.pool.GetDB()
@@ -66,15 +68,22 @@ func (gs *GormStore) Init(_ runtime.BuilderContext) error {
if err :=
db.Scopes(TableScope(gs.kind.ToString())).AutoMigrate(&ResourceModel{}); err !=
nil {
return fmt.Errorf("failed to migrate schema for %s: %w",
gs.kind.ToString(), err)
}
+
+ // Migrate resource_indices table (shared across all resource kinds)
+ if err := db.AutoMigrate(&ResourceIndexModel{}); err != nil {
+ return fmt.Errorf("failed to migrate resource_indices: %w", err)
+ }
+
// Register indexers for the resource kind
indexers := index.IndexersRegistry().Indexers(gs.kind)
if err := gs.AddIndexers(indexers); err != nil {
return err
}
- // Rebuild indices from existing data in the database
- if err := gs.rebuildIndices(); err != nil {
- return fmt.Errorf("failed to rebuild indices for %s: %w",
gs.kind.ToString(), err)
+ // Backfill resource_indices from existing ResourceModel rows so that
+ // ListByIndexes/HasPrefix work correctly after restarts or upgrades.
+ if err := gs.backfillIndices(); err != nil {
+ return fmt.Errorf("failed to backfill indices for %s: %w",
gs.kind.ToString(), err)
}
logger.Infof("GORM store initialized for resource kind: %s",
gs.kind.ToString())
@@ -138,14 +147,15 @@ func (gs *GormStore) Add(obj interface{}) error {
return err
}
- if err := db.Scopes(TableScope(gs.kind.ToString())).Create(m).Error;
err != nil {
- return err
- }
-
- // Update indices after successful DB operation
- gs.indices.UpdateResource(resource, nil)
-
- return nil
+ return db.Transaction(func(tx *gorm.DB) error {
+ if err :=
tx.Scopes(TableScope(gs.kind.ToString())).Create(m).Error; err != nil {
+ return err
+ }
+ if err := gs.persistIndexEntriesTx(tx, resource, nil); err !=
nil {
+ return fmt.Errorf("failed to persist index entries for
%s: %w", resource.ResourceKey(), err)
+ }
+ return nil
+ })
}
// Update modifies an existing resource in the database
@@ -178,30 +188,32 @@ func (gs *GormStore) Update(obj interface{}) error {
}
db := gs.pool.GetDB()
- result :=
db.Scopes(TableScope(gs.kind.ToString())).Model(&ResourceModel{}).
- Where("resource_key = ?", resource.ResourceKey()).
- Updates(map[string]interface{}{
- "name": m.Name,
- "mesh": m.Mesh,
- "data": m.Data,
- })
-
- if result.Error != nil {
- return result.Error
- }
-
- if result.RowsAffected == 0 {
- return store.ErrorResourceNotFound(
- resource.ResourceKind().ToString(),
- resource.ResourceMeta().Name,
- resource.ResourceMesh(),
- )
- }
+ return db.Transaction(func(tx *gorm.DB) error {
+ result :=
tx.Scopes(TableScope(gs.kind.ToString())).Model(&ResourceModel{}).
+ Where("resource_key = ?", resource.ResourceKey()).
+ Updates(map[string]interface{}{
+ "name": m.Name,
+ "mesh": m.Mesh,
+ "data": m.Data,
+ })
+
+ if result.Error != nil {
+ return result.Error
+ }
- // Update indices: remove old and add new
- gs.indices.UpdateResource(resource, oldResource.(model.Resource))
+ if result.RowsAffected == 0 {
+ return store.ErrorResourceNotFound(
+ resource.ResourceKind().ToString(),
+ resource.ResourceMeta().Name,
+ resource.ResourceMesh(),
+ )
+ }
- return nil
+ if err := gs.persistIndexEntriesTx(tx, resource,
oldResource.(model.Resource)); err != nil {
+ return fmt.Errorf("failed to persist index entries for
%s: %w", resource.ResourceKey(), err)
+ }
+ return nil
+ })
}
// Delete removes a resource from the database
@@ -212,26 +224,26 @@ func (gs *GormStore) Delete(obj interface{}) error {
}
db := gs.pool.GetDB()
- result := db.Scopes(TableScope(gs.kind.ToString())).
- Where("resource_key = ?", resource.ResourceKey()).
- Delete(&ResourceModel{})
-
- if result.Error != nil {
- return result.Error
- }
+ return db.Transaction(func(tx *gorm.DB) error {
+ result := tx.Scopes(TableScope(gs.kind.ToString())).
+ Where("resource_key = ?", resource.ResourceKey()).
+ Delete(&ResourceModel{})
- if result.RowsAffected == 0 {
- return store.ErrorResourceNotFound(
- resource.ResourceKind().ToString(),
- resource.ResourceMeta().Name,
- resource.ResourceMesh(),
- )
- }
+ if result.Error != nil {
+ return result.Error
+ }
- // Remove from indices
- gs.indices.RemoveResource(resource)
+ if result.RowsAffected == 0 {
+ return store.ErrorResourceNotFound(
+ resource.ResourceKind().ToString(),
+ resource.ResourceMeta().Name,
+ resource.ResourceMesh(),
+ )
+ }
- return nil
+ return tx.Where("resource_kind = ? AND resource_key = ?",
gs.kind.ToString(), resource.ResourceKey()).
+ Delete(&ResourceIndexModel{}).Error
+ })
}
// List returns all resources of the configured kind from the database
@@ -308,8 +320,11 @@ func (gs *GormStore) Replace(list []interface{}, _ string)
error {
return err
}
- // Clear all indices
- gs.clearIndices()
+ // Delete all index entries for this resource kind
+ if err := tx.Where("resource_kind = ?", gs.kind.ToString()).
+ Delete(&ResourceIndexModel{}).Error; err != nil {
+ return err
+ }
// Return early if list is empty
if len(list) == 0 {
@@ -338,9 +353,31 @@ func (gs *GormStore) Replace(list []interface{}, _ string)
error {
return err
}
- // Rebuild indices for all resources
+ // Persist all index entries in bulk
+ var indexEntries []ResourceIndexModel
+ indexers := gs.GetIndexers()
for _, resource := range resources {
- gs.indices.UpdateResource(resource, nil)
+ for indexName, indexFunc := range indexers {
+ values, err := indexFunc(resource)
+ if err != nil {
+ continue
+ }
+ for _, v := range values {
+ indexEntries = append(indexEntries,
ResourceIndexModel{
+ ResourceKind:
gs.kind.ToString(),
+ IndexName: indexName,
+ IndexValue: v,
+ ResourceKey:
resource.ResourceKey(),
+ Operator:
string(index.Equals),
+ })
+ }
+ }
+ }
+
+ if len(indexEntries) > 0 {
+ if err := tx.CreateInBatches(&indexEntries, 100).Error;
err != nil {
+ return fmt.Errorf("failed to persist index
entries during replace: %w", err)
+ }
}
return nil
@@ -352,11 +389,12 @@ func (gs *GormStore) Resync() error {
}
func (gs *GormStore) Index(indexName string, obj interface{}) ([]interface{},
error) {
- if !gs.indices.IndexExists(indexName) {
+ if !gs.IndexExists(indexName) {
return nil, fmt.Errorf("index %s does not exist", indexName)
}
- indexFunc := gs.indices.GetIndexers()[indexName]
+ indexers := gs.GetIndexers()
+ indexFunc := indexers[indexName]
indexValues, err := indexFunc(obj)
if err != nil {
return nil, err
@@ -370,7 +408,7 @@ func (gs *GormStore) Index(indexName string, obj
interface{}) ([]interface{}, er
}
func (gs *GormStore) IndexKeys(indexName, indexedValue string) ([]string,
error) {
- if !gs.indices.IndexExists(indexName) {
+ if !gs.IndexExists(indexName) {
return nil, fmt.Errorf("index %s does not exist", indexName)
}
@@ -390,15 +428,24 @@ func (gs *GormStore) IndexKeys(indexName, indexedValue
string) ([]string, error)
}
func (gs *GormStore) ListIndexFuncValues(indexName string) []string {
- if !gs.indices.IndexExists(indexName) {
+ if !gs.IndexExists(indexName) {
return []string{}
}
- return gs.indices.ListIndexFuncValues(indexName)
+ var values []string
+ db := gs.pool.GetDB()
+ if err := db.Model(&ResourceIndexModel{}).
+ Where("resource_kind = ? AND index_name = ?",
gs.kind.ToString(), indexName).
+ Distinct("index_value").
+ Pluck("index_value", &values).Error; err != nil {
+ logger.Errorf("failed to list index func values for %s: %v",
indexName, err)
+ return []string{}
+ }
+ return values
}
func (gs *GormStore) ByIndex(indexName, indexedValue string) ([]interface{},
error) {
- if !gs.indices.IndexExists(indexName) {
+ if !gs.IndexExists(indexName) {
return nil, fmt.Errorf("index %s does not exist", indexName)
}
@@ -406,11 +453,28 @@ func (gs *GormStore) ByIndex(indexName, indexedValue
string) ([]interface{}, err
}
func (gs *GormStore) GetIndexers() cache.Indexers {
- return gs.indices.GetIndexers()
+ gs.mu.RLock()
+ defer gs.mu.RUnlock()
+
+ result := make(cache.Indexers, len(gs.indexers))
+ for k, v := range gs.indexers {
+ result[k] = v
+ }
+ return result
}
func (gs *GormStore) AddIndexers(newIndexers cache.Indexers) error {
- return gs.indices.AddIndexers(newIndexers)
+ gs.mu.Lock()
+ defer gs.mu.Unlock()
+
+ for name, indexFunc := range newIndexers {
+ if _, exists := gs.indexers[name]; exists {
+ return fmt.Errorf("indexer %s already exists", name)
+ }
+ gs.indexers[name] = indexFunc
+ }
+
+ return nil
}
func (gs *GormStore) GetByKeys(keys []string) ([]model.Resource, error) {
@@ -439,7 +503,7 @@ func (gs *GormStore) GetByKeys(keys []string)
([]model.Resource, error) {
return resources, nil
}
-func (gs *GormStore) ListByIndexes(indexes map[string]string)
([]model.Resource, error) {
+func (gs *GormStore) ListByIndexes(indexes []index.IndexCondition)
([]model.Resource, error) {
keys, err := gs.getKeysByIndexes(indexes)
if err != nil {
return nil, err
@@ -457,7 +521,7 @@ func (gs *GormStore) ListByIndexes(indexes
map[string]string) ([]model.Resource,
return resources, nil
}
-func (gs *GormStore) PageListByIndexes(indexes map[string]string, pq
model.PageReq) (*model.PageData[model.Resource], error) {
+func (gs *GormStore) PageListByIndexes(indexes []index.IndexCondition, pq
model.PageReq) (*model.PageData[model.Resource], error) {
keys, err := gs.getKeysByIndexes(indexes)
if err != nil {
return nil, err
@@ -485,17 +549,34 @@ func (gs *GormStore) PageListByIndexes(indexes
map[string]string, pq model.PageR
}
func (gs *GormStore) findByIndex(indexName, indexedValue string)
([]interface{}, error) {
- if !gs.indices.IndexExists(indexName) {
+ if !gs.IndexExists(indexName) {
return nil, fmt.Errorf("index %s does not exist", indexName)
}
- // Get resource keys from in-memory index
- keys := gs.indices.GetKeys(indexName, indexedValue)
+ // Get resource keys from database index
+ db := gs.pool.GetDB()
+ var entries []ResourceIndexModel
+ err := db.Where("resource_kind = ? AND index_name = ? AND index_value =
?",
+ gs.kind.ToString(), indexName, indexedValue).
+ Find(&entries).Error
+ if err != nil {
+ return nil, err
+ }
- if len(keys) == 0 {
+ if len(entries) == 0 {
return []interface{}{}, nil
}
+ // Collect unique resource keys
+ keys := make([]string, 0, len(entries))
+ seen := make(map[string]struct{})
+ for _, e := range entries {
+ if _, ok := seen[e.ResourceKey]; !ok {
+ keys = append(keys, e.ResourceKey)
+ seen[e.ResourceKey] = struct{}{}
+ }
+ }
+
// Fetch resources from DB by keys
resources, err := gs.GetByKeys(keys)
if err != nil {
@@ -511,7 +592,7 @@ func (gs *GormStore) findByIndex(indexName, indexedValue
string) ([]interface{},
return result, nil
}
-func (gs *GormStore) getKeysByIndexes(indexes map[string]string) ([]string,
error) {
+func (gs *GormStore) getKeysByIndexes(indexes []index.IndexCondition)
([]string, error) {
if len(indexes) == 0 {
return gs.ListKeys(), nil
}
@@ -519,8 +600,17 @@ func (gs *GormStore) getKeysByIndexes(indexes
map[string]string) ([]string, erro
var keySet map[string]struct{}
first := true
- for indexName, indexValue := range indexes {
- keys, err := gs.IndexKeys(indexName, indexValue)
+ for _, condition := range indexes {
+ var keys []string
+ var err error
+ switch condition.Operator {
+ case index.Equals:
+ keys, err = gs.IndexKeys(condition.IndexName,
condition.Value)
+ case index.HasPrefix:
+ keys, err =
gs.getKeysByPrefixFromDB(condition.IndexName, condition.Value)
+ default:
+ return nil, bizerror.New(bizerror.InvalidArgument,
"operator not yet supported: "+string(condition.Operator))
+ }
if err != nil {
return nil, err
}
@@ -550,37 +640,122 @@ func (gs *GormStore) getKeysByIndexes(indexes
map[string]string) ([]string, erro
return result, nil
}
-// clearIndices clears all in-memory indices
-func (gs *GormStore) clearIndices() {
- gs.indices.Clear()
+// persistIndexEntriesTx writes index entries for a resource within an
existing transaction.
+// If oldResource is not nil, first deletes its old entries scoped by
resource_kind.
+func (gs *GormStore) persistIndexEntriesTx(tx *gorm.DB, resource
model.Resource, oldResource model.Resource) error {
+ if oldResource != nil {
+ if err := tx.Where("resource_kind = ? AND resource_key = ?",
gs.kind.ToString(), oldResource.ResourceKey()).
+ Delete(&ResourceIndexModel{}).Error; err != nil {
+ return err
+ }
+ }
+
+ indexers := gs.GetIndexers()
+ var entries []ResourceIndexModel
+ for indexName, indexFunc := range indexers {
+ values, err := indexFunc(resource)
+ if err != nil {
+ continue
+ }
+ for _, v := range values {
+ entries = append(entries, ResourceIndexModel{
+ ResourceKind: gs.kind.ToString(),
+ IndexName: indexName,
+ IndexValue: v,
+ ResourceKey: resource.ResourceKey(),
+ Operator: string(index.Equals),
+ })
+ }
+ }
+
+ if len(entries) == 0 {
+ return nil
+ }
+
+ return tx.Create(&entries).Error
}
-// rebuildIndices rebuilds all in-memory indices from existing database records
-// This is called during initialization to ensure indices are populated with
existing data
-func (gs *GormStore) rebuildIndices() error {
- // Clear existing indices first
- gs.clearIndices()
+// backfillIndices rebuilds resource_indices from existing ResourceModel rows.
+// Called at Init time so that ListByIndexes works correctly after restarts or
upgrades.
+func (gs *GormStore) backfillIndices() error {
+ db := gs.pool.GetDB()
- // Load all resources from the database
+ // Load all existing resources for this kind
var models []ResourceModel
+ if err :=
db.Scopes(TableScope(gs.kind.ToString())).Find(&models).Error; err != nil {
+ return err
+ }
+ if len(models) == 0 {
+ return nil
+ }
+
+ return db.Transaction(func(tx *gorm.DB) error {
+ // Drop stale index rows for this kind and rebuild from scratch
+ if err := tx.Where("resource_kind = ?", gs.kind.ToString()).
+ Delete(&ResourceIndexModel{}).Error; err != nil {
+ return err
+ }
+
+ indexers := gs.GetIndexers()
+ var entries []ResourceIndexModel
+ for _, m := range models {
+ resource, err := m.ToResource()
+ if err != nil {
+ logger.Warnf("backfillIndices: failed to
deserialize resource %s: %v", m.ResourceKey, err)
+ continue
+ }
+ for indexName, indexFunc := range indexers {
+ values, err := indexFunc(resource)
+ if err != nil {
+ continue
+ }
+ for _, v := range values {
+ entries = append(entries,
ResourceIndexModel{
+ ResourceKind:
gs.kind.ToString(),
+ IndexName: indexName,
+ IndexValue: v,
+ ResourceKey:
resource.ResourceKey(),
+ Operator:
string(index.Equals),
+ })
+ }
+ }
+ }
+
+ if len(entries) == 0 {
+ return nil
+ }
+ return tx.CreateInBatches(&entries, 100).Error
+ })
+}
+
+// getKeysByPrefixFromDB retrieves resource keys matching a prefix from the
database
+func (gs *GormStore) getKeysByPrefixFromDB(indexName, prefix string)
([]string, error) {
db := gs.pool.GetDB()
- if err :=
db.Scopes(TableScope(gs.kind.ToString())).Model(&ResourceModel{}).Find(&models).Error;
err != nil {
- return fmt.Errorf("failed to load resources for index rebuild:
%w", err)
+ var entries []ResourceIndexModel
+ err := db.Where("resource_kind = ? AND index_name = ? AND index_value
LIKE ?",
+ gs.kind.ToString(), indexName, prefix+"%").
+ Find(&entries).Error
+ if err != nil {
+ return nil, err
}
- // Rebuild indices for all resources
- for _, m := range models {
- resource, err := m.ToResource()
- if err != nil {
- logger.Errorf("failed to deserialize resource during
index rebuild: %v", err)
- continue
+ keys := make([]string, 0, len(entries))
+ seen := make(map[string]struct{})
+ for _, e := range entries {
+ if _, ok := seen[e.ResourceKey]; !ok {
+ keys = append(keys, e.ResourceKey)
+ seen[e.ResourceKey] = struct{}{}
}
- // Add resource to indices (nil for oldResource since this is
initial load)
- gs.indices.UpdateResource(resource, nil)
}
+ return keys, nil
+}
- logger.Infof("Rebuilt indices for %s: loaded %d resources",
gs.kind.ToString(), len(models))
- return nil
+// IndexExists checks if an indexer with the given name exists
+func (gs *GormStore) IndexExists(indexName string) bool {
+ gs.mu.RLock()
+ defer gs.mu.RUnlock()
+ _, exists := gs.indexers[indexName]
+ return exists
}
// Pool returns the connection pool for this store
diff --git a/pkg/store/dbcommon/gorm_store_test.go
b/pkg/store/dbcommon/gorm_store_test.go
index da9718ed..b1dd1b8c 100644
--- a/pkg/store/dbcommon/gorm_store_test.go
+++ b/pkg/store/dbcommon/gorm_store_test.go
@@ -33,6 +33,7 @@ import (
storecfg "github.com/apache/dubbo-admin/pkg/config/store"
"github.com/apache/dubbo-admin/pkg/core/resource/model"
+ "github.com/apache/dubbo-admin/pkg/core/store/index"
)
// mockResource is a mock implementation of model.Resource for testing
@@ -170,7 +171,7 @@ func TestNewGormStore(t *testing.T) {
assert.Equal(t, kind, store.kind)
assert.Equal(t, "test-address", store.address)
assert.NotNil(t, store.pool)
- assert.NotNil(t, store.indices)
+ assert.NotNil(t, store.indexers)
assert.NotNil(t, store.stopCh)
}
@@ -775,7 +776,7 @@ func TestGormStore_ListByIndexes(t *testing.T) {
require.NoError(t, err)
// List by indexes
- indexes := map[string]string{"by-mesh": "mesh1"}
+ indexes := []index.IndexCondition{{IndexName: "by-mesh", Value:
"mesh1", Operator: index.Equals}}
resources, err := store.ListByIndexes(indexes)
assert.NoError(t, err)
assert.Len(t, resources, 2)
@@ -802,7 +803,7 @@ func TestGormStore_ListByIndexesEmpty(t *testing.T) {
require.NoError(t, err)
// List with empty indexes should return all resources
- resources, err := store.ListByIndexes(map[string]string{})
+ resources, err := store.ListByIndexes([]index.IndexCondition{})
assert.NoError(t, err)
assert.Len(t, resources, 1)
}
@@ -855,7 +856,7 @@ func TestGormStore_PageListByIndexes(t *testing.T) {
require.NoError(t, err)
// Page list by indexes
- indexes := map[string]string{"by-mesh": "mesh1"}
+ indexes := []index.IndexCondition{{IndexName: "by-mesh", Value:
"mesh1", Operator: index.Equals}}
pageReq := model.PageReq{
PageOffset: 0,
PageSize: 2,
@@ -918,7 +919,7 @@ func TestGormStore_PageListByIndexesOffsetBeyondTotal(t
*testing.T) {
require.NoError(t, err)
// Request page beyond total
- indexes := map[string]string{"by-mesh": "default"}
+ indexes := []index.IndexCondition{{IndexName: "by-mesh", Value:
"default", Operator: index.Equals}}
pageReq := model.PageReq{
PageOffset: 10,
PageSize: 2,
@@ -999,9 +1000,9 @@ func TestGormStore_MultipleIndexes(t *testing.T) {
}
// Test multiple indexes - get all resources in mesh1 and default
namespace
- indexes := map[string]string{
- "by-mesh": "mesh1",
- "by-namespace": "default",
+ indexes := []index.IndexCondition{
+ {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals},
+ {IndexName: "by-namespace", Value: "default", Operator:
index.Equals},
}
result, err := store.ListByIndexes(indexes)
assert.NoError(t, err)
@@ -1238,79 +1239,6 @@ func TestGormStore_ReplaceIndices(t *testing.T) {
assert.Contains(t, keys, "test-key-2")
}
-func TestGormStore_InitRebuildIndices(t *testing.T) {
- // This test verifies that indices are rebuilt from existing data
during Init()
- // Simulates the scenario where a GormStore starts with existing data
in the database
-
- // Create store and add data
- store, cleanup := setupTestStore(t)
- defer cleanup()
-
- err := store.Init(nil)
- require.NoError(t, err)
-
- // Add indexer before adding data
- indexers := map[string]cache.IndexFunc{
- "by-mesh": func(obj interface{}) ([]string, error) {
- resource := obj.(model.Resource)
- return []string{resource.ResourceMesh()}, nil
- },
- }
- err = store.AddIndexers(indexers)
- require.NoError(t, err)
-
- // Add some resources to the database
- mockRes1 := &mockResource{
- Kind: "TestResource",
- Key: "test-key-1",
- Mesh: "mesh1",
- Meta: metav1.ObjectMeta{Name: "test-resource-1"},
- }
- mockRes2 := &mockResource{
- Kind: "TestResource",
- Key: "test-key-2",
- Mesh: "mesh2",
- Meta: metav1.ObjectMeta{Name: "test-resource-2"},
- }
- err = store.Add(mockRes1)
- require.NoError(t, err)
- err = store.Add(mockRes2)
- require.NoError(t, err)
-
- // Verify indices are populated
- keys, err := store.IndexKeys("by-mesh", "mesh1")
- assert.NoError(t, err)
- assert.Contains(t, keys, "test-key-1")
-
- // Now simulate a restart by creating a new store instance with the
same pool
- // This simulates the scenario where existing data exists in the
database
- pool := store.pool
- pool.IncrementRef() // Increment ref count since we're creating another
store using it
-
- newStore := NewGormStore("TestResource", pool.Address(), pool)
-
- // Add indexers BEFORE Init to ensure they're available during index
rebuild
- err = newStore.AddIndexers(indexers)
- require.NoError(t, err)
-
- // Init should rebuild indices from existing database data
- err = newStore.Init(nil)
- require.NoError(t, err)
-
- // Verify indices were rebuilt with existing data
- keys, err = newStore.IndexKeys("by-mesh", "mesh1")
- assert.NoError(t, err)
- assert.Contains(t, keys, "test-key-1", "Index should contain existing
data after Init()")
-
- keys, err = newStore.IndexKeys("by-mesh", "mesh2")
- assert.NoError(t, err)
- assert.Contains(t, keys, "test-key-2", "Index should contain existing
data after Init()")
-
- // Verify all keys are present
- allKeys := newStore.ListKeys()
- assert.Len(t, allKeys, 2)
-}
-
func TestGormStore_Resync(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
@@ -1475,3 +1403,290 @@ func TestGormStore_InvalidResourceType(t *testing.T) {
_, _, err = store.Get("not-a-resource")
assert.Error(t, err)
}
+
+func TestGormStore_IndexPersistence(t *testing.T) {
+ store, cleanup := setupTestStore(t)
+ defer cleanup()
+
+ err := store.Init(nil)
+ require.NoError(t, err)
+
+ // Add indexer for IP addresses
+ indexers := map[string]cache.IndexFunc{
+ "by-ip": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ // Simulate an IP address from the resource key
+ return []string{resource.ResourceKey()}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ require.NoError(t, err)
+
+ // Create and add resources with IP-like keys
+ mockRes1 := &mockResource{
+ Kind: "TestResource",
+ Key: "192.168.1.1",
+ Mesh: "default",
+ Meta: metav1.ObjectMeta{Name: "resource-1"},
+ }
+ mockRes2 := &mockResource{
+ Kind: "TestResource",
+ Key: "192.168.1.2",
+ Mesh: "default",
+ Meta: metav1.ObjectMeta{Name: "resource-2"},
+ }
+
+ err = store.Add(mockRes1)
+ require.NoError(t, err)
+ err = store.Add(mockRes2)
+ require.NoError(t, err)
+
+ // Verify indices are persisted to resource_indices table
+ db := store.pool.GetDB()
+ var count int64
+ err = db.Model(&ResourceIndexModel{}).
+ Where("resource_kind = ? AND index_name = ?", "TestResource",
"by-ip").
+ Count(&count).Error
+ assert.NoError(t, err)
+ assert.Equal(t, int64(2), count, "Both index entries should be
persisted to resource_indices table")
+
+ // Verify operator field is set to "Equals"
+ var entries []ResourceIndexModel
+ err = db.Where("resource_kind = ? AND index_name = ?", "TestResource",
"by-ip").
+ Find(&entries).Error
+ assert.NoError(t, err)
+ for _, entry := range entries {
+ assert.Equal(t, "Equals", entry.Operator, "Operator field
should be set to Equals")
+ }
+}
+
+func TestGormStore_ListByIndexes_HasPrefix(t *testing.T) {
+ store, cleanup := setupTestStore(t)
+ defer cleanup()
+
+ err := store.Init(nil)
+ require.NoError(t, err)
+
+ // Add indexer for IP addresses
+ indexers := map[string]cache.IndexFunc{
+ "by-ip": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ return []string{resource.ResourceKey()}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ require.NoError(t, err)
+
+ // Create resources with IP-like keys
+ mockRes1 := &mockResource{
+ Kind: "TestResource",
+ Key: "192.168.1.1",
+ Mesh: "default",
+ Meta: metav1.ObjectMeta{Name: "resource-1"},
+ }
+ mockRes2 := &mockResource{
+ Kind: "TestResource",
+ Key: "192.168.1.2",
+ Mesh: "default",
+ Meta: metav1.ObjectMeta{Name: "resource-2"},
+ }
+ mockRes3 := &mockResource{
+ Kind: "TestResource",
+ Key: "10.0.0.1",
+ Mesh: "default",
+ Meta: metav1.ObjectMeta{Name: "resource-3"},
+ }
+
+ err = store.Add(mockRes1)
+ require.NoError(t, err)
+ err = store.Add(mockRes2)
+ require.NoError(t, err)
+ err = store.Add(mockRes3)
+ require.NoError(t, err)
+
+ // Query with HasPrefix operator
+ indexes := []index.IndexCondition{
+ {IndexName: "by-ip", Value: "192.168", Operator:
index.HasPrefix},
+ }
+ resources, err := store.ListByIndexes(indexes)
+ assert.NoError(t, err)
+ assert.Len(t, resources, 2)
+
+ // Verify the correct resources were returned
+ keys := make([]string, len(resources))
+ for i, res := range resources {
+ keys[i] = res.ResourceKey()
+ }
+ assert.Contains(t, keys, "192.168.1.1")
+ assert.Contains(t, keys, "192.168.1.2")
+ assert.NotContains(t, keys, "10.0.0.1")
+}
+
+func TestGormStore_PageListByIndexes_HasPrefix(t *testing.T) {
+ store, cleanup := setupTestStore(t)
+ defer cleanup()
+
+ err := store.Init(nil)
+ require.NoError(t, err)
+
+ // Add indexer for IP addresses
+ indexers := map[string]cache.IndexFunc{
+ "by-ip": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ return []string{resource.ResourceKey()}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ require.NoError(t, err)
+
+ // Create resources with IP-like keys
+ for i := 1; i <= 5; i++ {
+ mockRes := &mockResource{
+ Kind: "TestResource",
+ Key: fmt.Sprintf("192.168.1.%d", i),
+ Mesh: "default",
+ Meta: metav1.ObjectMeta{Name:
fmt.Sprintf("resource-%d", i)},
+ }
+ err = store.Add(mockRes)
+ require.NoError(t, err)
+ }
+
+ // Query with HasPrefix operator and pagination
+ indexes := []index.IndexCondition{
+ {IndexName: "by-ip", Value: "192.168", Operator:
index.HasPrefix},
+ }
+ pageReq := model.PageReq{
+ PageOffset: 0,
+ PageSize: 2,
+ }
+ pageData, err := store.PageListByIndexes(indexes, pageReq)
+ assert.NoError(t, err)
+ assert.Equal(t, 5, pageData.Total)
+ assert.Len(t, pageData.Data, 2)
+
+ // Verify second page
+ pageReq.PageOffset = 2
+ pageData, err = store.PageListByIndexes(indexes, pageReq)
+ assert.NoError(t, err)
+ assert.Equal(t, 5, pageData.Total)
+ assert.Len(t, pageData.Data, 2)
+
+ // Verify last page
+ pageReq.PageOffset = 4
+ pageData, err = store.PageListByIndexes(indexes, pageReq)
+ assert.NoError(t, err)
+ assert.Equal(t, 5, pageData.Total)
+ assert.Len(t, pageData.Data, 1)
+}
+
+func TestGormStore_DeleteIndex_RemovesFromDB(t *testing.T) {
+ store, cleanup := setupTestStore(t)
+ defer cleanup()
+
+ err := store.Init(nil)
+ require.NoError(t, err)
+
+ // Add indexer
+ indexers := map[string]cache.IndexFunc{
+ "by-name": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ return []string{resource.ResourceMeta().Name}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ require.NoError(t, err)
+
+ // Create and add resource
+ mockRes := &mockResource{
+ Kind: "TestResource",
+ Key: "test-key",
+ Mesh: "default",
+ Meta: metav1.ObjectMeta{Name: "test-resource"},
+ }
+ err = store.Add(mockRes)
+ require.NoError(t, err)
+
+ // Verify index entry exists in DB
+ db := store.pool.GetDB()
+ var count int64
+ err = db.Model(&ResourceIndexModel{}).
+ Where("resource_kind = ? AND resource_key = ?", "TestResource",
"test-key").
+ Count(&count).Error
+ assert.NoError(t, err)
+ assert.Greater(t, count, int64(0), "Index entry should exist after Add")
+
+ // Delete the resource
+ err = store.Delete(mockRes)
+ require.NoError(t, err)
+
+ // Verify index entry is removed from DB
+ count = 0
+ err = db.Model(&ResourceIndexModel{}).
+ Where("resource_kind = ? AND resource_key = ?", "TestResource",
"test-key").
+ Count(&count).Error
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), count, "Index entry should be removed after
Delete")
+}
+
+func TestGormStore_UpdateIndex_UpdatesInDB(t *testing.T) {
+ store, cleanup := setupTestStore(t)
+ defer cleanup()
+
+ err := store.Init(nil)
+ require.NoError(t, err)
+
+ // Add indexer
+ indexers := map[string]cache.IndexFunc{
+ "by-mesh": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ return []string{resource.ResourceMesh()}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ require.NoError(t, err)
+
+ // Create and add resource
+ mockRes := &mockResource{
+ Kind: "TestResource",
+ Key: "test-key",
+ Mesh: "mesh1",
+ Meta: metav1.ObjectMeta{Name: "test-resource"},
+ }
+ err = store.Add(mockRes)
+ require.NoError(t, err)
+
+ // Verify initial index entry in DB
+ db := store.pool.GetDB()
+ var count int64
+ err = db.Model(&ResourceIndexModel{}).
+ Where("resource_kind = ? AND resource_key = ? AND index_value =
?", "TestResource", "test-key", "mesh1").
+ Count(&count).Error
+ assert.NoError(t, err)
+ assert.Equal(t, int64(1), count, "Initial index entry should exist")
+
+ // Update resource with different mesh
+ updatedRes := &mockResource{
+ Kind: "TestResource",
+ Key: "test-key",
+ Mesh: "mesh2",
+ Meta: metav1.ObjectMeta{Name: "test-resource"},
+ }
+ err = store.Update(updatedRes)
+ require.NoError(t, err)
+
+ // Verify old index entry is removed
+ count = 0
+ err = db.Model(&ResourceIndexModel{}).
+ Where("resource_kind = ? AND resource_key = ? AND index_value =
?", "TestResource", "test-key", "mesh1").
+ Count(&count).Error
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), count, "Old index entry should be removed")
+
+ // Verify new index entry exists
+ count = 0
+ err = db.Model(&ResourceIndexModel{}).
+ Where("resource_kind = ? AND resource_key = ? AND index_value =
?", "TestResource", "test-key", "mesh2").
+ Count(&count).Error
+ assert.NoError(t, err)
+ assert.Equal(t, int64(1), count, "New index entry should exist")
+}
diff --git a/pkg/store/dbcommon/index.go b/pkg/store/dbcommon/index.go
deleted file mode 100644
index 5c535619..00000000
--- a/pkg/store/dbcommon/index.go
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package dbcommon
-
-import (
- "fmt"
- "sync"
-
- set "github.com/duke-git/lancet/v2/datastructure/set"
- "k8s.io/client-go/tools/cache"
-
- "github.com/apache/dubbo-admin/pkg/core/resource/model"
-)
-
-// ValueIndex represents the mapping from indexed values to resource keys for
a single index.
-// Structure: map[indexedValue]set[resourceKey]
-// Example: map["default"]{"resource1", "resource2", "resource3"}
-type ValueIndex struct {
- values map[string]set.Set[string]
-}
-
-// NewValueIndex creates a new ValueIndex
-func NewValueIndex() *ValueIndex {
- return &ValueIndex{
- values: make(map[string]set.Set[string]),
- }
-}
-
-// Add adds a resource key to the specified indexed value
-func (vi *ValueIndex) Add(indexedValue, resourceKey string) {
- if vi.values[indexedValue] == nil {
- vi.values[indexedValue] = set.New[string]()
- }
- vi.values[indexedValue].Add(resourceKey)
-}
-
-// Remove removes a resource key from the specified indexed value
-// Returns true if the value entry becomes empty after removal
-func (vi *ValueIndex) Remove(indexedValue, resourceKey string) bool {
- if vi.values[indexedValue] == nil {
- return false
- }
-
- vi.values[indexedValue].Delete(resourceKey)
-
- // Check if the set is now empty
- if vi.values[indexedValue].Size() == 0 {
- delete(vi.values, indexedValue)
- return true
- }
-
- return false
-}
-
-// GetKeys returns all resource keys for the specified indexed value
-func (vi *ValueIndex) GetKeys(indexedValue string) []string {
- if vi.values[indexedValue] == nil {
- return []string{}
- }
- return vi.values[indexedValue].ToSlice()
-}
-
-// GetAllValues returns all indexed values in this ValueIndex
-func (vi *ValueIndex) GetAllValues() []string {
- if len(vi.values) == 0 {
- return []string{}
- }
-
- result := make([]string, 0, len(vi.values))
- for value := range vi.values {
- result = append(result, value)
- }
- return result
-}
-
-// IsEmpty returns true if the ValueIndex has no entries
-func (vi *ValueIndex) IsEmpty() bool {
- return len(vi.values) == 0
-}
-
-// Index is a thread-safe in-memory index structure that manages multiple
named indices.
-// Each index maps values to sets of resource keys.
-//
-// Structure: map[indexName]*ValueIndex
-// Example: map["mesh"]*ValueIndex where ValueIndex contains {"default":
{"res1", "res2"}}
-type Index struct {
- mu sync.RWMutex
- indices map[string]*ValueIndex // map[indexName]*ValueIndex
- indexers cache.Indexers // Index functions for creating indices
-}
-
-// NewIndex creates a new empty Index instance
-func NewIndex() *Index {
- return &Index{
- indices: make(map[string]*ValueIndex),
- indexers: cache.Indexers{},
- }
-}
-
-// AddIndexers adds new indexer functions to the Index
-// Returns an error if an indexer with the same name already exists
-func (idx *Index) AddIndexers(newIndexers cache.Indexers) error {
- idx.mu.Lock()
- defer idx.mu.Unlock()
-
- for name, indexFunc := range newIndexers {
- if _, exists := idx.indexers[name]; exists {
- return fmt.Errorf("indexer %s already exists", name)
- }
- idx.indexers[name] = indexFunc
- }
-
- return nil
-}
-
-// GetIndexers returns a copy of all registered indexers
-func (idx *Index) GetIndexers() cache.Indexers {
- idx.mu.RLock()
- defer idx.mu.RUnlock()
-
- result := make(cache.Indexers, len(idx.indexers))
- for k, v := range idx.indexers {
- result[k] = v
- }
- return result
-}
-
-// UpdateResource atomically updates all indices for a resource
-// If oldResource is nil, it's treated as an add operation
-// If oldResource is not nil, it's treated as an update operation (remove old,
add new)
-// This is a high-level atomic operation that handles all indexers internally
-func (idx *Index) UpdateResource(newResource model.Resource, oldResource
model.Resource) {
- idx.mu.Lock()
- defer idx.mu.Unlock()
-
- // Remove old resource from indices if this is an update
- if oldResource != nil {
- idx.removeResourceUnsafe(oldResource)
- }
-
- // Add new resource to indices
- idx.addResourceUnsafe(newResource)
-}
-
-// RemoveResource atomically removes a resource from all indices
-func (idx *Index) RemoveResource(resource model.Resource) {
- idx.mu.Lock()
- defer idx.mu.Unlock()
-
- idx.removeResourceUnsafe(resource)
-}
-
-// GetKeys returns all resource keys for a given index name and value
-// Returns an empty slice if the index name or value doesn't exist
-func (idx *Index) GetKeys(indexName, indexValue string) []string {
- idx.mu.RLock()
- defer idx.mu.RUnlock()
-
- valueIndex := idx.indices[indexName]
- if valueIndex == nil {
- return []string{}
- }
-
- return valueIndex.GetKeys(indexValue)
-}
-
-// ListIndexFuncValues returns all indexed values for a given index name
-// This directly retrieves values from the in-memory index without
recalculating
-func (idx *Index) ListIndexFuncValues(indexName string) []string {
- idx.mu.RLock()
- defer idx.mu.RUnlock()
-
- valueIndex := idx.indices[indexName]
- if valueIndex == nil {
- return []string{}
- }
-
- return valueIndex.GetAllValues()
-}
-
-// Clear removes all entries from the index
-func (idx *Index) Clear() {
- idx.mu.Lock()
- defer idx.mu.Unlock()
- idx.indices = make(map[string]*ValueIndex)
-}
-
-// IndexExists checks if an indexer with the given name exists
-func (idx *Index) IndexExists(indexName string) bool {
- idx.mu.RLock()
- defer idx.mu.RUnlock()
- _, exists := idx.indexers[indexName]
- return exists
-}
-
-// addResourceUnsafe adds a resource to all indices (must be called with lock
held)
-func (idx *Index) addResourceUnsafe(resource model.Resource) {
- for indexName, indexFunc := range idx.indexers {
- values, err := indexFunc(resource)
- if err != nil {
- continue
- }
-
- // Ensure the ValueIndex exists for this index name
- if idx.indices[indexName] == nil {
- idx.indices[indexName] = NewValueIndex()
- }
-
- // Add resource key to each indexed value
- for _, value := range values {
- idx.indices[indexName].Add(value,
resource.ResourceKey())
- }
- }
-}
-
-// removeResourceUnsafe removes a resource from all indices (must be called
with lock held)
-func (idx *Index) removeResourceUnsafe(resource model.Resource) {
- for indexName, indexFunc := range idx.indexers {
- values, err := indexFunc(resource)
- if err != nil {
- continue
- }
-
- valueIndex := idx.indices[indexName]
- if valueIndex == nil {
- continue
- }
-
- // Remove resource key from each indexed value
- for _, value := range values {
- valueIndex.Remove(value, resource.ResourceKey())
- }
-
- // Clean up empty ValueIndex
- if valueIndex.IsEmpty() {
- delete(idx.indices, indexName)
- }
- }
-}
diff --git a/pkg/store/dbcommon/model.go b/pkg/store/dbcommon/model.go
index 5cf66c69..4bdecc13 100644
--- a/pkg/store/dbcommon/model.go
+++ b/pkg/store/dbcommon/model.go
@@ -123,3 +123,22 @@ func FromResource(resource model.Resource)
(*ResourceModel, error) {
Data: data,
}, nil
}
+
+// ResourceIndexModel represents a persisted index entry in the database
+// This table stores index mappings to enable prefix queries and provide index
persistence
+// across multiple replicas in distributed deployments
+// Table: resource_indices (shared across all resource kinds)
+type ResourceIndexModel struct {
+ ID uint `gorm:"primarykey"`
// Auto-incrementing primary key
+ ResourceKind string `gorm:"type:varchar(64);not
null;index:idx_kind_name_value"` // Resource kind (e.g., "Instance")
+ IndexName string `gorm:"type:varchar(128);not
null;index:idx_kind_name_value"` // Index name (e.g., "idx_instance_ip")
+ IndexValue string `gorm:"type:varchar(255);not
null;index:idx_kind_name_value"` // Indexed value (e.g., "192.168.1.1")
+ ResourceKey string `gorm:"type:varchar(255);not
null;index:idx_resource_key"` // Resource unique key
+ Operator string `gorm:"type:varchar(32);not null;default:Equals"`
// Index operator type, e.g., Equals, HasPrefix
+}
+
+// TableName specifies the table name for ResourceIndexModel
+// Unlike ResourceModel which uses dynamic per-kind tables, resource_indices
is a shared global table
+func (ResourceIndexModel) TableName() string {
+ return "resource_indices"
+}
diff --git a/pkg/store/memory/store.go b/pkg/store/memory/store.go
index 28ce04d2..0b392bd5 100644
--- a/pkg/store/memory/store.go
+++ b/pkg/store/memory/store.go
@@ -18,9 +18,13 @@
package memory
import (
+ "fmt"
"reflect"
"sort"
+ "strings"
+ "sync"
+ "github.com/armon/go-radix"
set "github.com/duke-git/lancet/v2/datastructure/set"
"github.com/duke-git/lancet/v2/slice"
"k8s.io/client-go/tools/cache"
@@ -33,8 +37,10 @@ import (
)
type resourceStore struct {
- rk coremodel.ResourceKind
- storeProxy cache.Indexer
+ rk coremodel.ResourceKind
+ storeProxy cache.Indexer
+ prefixTrees map[string]*radix.Tree
+ treesMu sync.RWMutex
}
var _ store.ManagedResourceStore = &resourceStore{}
@@ -55,6 +61,11 @@ func (rs *resourceStore) Init(_ runtime.BuilderContext)
error {
},
indexers,
)
+ // Initialize RadixTree for each index for prefix matching support
+ rs.prefixTrees = make(map[string]*radix.Tree)
+ for indexName := range indexers {
+ rs.prefixTrees[indexName] = radix.New()
+ }
return nil
}
@@ -63,15 +74,47 @@ func (rs *resourceStore) Start(_ runtime.Runtime, _ <-chan
struct{}) error {
}
func (rs *resourceStore) Add(obj interface{}) error {
- return rs.storeProxy.Add(obj)
+ if err := rs.storeProxy.Add(obj); err != nil {
+ return err
+ }
+ r, ok := obj.(coremodel.Resource)
+ if ok {
+ rs.addToTrees(r)
+ }
+ return nil
}
func (rs *resourceStore) Update(obj interface{}) error {
- return rs.storeProxy.Update(obj)
+ r, ok := obj.(coremodel.Resource)
+ var oldRes coremodel.Resource
+ if ok {
+ // Fetch old resource before mutating the store
+ oldObj, exists, err := rs.storeProxy.Get(r)
+ if exists && err == nil {
+ oldRes, _ = oldObj.(coremodel.Resource)
+ }
+ }
+ if err := rs.storeProxy.Update(obj); err != nil {
+ return err
+ }
+ // Only mutate trees after a successful store update
+ if ok {
+ if oldRes != nil {
+ rs.removeFromTrees(oldRes)
+ }
+ rs.addToTrees(r)
+ }
+ return nil
}
func (rs *resourceStore) Delete(obj interface{}) error {
- return rs.storeProxy.Delete(obj)
+ if err := rs.storeProxy.Delete(obj); err != nil {
+ return err
+ }
+ if r, ok := obj.(coremodel.Resource); ok {
+ rs.removeFromTrees(r)
+ }
+ return nil
}
func (rs *resourceStore) List() []interface{} {
@@ -91,7 +134,25 @@ func (rs *resourceStore) GetByKey(key string) (item
interface{}, exists bool, er
}
func (rs *resourceStore) Replace(i []interface{}, s string) error {
- return rs.storeProxy.Replace(i, s)
+ // Clear all trees before replace
+ rs.treesMu.Lock()
+ for indexName := range rs.prefixTrees {
+ rs.prefixTrees[indexName] = radix.New()
+ }
+ rs.treesMu.Unlock()
+
+ if err := rs.storeProxy.Replace(i, s); err != nil {
+ return err
+ }
+
+ // Add all new resources to trees
+ for _, obj := range i {
+ r, ok := obj.(coremodel.Resource)
+ if ok {
+ rs.addToTrees(r)
+ }
+ }
+ return nil
}
func (rs *resourceStore) Resync() error {
@@ -119,7 +180,20 @@ func (rs *resourceStore) GetIndexers() cache.Indexers {
}
func (rs *resourceStore) AddIndexers(newIndexers cache.Indexers) error {
- return rs.storeProxy.AddIndexers(newIndexers)
+ rs.treesMu.Lock()
+ defer rs.treesMu.Unlock()
+
+ if err := rs.storeProxy.AddIndexers(newIndexers); err != nil {
+ return err
+ }
+
+ // Add RadixTrees for new indexers
+ for indexName := range newIndexers {
+ if _, exists := rs.prefixTrees[indexName]; !exists {
+ rs.prefixTrees[indexName] = radix.New()
+ }
+ }
+ return nil
}
func (rs *resourceStore) GetByKeys(keys []string) ([]coremodel.Resource,
error) {
@@ -141,7 +215,7 @@ func (rs *resourceStore) GetByKeys(keys []string)
([]coremodel.Resource, error)
return resources, nil
}
-func (rs *resourceStore) ListByIndexes(indexes map[string]string)
([]coremodel.Resource, error) {
+func (rs *resourceStore) ListByIndexes(indexes []index.IndexCondition)
([]coremodel.Resource, error) {
keys, err := rs.getKeysByIndexes(indexes)
if err != nil {
return nil, err
@@ -156,7 +230,7 @@ func (rs *resourceStore) ListByIndexes(indexes
map[string]string) ([]coremodel.R
return resources, nil
}
-func (rs *resourceStore) PageListByIndexes(indexes map[string]string, pq
coremodel.PageReq) (*coremodel.PageData[coremodel.Resource], error) {
+func (rs *resourceStore) PageListByIndexes(indexes []index.IndexCondition, pq
coremodel.PageReq) (*coremodel.PageData[coremodel.Resource], error) {
keys, err := rs.getKeysByIndexes(indexes)
if err != nil {
return nil, err
@@ -183,17 +257,29 @@ func (rs *resourceStore) PageListByIndexes(indexes
map[string]string, pq coremod
return pageData, nil
}
-func (rs *resourceStore) getKeysByIndexes(indexes map[string]string)
([]string, error) {
+func (rs *resourceStore) getKeysByIndexes(indexes []index.IndexCondition)
([]string, error) {
if len(indexes) == 0 {
return []string{}, nil
}
keySet := set.New[string]()
first := true
- for indexName, indexValue := range indexes {
- keys, err := rs.storeProxy.IndexKeys(indexName, indexValue)
+ for _, condition := range indexes {
+ var keys []string
+ var err error
+
+ switch condition.Operator {
+ case index.Equals:
+ keys, err =
rs.storeProxy.IndexKeys(condition.IndexName, condition.Value)
+ case index.HasPrefix:
+ keys, err = rs.getKeysByPrefix(condition.IndexName,
condition.Value)
+ default:
+ return nil, bizerror.New(bizerror.InvalidArgument,
"operator not yet supported: "+string(condition.Operator))
+ }
+
if err != nil {
return nil, err
}
+
if first {
keySet = set.FromSlice(keys)
first = false
@@ -204,3 +290,78 @@ func (rs *resourceStore) getKeysByIndexes(indexes
map[string]string) ([]string,
}
return keySet.ToSlice(), nil
}
+
+// addToTrees adds a resource to all relevant RadixTrees for prefix matching
+func (rs *resourceStore) addToTrees(resource coremodel.Resource) {
+ rs.treesMu.Lock()
+ defer rs.treesMu.Unlock()
+
+ // Get indexers from storeProxy, not from global registry
+ // This ensures we include both init-time and dynamically-added indexers
+ indexers := rs.storeProxy.GetIndexers()
+ for indexName, indexFunc := range indexers {
+ values, err := indexFunc(resource)
+ if err != nil {
+ continue
+ }
+ tree, ok := rs.prefixTrees[indexName]
+ if !ok || tree == nil {
+ continue
+ }
+ for _, v := range values {
+ // Key format: "indexValue/resourceKey"
+ key := v + "/" + resource.ResourceKey()
+ tree.Insert(key, struct{}{})
+ }
+ }
+}
+
+// removeFromTrees removes a resource from all relevant RadixTrees
+func (rs *resourceStore) removeFromTrees(resource coremodel.Resource) {
+ rs.treesMu.Lock()
+ defer rs.treesMu.Unlock()
+
+ // Get indexers from storeProxy, not from global registry
+ // This ensures we include both init-time and dynamically-added indexers
+ indexers := rs.storeProxy.GetIndexers()
+ for indexName, indexFunc := range indexers {
+ values, err := indexFunc(resource)
+ if err != nil {
+ continue
+ }
+ tree, ok := rs.prefixTrees[indexName]
+ if !ok || tree == nil {
+ continue
+ }
+ for _, v := range values {
+ // Key format: "indexValue/resourceKey"
+ key := v + "/" + resource.ResourceKey()
+ tree.Delete(key)
+ }
+ }
+}
+
+// getKeysByPrefix retrieves resource keys by prefix match using RadixTree
+func (rs *resourceStore) getKeysByPrefix(indexName, prefix string) ([]string,
error) {
+ rs.treesMu.RLock()
+ defer rs.treesMu.RUnlock()
+
+ tree, ok := rs.prefixTrees[indexName]
+ if !ok {
+ return nil, fmt.Errorf("index %s does not exist", indexName)
+ }
+
+ var keys []string
+ tree.WalkPrefix(prefix, func(k string, v interface{}) bool {
+ // Key format: "indexValue/resourceKey"
+ // Key format: "indexValue/resourceKey"
+ // Use Index (first "/") because resourceKey itself contains
"/" (mesh/name)
+ idx := strings.Index(k, "/")
+ if idx >= 0 && idx < len(k)-1 {
+ keys = append(keys, k[idx+1:])
+ }
+ return false // Continue walking
+ })
+
+ return keys, nil
+}
diff --git a/pkg/store/memory/store_test.go b/pkg/store/memory/store_test.go
index 8d1f1104..8ad40199 100644
--- a/pkg/store/memory/store_test.go
+++ b/pkg/store/memory/store_test.go
@@ -28,6 +28,7 @@ import (
"k8s.io/client-go/tools/cache"
"github.com/apache/dubbo-admin/pkg/core/resource/model"
+ "github.com/apache/dubbo-admin/pkg/core/store/index"
)
// mockResource is a mock implementation of model.Resource for testing
@@ -377,7 +378,7 @@ func TestResourceStore_ListByIndexes(t *testing.T) {
assert.NoError(t, err)
// List by indexes
- indexes := map[string]string{"by-mesh": "mesh1"}
+ indexes := []index.IndexCondition{{IndexName: "by-mesh", Value:
"mesh1", Operator: index.Equals}}
resources, err := store.ListByIndexes(indexes)
assert.NoError(t, err)
assert.Len(t, resources, 2)
@@ -432,7 +433,7 @@ func TestResourceStore_PageListByIndexes(t *testing.T) {
assert.NoError(t, err)
// Page list by indexes
- indexes := map[string]string{"by-mesh": "mesh1"}
+ indexes := []index.IndexCondition{{IndexName: "by-mesh", Value:
"mesh1", Operator: index.Equals}}
pageReq := model.PageReq{
PageOffset: 0,
PageSize: 2,
@@ -543,9 +544,9 @@ func TestResourceStore_MultipleIndexes(t *testing.T) {
}
// Test multiple indexes - get all prod env resources in mesh1
- indexes := map[string]string{
- "by-mesh": "mesh1",
- "by-version": "v1",
+ indexes := []index.IndexCondition{
+ {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals},
+ {IndexName: "by-version", Value: "v1", Operator: index.Equals},
}
result, err := store.ListByIndexes(indexes)
assert.NoError(t, err)
@@ -784,3 +785,425 @@ func TestResourceStore_ListIndexFuncValues(t *testing.T) {
assert.Contains(t, values, "active")
assert.Contains(t, values, "inactive")
}
+
+func TestResourceStore_HasPrefixMatch(t *testing.T) {
+ store := NewMemoryResourceStore("TestInstance")
+ err := store.Init(nil)
+ assert.NoError(t, err)
+
+ // Create mock resources with IP-like values for prefix matching
+ mockRes1 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-1",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-1",
+ Labels: map[string]string{
+ "ip": "192.168.1.10",
+ },
+ },
+ }
+
+ mockRes2 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-2",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-2",
+ Labels: map[string]string{
+ "ip": "192.168.1.20",
+ },
+ },
+ }
+
+ mockRes3 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-3",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-3",
+ Labels: map[string]string{
+ "ip": "10.0.0.5",
+ },
+ },
+ }
+
+ mockRes4 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-4",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-4",
+ Labels: map[string]string{
+ "ip": "192.168.2.1",
+ },
+ },
+ }
+
+ // Add indexer
+ indexers := map[string]cache.IndexFunc{
+ "by-ip": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ ip := resource.ResourceMeta().Labels["ip"]
+ return []string{ip}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ assert.NoError(t, err)
+
+ // Add resources
+ err = store.Add(mockRes1)
+ assert.NoError(t, err)
+ err = store.Add(mockRes2)
+ assert.NoError(t, err)
+ err = store.Add(mockRes3)
+ assert.NoError(t, err)
+ err = store.Add(mockRes4)
+ assert.NoError(t, err)
+
+ // Test HasPrefix match: find all IPs starting with "192.168.1."
+ conditions := []index.IndexCondition{
+ {IndexName: "by-ip", Value: "192.168.1.", Operator:
index.HasPrefix},
+ }
+ result, err := store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 2)
+
+ // Verify the correct instances are returned
+ keys := make([]string, len(result))
+ for i, res := range result {
+ keys[i] = res.ResourceKey()
+ }
+ assert.Contains(t, keys, "instance-1")
+ assert.Contains(t, keys, "instance-2")
+
+ // Test HasPrefix match: find all IPs starting with "192.168."
+ conditions = []index.IndexCondition{
+ {IndexName: "by-ip", Value: "192.168.", Operator:
index.HasPrefix},
+ }
+ result, err = store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 3)
+
+ keys = make([]string, len(result))
+ for i, res := range result {
+ keys[i] = res.ResourceKey()
+ }
+ assert.Contains(t, keys, "instance-1")
+ assert.Contains(t, keys, "instance-2")
+ assert.Contains(t, keys, "instance-4")
+
+ // Test HasPrefix match: find all IPs starting with "10."
+ conditions = []index.IndexCondition{
+ {IndexName: "by-ip", Value: "10.", Operator: index.HasPrefix},
+ }
+ result, err = store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 1)
+ assert.Equal(t, "instance-3", result[0].ResourceKey())
+}
+
+func TestResourceStore_HasPrefixPageList(t *testing.T) {
+ store := NewMemoryResourceStore("TestInstance")
+ err := store.Init(nil)
+ assert.NoError(t, err)
+
+ // Create mock resources
+ mockRes1 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-1",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-1",
+ Labels: map[string]string{
+ "ip": "192.168.1.10",
+ },
+ },
+ }
+
+ mockRes2 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-2",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-2",
+ Labels: map[string]string{
+ "ip": "192.168.1.20",
+ },
+ },
+ }
+
+ mockRes3 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-3",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-3",
+ Labels: map[string]string{
+ "ip": "192.168.1.30",
+ },
+ },
+ }
+
+ // Add indexer
+ indexers := map[string]cache.IndexFunc{
+ "by-ip": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ ip := resource.ResourceMeta().Labels["ip"]
+ return []string{ip}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ assert.NoError(t, err)
+
+ // Add resources
+ err = store.Add(mockRes1)
+ assert.NoError(t, err)
+ err = store.Add(mockRes2)
+ assert.NoError(t, err)
+ err = store.Add(mockRes3)
+ assert.NoError(t, err)
+
+ // Test HasPrefix with pagination
+ conditions := []index.IndexCondition{
+ {IndexName: "by-ip", Value: "192.168.1.", Operator:
index.HasPrefix},
+ }
+ pageReq := model.PageReq{
+ PageOffset: 0,
+ PageSize: 2,
+ }
+
+ pageData, err := store.PageListByIndexes(conditions, pageReq)
+ assert.NoError(t, err)
+ assert.Equal(t, 3, pageData.Total)
+ assert.Equal(t, 0, pageData.PageOffset)
+ assert.Equal(t, 2, pageData.PageSize)
+ assert.Len(t, pageData.Data, 2)
+
+ // Test second page
+ pageReq.PageOffset = 2
+ pageData, err = store.PageListByIndexes(conditions, pageReq)
+ assert.NoError(t, err)
+ assert.Equal(t, 3, pageData.Total)
+ assert.Equal(t, 2, pageData.PageOffset)
+ assert.Len(t, pageData.Data, 1)
+}
+
+func TestResourceStore_HasPrefixWithMultipleConditions(t *testing.T) {
+ store := NewMemoryResourceStore("TestInstance")
+ err := store.Init(nil)
+ assert.NoError(t, err)
+
+ // Create mock resources
+ mockRes1 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-1",
+ mesh: "mesh1",
+ meta: metav1.ObjectMeta{
+ Name: "instance-1",
+ Labels: map[string]string{
+ "ip": "192.168.1.10",
+ },
+ },
+ }
+
+ mockRes2 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-2",
+ mesh: "mesh1",
+ meta: metav1.ObjectMeta{
+ Name: "instance-2",
+ Labels: map[string]string{
+ "ip": "192.168.1.20",
+ },
+ },
+ }
+
+ mockRes3 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-3",
+ mesh: "mesh2",
+ meta: metav1.ObjectMeta{
+ Name: "instance-3",
+ Labels: map[string]string{
+ "ip": "192.168.1.30",
+ },
+ },
+ }
+
+ // Add indexers
+ indexers := map[string]cache.IndexFunc{
+ "by-ip": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ ip := resource.ResourceMeta().Labels["ip"]
+ return []string{ip}, nil
+ },
+ "by-mesh": func(obj interface{}) ([]string, error) {
+ return []string{obj.(model.Resource).ResourceMesh()},
nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ assert.NoError(t, err)
+
+ // Add resources
+ err = store.Add(mockRes1)
+ assert.NoError(t, err)
+ err = store.Add(mockRes2)
+ assert.NoError(t, err)
+ err = store.Add(mockRes3)
+ assert.NoError(t, err)
+
+ // Test combined: HasPrefix on IP AND Equals on mesh
+ conditions := []index.IndexCondition{
+ {IndexName: "by-ip", Value: "192.168.1.", Operator:
index.HasPrefix},
+ {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals},
+ }
+ result, err := store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 2)
+
+ keys := make([]string, len(result))
+ for i, res := range result {
+ keys[i] = res.ResourceKey()
+ }
+ assert.Contains(t, keys, "instance-1")
+ assert.Contains(t, keys, "instance-2")
+ assert.NotContains(t, keys, "instance-3")
+}
+
+func TestResourceStore_HasPrefixAfterUpdate(t *testing.T) {
+ store := NewMemoryResourceStore("TestInstance")
+ err := store.Init(nil)
+ assert.NoError(t, err)
+
+ // Create initial mock resource
+ mockRes1 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-1",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-1",
+ Labels: map[string]string{
+ "ip": "192.168.1.10",
+ },
+ },
+ }
+
+ // Add indexer
+ indexers := map[string]cache.IndexFunc{
+ "by-ip": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ ip := resource.ResourceMeta().Labels["ip"]
+ return []string{ip}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ assert.NoError(t, err)
+
+ // Add resource
+ err = store.Add(mockRes1)
+ assert.NoError(t, err)
+
+ // Verify it matches 192.168.1.
+ conditions := []index.IndexCondition{
+ {IndexName: "by-ip", Value: "192.168.1.", Operator:
index.HasPrefix},
+ }
+ result, err := store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 1)
+
+ // Update resource with different IP
+ updatedRes := &mockResource{
+ kind: "TestInstance",
+ key: "instance-1",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-1",
+ Labels: map[string]string{
+ "ip": "10.0.0.5",
+ },
+ },
+ }
+ err = store.Update(updatedRes)
+ assert.NoError(t, err)
+
+ // Verify it no longer matches 192.168.1.
+ result, err = store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 0)
+
+ // Verify it matches 10.0.
+ conditions = []index.IndexCondition{
+ {IndexName: "by-ip", Value: "10.0.", Operator: index.HasPrefix},
+ }
+ result, err = store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 1)
+ assert.Equal(t, "instance-1", result[0].ResourceKey())
+}
+
+func TestResourceStore_HasPrefixAfterDelete(t *testing.T) {
+ store := NewMemoryResourceStore("TestInstance")
+ err := store.Init(nil)
+ assert.NoError(t, err)
+
+ // Create mock resources
+ mockRes1 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-1",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-1",
+ Labels: map[string]string{
+ "ip": "192.168.1.10",
+ },
+ },
+ }
+
+ mockRes2 := &mockResource{
+ kind: "TestInstance",
+ key: "instance-2",
+ mesh: "default",
+ meta: metav1.ObjectMeta{
+ Name: "instance-2",
+ Labels: map[string]string{
+ "ip": "192.168.1.20",
+ },
+ },
+ }
+
+ // Add indexer
+ indexers := map[string]cache.IndexFunc{
+ "by-ip": func(obj interface{}) ([]string, error) {
+ resource := obj.(model.Resource)
+ ip := resource.ResourceMeta().Labels["ip"]
+ return []string{ip}, nil
+ },
+ }
+ err = store.AddIndexers(indexers)
+ assert.NoError(t, err)
+
+ // Add resources
+ err = store.Add(mockRes1)
+ assert.NoError(t, err)
+ err = store.Add(mockRes2)
+ assert.NoError(t, err)
+
+ // Verify both match 192.168.1.
+ conditions := []index.IndexCondition{
+ {IndexName: "by-ip", Value: "192.168.1.", Operator:
index.HasPrefix},
+ }
+ result, err := store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 2)
+
+ // Delete one resource
+ err = store.Delete(mockRes1)
+ assert.NoError(t, err)
+
+ // Verify only one still matches
+ result, err = store.ListByIndexes(conditions)
+ assert.NoError(t, err)
+ assert.Len(t, result, 1)
+ assert.Equal(t, "instance-2", result[0].ResourceKey())
+}