[VOL-5594] Reduce the number of kvstore clients
Change-Id: I7747b0d5729dabf97764432592c5984e5b941bfe
Signed-off-by: Abhay Kumar <abhay.kumar@radisys.com>
diff --git a/VERSION b/VERSION
index 521bb4f..7c66fca 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-4.6.26
+4.7.1
diff --git a/go.mod b/go.mod
index 49626c5..3a2a902 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@
github.com/cenkalti/backoff/v3 v3.1.1
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
- github.com/opencord/voltha-lib-go/v7 v7.8.4
+ github.com/opencord/voltha-lib-go/v7 v7.9.1
github.com/opencord/voltha-protos/v5 v5.7.3
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
diff --git a/go.sum b/go.sum
index 62d719a..d72db36 100644
--- a/go.sum
+++ b/go.sum
@@ -158,8 +158,8 @@
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
-github.com/opencord/voltha-lib-go/v7 v7.8.4 h1:RDxyjtoiCk67mx+fTJohB33VEqQLtnExuMb89RWuyAQ=
-github.com/opencord/voltha-lib-go/v7 v7.8.4/go.mod h1:UELc2f28A2tGsZRu5+XVIWSdEYffACLjWycLlf1o/iI=
+github.com/opencord/voltha-lib-go/v7 v7.9.1 h1:qr/LsU1CRtRrN2xPRgNixONqq9kxS+/ji3YDFVeBLA4=
+github.com/opencord/voltha-lib-go/v7 v7.9.1/go.mod h1:UELc2f28A2tGsZRu5+XVIWSdEYffACLjWycLlf1o/iI=
github.com/opencord/voltha-protos/v5 v5.7.3 h1:fE4IwjYQzZ/QdfwUKDCIxirua0++zqu2JgbkKwXm6No=
github.com/opencord/voltha-protos/v5 v5.7.3/go.mod h1:SoKoF6xSY0+v9A//7y2sAX3YgX2Wy4IeBgyMX5n7eOs=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
diff --git a/internal/pkg/core/device_handler.go b/internal/pkg/core/device_handler.go
index 858aa6a..a040099 100755
--- a/internal/pkg/core/device_handler.go
+++ b/internal/pkg/core/device_handler.go
@@ -228,7 +228,9 @@
dh.perPonOnuIndicationChannel = make(map[uint32]onuIndicationChannels)
dh.childAdapterClients = make(map[string]*vgrpc.Client)
dh.cfg = cfg
- dh.kvStore = SetKVClient(ctx, dh.openOLT.KVStoreType, dh.openOLT.KVStoreAddress, dh.device.Id, dh.cm.Backend.PathPrefix)
+ // kvStore backend is shared across the flows, groups and resource manager to store the allocated resources and other relevant information.
+ // The kvstore is initialized with a unique path prefix for each device to avoid any conflict with other devices.
+ dh.kvStore = db.NewBackend(ctx, dh.openOLT.KVStoreType, dh.openOLT.KVStoreAddress, rsrcMgr.KvstoreTimeout, fmt.Sprintf(rsrcMgr.BasePathKvStore, dh.cm.Backend.PathPrefix, dh.device.Id))
if dh.kvStore == nil {
logger.Error(ctx, "Failed to setup KV store")
return nil
@@ -252,55 +254,23 @@
return &dh
}
-func newKVClient(ctx context.Context, storeType string, address string, timeout time.Duration) (kvstore.Client, error) {
- logger.Infow(ctx, "kv-store-type", log.Fields{"store": storeType})
- switch storeType {
- case "etcd":
- return kvstore.NewEtcdClient(ctx, address, timeout, log.FatalLevel)
- case "redis":
- return kvstore.NewRedisClient(address, timeout, false)
- case "redis-sentinel":
- return kvstore.NewRedisClient(address, timeout, true)
- }
- return nil, errors.New("unsupported-kv-store")
-}
-
-// SetKVClient sets the KV client and return a kv backend
-func SetKVClient(ctx context.Context, backend string, addr string, DeviceID string, basePathKvStore string) *db.Backend {
- kvClient, err := newKVClient(ctx, backend, addr, rsrcMgr.KvstoreTimeout)
- if err != nil {
- logger.Fatalw(ctx, "Failed to init KV client\n", log.Fields{"err": err})
- return nil
- }
- kvbackend := &db.Backend{
- Client: kvClient,
- StoreType: backend,
- Address: addr,
- Timeout: rsrcMgr.KvstoreTimeout,
- PathPrefix: fmt.Sprintf(rsrcMgr.BasePathKvStore, basePathKvStore, DeviceID)}
-
- return kvbackend
-}
-
-// CloseKVClient closes open KV clients
-// This method is thread-safe and can be called concurrently
+// CloseKVClient closes the KV client used by this device handler.
+// The kvStore backend is created once in NewDeviceHandler and shared across all
+// per-PON-interface resource managers (dh.resourceMgr) and flow managers
+// (dh.flowMgr). Since they all point to the same underlying KV client, it must
+// be closed exactly once here. Iterating over each resource/flow manager and
+// closing the client per PON interface would double-close the shared client.
func (dh *DeviceHandler) CloseKVClient(ctx context.Context) {
- // Acquire read lock to safely iterate over resourceMgr and flowMgr slices
- // Use RLock since we're only reading the slices, not modifying them
- dh.lockDevice.RLock()
- defer dh.lockDevice.RUnlock()
-
+ if dh.kvStore != nil && dh.kvStore.Client != nil {
+ dh.kvStore.Client.Close(ctx)
+ dh.kvStore.Client = nil
+ }
+ // Drop the resource managers' references to the now-closed shared backend so
+ // any later access does not operate on a closed client.
if dh.resourceMgr != nil {
for _, rscMgr := range dh.resourceMgr {
if rscMgr != nil {
- rscMgr.CloseKVClient(ctx)
- }
- }
- }
- if dh.flowMgr != nil {
- for _, flMgr := range dh.flowMgr {
- if flMgr != nil {
- flMgr.CloseKVClient(ctx)
+ rscMgr.KVStore = nil
}
}
}
@@ -604,13 +574,14 @@
if err = indications.CloseSend(); err != nil {
// Ok to ignore here, because we landed here due to a problem on the stream
// In all probability, the closeSend call may fail
- logger.Debugw(ctx, "error-closing-send stream--error-ignored",
+ logger.Warnw(ctx, "error-closing-send stream--error-ignored",
log.Fields{"err": err,
"device-id": dh.device.Id})
}
if indications, err = dh.startOpenOltIndicationStream(ctx); err != nil {
return err
}
+ logger.Debugw(ctx, "successfully-restarted-indication-stream", log.Fields{"device-id": dh.device.Id})
// once we re-initialized the indication stream, continue to read indications
continue
}
@@ -1353,7 +1324,8 @@
dh.StopAllFlowRoutines(ctx)
}
- dh.CloseKVClient(ctx)
+ // Do NOT close the shared KV client on OLT reboot/re-init
+ // dh.CloseKVClient(ctx)
if err != nil {
return olterrors.NewErrAdapter("populate-device-info-failed", log.Fields{"device-id": dh.device.Id}, err)
@@ -1377,7 +1349,7 @@
// There is only one NNI manager since multiple NNI is not supported for now
for i = 0; i < dh.totalPonPorts+1; i++ {
// Instantiate resource manager
- if dh.resourceMgr[i] = rsrcMgr.NewResourceMgr(ctx, i, dh.device.Id, dh.openOLT.KVStoreAddress, dh.openOLT.KVStoreType, dh.device.Type, dh.deviceInfo, dh.cm.Backend.PathPrefix); dh.resourceMgr[i] == nil {
+ if dh.resourceMgr[i] = rsrcMgr.NewResourceMgr(ctx, i, dh.device.Id, dh.openOLT.KVStoreAddress, dh.openOLT.KVStoreType, dh.device.Type, dh.deviceInfo, dh.cm.Backend.PathPrefix, dh.kvStore, dh.openOLT.PonRsrcMgr, dh.openOLT.PonRsrcMgrTech); dh.resourceMgr[i] == nil {
return olterrors.ErrResourceManagerInstantiating
}
}
@@ -1630,7 +1602,8 @@
// ProxyOmciRequests sends the proxied OMCI message to the target device
func (dh *DeviceHandler) ProxyOmciRequests(ctx context.Context, omciMsgs *ia.OmciMessages) error {
- if DeviceState(dh.device.ConnectStatus) != DeviceState(voltha.ConnectStatus_REACHABLE) {
+ if (DeviceState(dh.device.ConnectStatus) != DeviceState(voltha.ConnectStatus_REACHABLE)) ||
+ (DeviceState(dh.device.OperStatus) != DeviceState(voltha.OperStatus_ACTIVE)) {
return status.Error(codes.Unavailable, "OLT unreachable")
}
if omciMsgs.GetProxyAddress() == nil {
@@ -2670,6 +2643,8 @@
} else {
logger.Debugw(ctx, "successfully-removed-device-from-Resource-manager-KV-store", log.Fields{"device-id": dh.device.Id})
}
+ // Only close the KV client connection in case of 'DeleteDevice', the connection is still being used for 'doStateUp' & 'updateStateRebooted'.
+ dh.CloseKVClient(ctx)
dh.removeOnuIndicationChannels(ctx)
// Reset the state
@@ -2717,6 +2692,7 @@
func (dh *DeviceHandler) cleanupDeviceResources(ctx context.Context) error {
var errs []error
if dh.resourceMgr != nil && dh.totalPonPorts > 0 {
+ logger.Debugw(ctx, "resource cleanup", log.Fields{"device-id": dh.device.Id})
var ponPort uint32
for ponPort = 0; ponPort < dh.totalPonPorts; ponPort++ {
if dh.resourceMgr[ponPort] == nil {
@@ -2752,13 +2728,12 @@
}
if len(errs) == 0 {
+ logger.Debugw(ctx, "resource cleanup completed successfully", log.Fields{"device-id": dh.device.Id})
// Take one final sweep at cleaning up KV store for the OLT device
// Clean everything at <base-path-prefix>/openolt/<device-id>
if err := dh.kvStore.DeleteWithPrefix(ctx, ""); err != nil {
errs = append(errs, err)
}
- logger.Debugw(ctx, "lockDevice for KVStore close client", log.Fields{"deviceID": dh.device.Id})
- dh.CloseKVClient(ctx)
}
/*Delete ONU map for the device*/
@@ -3109,6 +3084,8 @@
if dh.isReadIndicationRoutineActive {
dh.stopIndications <- true
}
+ // Stop the routines that are processing indications as the OLT is unreachable and any further processing of indications will lead to inconsistencies/panics
+ dh.removeOnuIndicationChannels(ctx)
dh.lockDevice.RUnlock()
dh.transitionMap.Handle(ctx, DeviceInit)
}
@@ -3146,7 +3123,8 @@
if dh.isHeartbeatCheckActive {
dh.stopHeartbeatCheck <- true
}
-
+ // Stop the routines that are processing indications as the OLT is unreachable and any further processing of indications will lead to inconsistencies/panics
+ dh.removeOnuIndicationChannels(ctx)
dh.lockDevice.RUnlock()
if err = dh.updateDeviceStateInCore(ctx, &ca.DeviceStateFilter{
diff --git a/internal/pkg/core/device_handler_test.go b/internal/pkg/core/device_handler_test.go
index b4274c9..bb74e35 100644
--- a/internal/pkg/core/device_handler_test.go
+++ b/internal/pkg/core/device_handler_test.go
@@ -160,7 +160,8 @@
ChannelId: 1,
ChannelGroupId: 1,
},
- ConnectStatus: 2,
+ ConnectStatus: cmn.ConnectStatus_REACHABLE,
+ OperStatus: cmn.OperStatus_ACTIVE,
}
mcs := newMockCoreService()
cc := mocks.NewMockCoreClient(mcs)
diff --git a/internal/pkg/core/openolt.go b/internal/pkg/core/openolt.go
index 0dd2325..f85d06d 100644
--- a/internal/pkg/core/openolt.go
+++ b/internal/pkg/core/openolt.go
@@ -24,11 +24,13 @@
"time"
conf "github.com/opencord/voltha-lib-go/v7/pkg/config"
+ "github.com/opencord/voltha-lib-go/v7/pkg/db"
"github.com/opencord/voltha-lib-go/v7/pkg/events/eventif"
vgrpc "github.com/opencord/voltha-lib-go/v7/pkg/grpc"
"github.com/opencord/voltha-lib-go/v7/pkg/log"
"github.com/opencord/voltha-openolt-adapter/internal/pkg/config"
"github.com/opencord/voltha-openolt-adapter/internal/pkg/olterrors"
+ rsrcMgr "github.com/opencord/voltha-openolt-adapter/internal/pkg/resourcemanager"
"github.com/opencord/voltha-protos/v5/go/adapter_service"
"github.com/opencord/voltha-protos/v5/go/common"
ca "github.com/opencord/voltha-protos/v5/go/core_adapter"
@@ -63,6 +65,11 @@
enableGemStats bool
CheckOnuDevExistenceAtOnuDiscovery bool
ForceOnuDiscIndProcessing bool
+ PonRsrcMgr *db.Backend
+ PonRsrcMgrTech *db.Backend
+ TpDefault *db.Backend
+ Tprofiles *db.Backend
+ TpInstances *db.Backend
}
// NewOpenOLT returns a new instance of OpenOLT
@@ -88,6 +95,15 @@
openOLT.rpcTimeout = cfg.RPCTimeout
openOLT.CheckOnuDevExistenceAtOnuDiscovery = cfg.CheckOnuDevExistenceAtOnuDiscovery
openOLT.ForceOnuDiscIndProcessing = cfg.ForceOnuDiscIndProcessing
+ logger.Infow(ctx, "cm-backend-prefix", log.Fields{"prefix": cm.Backend.PathPrefix})
+ // kvStore backends for resource manager and technology profiles. These are used by the device handlers to store the allocated resources and other relevant information.
+ openOLT.PonRsrcMgr = db.NewBackend(ctx, cfg.KVStoreType, cfg.KVStoreAddress, rsrcMgr.KvstoreTimeout, cm.Backend.PathPrefix+"/resource_manager")
+ openOLT.PonRsrcMgrTech = db.NewBackend(ctx, cfg.KVStoreType, cfg.KVStoreAddress, rsrcMgr.KvstoreTimeout, cm.Backend.PathPrefix+"/resource_manager/config")
+ openOLT.TpDefault = db.NewBackend(ctx, cfg.KVStoreType, cfg.KVStoreAddress, rsrcMgr.KvstoreTimeout, cm.Backend.PathPrefix+"/technology_profiles")
+ // Tprofiles uses a fixed global path because tech-profile definitions are shared across all VOLTHA stacks.
+ openOLT.Tprofiles = db.NewBackend(ctx, cfg.KVStoreType, cfg.KVStoreAddress, rsrcMgr.KvstoreTimeout, "service/voltha/technology_profiles")
+ openOLT.TpInstances = db.NewBackend(ctx, cfg.KVStoreType, cfg.KVStoreAddress, rsrcMgr.KvstoreTimeout, cm.Backend.PathPrefix+"/resource_instances")
+
return &openOLT
}
@@ -105,6 +121,14 @@
// Stop the device handlers
oo.stopAllDeviceHandlers(ctx)
+ // Close shared KV store backends and nil out clients to prevent double-close from downstream CloseKVClient calls.
+ for _, backend := range []*db.Backend{oo.PonRsrcMgr, oo.PonRsrcMgrTech, oo.TpDefault, oo.Tprofiles, oo.TpInstances} {
+ if backend != nil && backend.Client != nil {
+ backend.Client.Close(ctx)
+ backend.Client = nil
+ }
+ }
+
// Stop the core grpc client connection
if oo.coreClient != nil {
oo.coreClient.Stop(ctx)
diff --git a/internal/pkg/core/openolt_flowmgr.go b/internal/pkg/core/openolt_flowmgr.go
index 640a00b..6485084 100644
--- a/internal/pkg/core/openolt_flowmgr.go
+++ b/internal/pkg/core/openolt_flowmgr.go
@@ -218,16 +218,9 @@
ponPortIdx uint32 // Pon Port this FlowManager is responsible for
}
-// CloseKVClient closes open KV clients
-func (f *OpenOltFlowMgr) CloseKVClient(ctx context.Context) {
- if f.techprofile != nil {
- f.techprofile.CloseKVClient(ctx)
- }
-}
-
// NewFlowManager creates OpenOltFlowMgr object and initializes the parameters
func NewFlowManager(ctx context.Context, dh *DeviceHandler, rMgr *rsrcMgr.OpenOltResourceMgr, grpMgr *OpenOltGroupMgr, ponPortIdx uint32) *OpenOltFlowMgr {
- logger.Infow(ctx, "initializing-flow-manager", log.Fields{"device-id": dh.device.Id})
+ logger.Infow(ctx, "initializing-flow-manager", log.Fields{"device-id": dh.device.Id, "pon-port-idx": ponPortIdx})
var flowMgr OpenOltFlowMgr
var err error
@@ -261,7 +254,7 @@
// load interface to multicast queue map from kv store
flowMgr.grpMgr.LoadInterfaceToMulticastQueueMap(ctx)
- logger.Debugw(ctx, "initialization-of-flow-manager-success", log.Fields{"device-id": dh.device.Id})
+ logger.Debugw(ctx, "initialization-of-flow-manager-success", log.Fields{"device-id": dh.device.Id, "pon-port-idx": ponPortIdx})
return &flowMgr
}
@@ -996,7 +989,8 @@
if intfID == f.ponPortIdx { // initialize only for the pon port that this flow manager is managing
var err error
f.techprofile, err = tp.NewTechProfile(ctx, intfID, f.resourceMgr.DeviceID, f.resourceMgr.PonRsrMgr, f.resourceMgr.PonRsrMgr.Backend,
- f.resourceMgr.PonRsrMgr.Address, f.deviceHandler.cm.Backend.PathPrefix)
+ f.resourceMgr.PonRsrMgr.Address, f.deviceHandler.cm.Backend.PathPrefix, f.deviceHandler.openOLT.TpDefault, f.deviceHandler.openOLT.Tprofiles,
+ f.deviceHandler.openOLT.TpInstances)
if err != nil || f.techprofile == nil {
logger.Errorw(ctx, "failed-to-allocate-to-techprofile-for-pon-port", log.Fields{"intfID": intfID, "err": err})
return fmt.Errorf("failed-to-allocate-tech-profile-for-pon-port--pon-%v-err-%v", intfID, err)
diff --git a/internal/pkg/core/openolt_test.go b/internal/pkg/core/openolt_test.go
index 720861e..5c0a508 100644
--- a/internal/pkg/core/openolt_test.go
+++ b/internal/pkg/core/openolt_test.go
@@ -29,6 +29,7 @@
"testing"
conf "github.com/opencord/voltha-lib-go/v7/pkg/config"
+ "github.com/opencord/voltha-lib-go/v7/pkg/db"
vgrpc "github.com/opencord/voltha-lib-go/v7/pkg/grpc"
"github.com/opencord/voltha-protos/v5/go/openolt"
"github.com/stretchr/testify/assert"
@@ -93,6 +94,7 @@
}
func TestNewOpenOLT(t *testing.T) {
+ mockCm := &conf.ConfigManager{Backend: &db.Backend{PathPrefix: "service/voltha"}}
tests := []struct {
name string
fields *fields
@@ -100,9 +102,9 @@
cm *conf.ConfigManager
want *OpenOLT
}{
- {"newopenolt-1", &fields{}, &config.AdapterFlags{OnuNumber: 1, KVStoreAddress: "1.1.1.1:1", KVStoreType: "etcd"}, &conf.ConfigManager{},
+ {"newopenolt-1", &fields{}, &config.AdapterFlags{OnuNumber: 1, KVStoreAddress: "1.1.1.1:1", KVStoreType: "etcd"}, mockCm,
&OpenOLT{numOnus: 1, KVStoreAddress: "1.1.1.1:1", KVStoreType: "etcd"}},
- {"newopenolt-2", &fields{}, &config.AdapterFlags{OnuNumber: 2, KVStoreAddress: "2.2.2.2:2", KVStoreType: "etcd"}, &conf.ConfigManager{},
+ {"newopenolt-2", &fields{}, &config.AdapterFlags{OnuNumber: 2, KVStoreAddress: "2.2.2.2:2", KVStoreType: "etcd"}, mockCm,
&OpenOLT{numOnus: 2, KVStoreAddress: "2.2.2.2:2", KVStoreType: "etcd"}},
}
for _, tt := range tests {
diff --git a/internal/pkg/resourcemanager/resourcemanager.go b/internal/pkg/resourcemanager/resourcemanager.go
index 96245ca..da46408 100644
--- a/internal/pkg/resourcemanager/resourcemanager.go
+++ b/internal/pkg/resourcemanager/resourcemanager.go
@@ -209,21 +209,10 @@
return kvbackend
}
-// CloseKVClient closes open KV clients
-func (rsrcMgr *OpenOltResourceMgr) CloseKVClient(ctx context.Context) {
- if rsrcMgr.KVStore != nil {
- rsrcMgr.KVStore.Client.Close(ctx)
- rsrcMgr.KVStore = nil
- }
- if rsrcMgr.PonRsrMgr != nil {
- rsrcMgr.PonRsrMgr.CloseKVClient(ctx)
- }
-}
-
// NewResourceMgr init a New resource manager instance which in turn instantiates pon resource manager
// instances according to technology. Initializes the default resource ranges for all
// the resources.
-func NewResourceMgr(ctx context.Context, PonIntfID uint32, deviceID string, KVStoreAddress string, kvStoreType string, deviceType string, devInfo *openolt.DeviceInfo, basePathKvStore string) *OpenOltResourceMgr {
+func NewResourceMgr(ctx context.Context, PonIntfID uint32, deviceID string, KVStoreAddress string, kvStoreType string, deviceType string, devInfo *openolt.DeviceInfo, basePathKvStore string, kvStore *db.Backend, ponrsrcmgr *db.Backend, ponmgrTech *db.Backend) *OpenOltResourceMgr {
var ResourceMgr OpenOltResourceMgr
logger.Debugf(ctx, "Init new resource manager , ponIf: %v, address: %s, device-id: %s", PonIntfID, KVStoreAddress, deviceID)
ResourceMgr.PonIntfID = PonIntfID
@@ -232,8 +221,7 @@
ResourceMgr.DeviceType = deviceType
ResourceMgr.DevInfo = devInfo
- Backend := kvStoreType
- ResourceMgr.KVStore = SetKVClient(ctx, Backend, ResourceMgr.Address, deviceID, basePathKvStore)
+ ResourceMgr.KVStore = kvStore
if ResourceMgr.KVStore == nil {
logger.Error(ctx, "Failed to setup KV store")
}
@@ -249,7 +237,7 @@
logger.Debugf(ctx, "Device info technology %s, intf-id %v", technology, PonIntfID)
rsrMgr, err := ponrmgr.NewPONResourceManager(ctx, technology, deviceType, deviceID,
- Backend, ResourceMgr.Address, basePathKvStore)
+ kvStoreType, ResourceMgr.Address, basePathKvStore, ponrsrcmgr, ponmgrTech)
if err != nil {
logger.Errorf(ctx, "Failed to create pon resource manager instance for technology %s", technology)
return nil
diff --git a/internal/pkg/resourcemanager/resourcemanager_test.go b/internal/pkg/resourcemanager/resourcemanager_test.go
index 4763a31..6eb058f 100644
--- a/internal/pkg/resourcemanager/resourcemanager_test.go
+++ b/internal/pkg/resourcemanager/resourcemanager_test.go
@@ -335,7 +335,8 @@
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
- if got := NewResourceMgr(ctx, tt.args.intfID, tt.args.deviceID, tt.args.KVStoreAddress, tt.args.kvStoreType, tt.args.deviceType, tt.args.devInfo, tt.args.kvStorePrefix); reflect.TypeOf(got) != reflect.TypeOf(tt.want) {
+ mockBackend := &db.Backend{Client: &MockResKVClient{}}
+ if got := NewResourceMgr(ctx, tt.args.intfID, tt.args.deviceID, tt.args.KVStoreAddress, tt.args.kvStoreType, tt.args.deviceType, tt.args.devInfo, tt.args.kvStorePrefix, mockBackend, &db.Backend{Client: &MockResKVClient{}}, &db.Backend{Client: &MockResKVClient{}}); reflect.TypeOf(got) != reflect.TypeOf(tt.want) {
t.Errorf("NewResourceMgr() = %v, want %v", got, tt.want)
}
})
diff --git a/pkg/mocks/mockEventproxy.go b/pkg/mocks/mockEventproxy.go
index f0e8a5d..43d0489 100644
--- a/pkg/mocks/mockEventproxy.go
+++ b/pkg/mocks/mockEventproxy.go
@@ -47,6 +47,15 @@
return nil
}
+// SendKpiEvent3 mocks the SendKpiEvent3 function
+func (me *MockEventProxy) SendKpiEvent3(ctx context.Context, id string, deviceEvent *voltha.KpiEvent3, category voltha.EventCategory_Types,
+ subCategory voltha.EventSubCategory_Types, raisedTs int64) error {
+ if raisedTs == 0 {
+ return errors.New("raisedTS cannot be zero")
+ }
+ return nil
+}
+
// SendRPCEvent mocks the SendRPCEvent function
func (me *MockEventProxy) SendRPCEvent(ctx context.Context, id string, deviceEvent *voltha.RPCEvent, category voltha.EventCategory_Types,
subCategory *voltha.EventSubCategory_Types, raisedTs int64) error {
@@ -80,12 +89,3 @@
subCategory eventif.EventSubCategory, raisedTs int64, key string) error {
return nil
}
-
-// SendKpiEvent3 mocks the SendKpiEvent3 function
-func (me *MockEventProxy) SendKpiEvent3(ctx context.Context, id string, deviceEvent *voltha.KpiEvent3, category eventif.EventCategory,
- subCategory eventif.EventSubCategory, raisedTs int64) error {
- if raisedTs == 0 {
- return errors.New("raisedTS cannot be zero")
- }
- return nil
-}
diff --git a/vendor/github.com/opencord/voltha-lib-go/v7/pkg/db/kvstore/redisclient.go b/vendor/github.com/opencord/voltha-lib-go/v7/pkg/db/kvstore/redisclient.go
index 3a7e512..f27fc06 100644
--- a/vendor/github.com/opencord/voltha-lib-go/v7/pkg/db/kvstore/redisclient.go
+++ b/vendor/github.com/opencord/voltha-lib-go/v7/pkg/db/kvstore/redisclient.go
@@ -33,6 +33,7 @@
watchedChannels sync.Map
writeLock sync.Mutex
keyReservationsLock sync.RWMutex
+ pageSize int
}
func NewRedisClient(addr string, timeout time.Duration, useSentinel bool) (*RedisClient, error) {
@@ -49,7 +50,7 @@
}
reservations := make(map[string]time.Duration)
- return &RedisClient{redisAPI: r, keyReservations: reservations}, nil
+ return &RedisClient{redisAPI: r, keyReservations: reservations, pageSize: 5000}, nil
}
func (c *RedisClient) Get(ctx context.Context, key string) (*KVPair, error) {
@@ -62,8 +63,12 @@
return NewKVPair(key, valBytes, "", 0, 0), nil
}
-func (c *RedisClient) Put(ctx context.Context, key string, value interface{}) error {
+const (
+ // Sorted set name for storing all keys
+ keysSetName = "voltha:keys"
+)
+func (c *RedisClient) Put(ctx context.Context, key string, value interface{}) error {
// Validate that we can convert value to a string as etcd API expects a string
var val string
var er error
@@ -71,53 +76,197 @@
return fmt.Errorf("unexpected-type-%T", value)
}
- // Check if there is already a lease for this key - if there is then use it, otherwise a PUT will make
- // that KV key permanent instead of automatically removing it after a lease expiration
- setErr := c.redisAPI.Set(ctx, key, val, 0)
- err := setErr.Err()
+ // Use a pipeline for atomic operations
+ pipe := c.redisAPI.TxPipeline()
+ // Set the key-value pair
+ pipe.Set(ctx, key, val, 0)
+
+ // Add key to sorted set with score 0 for efficient prefix queries
+ pipe.ZAdd(ctx, keysSetName, &redis.Z{
+ Score: 0,
+ Member: key,
+ })
+
+ // Execute pipeline
+ cmds, err := pipe.Exec(ctx)
if err != nil {
- switch setErr.Err() {
+ switch err {
case context.Canceled:
- logger.Warnw(ctx, "context-cancelled", log.Fields{"error": err})
+ logger.Warnw(ctx, "redis put failed : context-cancelled", log.Fields{"error": err, "key": key})
case context.DeadlineExceeded:
- logger.Warnw(ctx, "context-deadline-exceeded", log.Fields{"error": err})
+ logger.Warnw(ctx, "redis put failed : context-deadline-exceeded", log.Fields{"error": err, "key": key})
default:
- logger.Warnw(ctx, "bad-endpoints", log.Fields{"error": err})
+ logger.Warnw(ctx, "redis put failed : bad-endpoints", log.Fields{"error": err, "key": key})
}
return err
}
+
+ // Check individual command errors
+ for _, cmd := range cmds {
+ if cmd.Err() != nil {
+ logger.Warnw(ctx, "redis-pipeline-command-failed", log.Fields{"error": cmd.Err(), "key": key, "Cmd": cmd.String()})
+ return cmd.Err()
+ }
+ }
return nil
}
-func (c *RedisClient) scanAllKeysWithPrefix(ctx context.Context, key string) ([]string, error) {
- var err error
- allkeys := []string{}
- cont := true
- cursor := uint64(0)
- matchPrefix := key + "*"
+func (c *RedisClient) Delete(ctx context.Context, key string) error {
+ // Use a pipeline for atomic operations
+ pipe := c.redisAPI.TxPipeline()
- for cont {
- // search in the first 10000 entries starting from the point indicated by the cursor
- logger.Debugw(ctx, "redis-scan", log.Fields{"matchPrefix": matchPrefix, "cursor": cursor})
- var keys []string
- keys, cursor, err = c.redisAPI.Scan(context.Background(), cursor, matchPrefix, 10000).Result()
+ // Delete the key
+ pipe.Del(ctx, key)
+
+ // Remove key from sorted set
+ pipe.ZRem(ctx, keysSetName, key)
+
+ // Execute pipeline
+ cmds, err := pipe.Exec(ctx)
+ if err != nil {
+ logger.Errorw(ctx, "failed-to-delete-key", log.Fields{"key": key, "error": err})
+ return err
+ }
+ // Check individual command errors
+ for _, cmd := range cmds {
+ if cmd.Err() != nil && cmd.Err() != redis.Nil {
+ logger.Warnw(ctx, "redis-pipeline-command-failed", log.Fields{"error": cmd.Err(), "key": key, "Cmd": cmd.String()})
+ return cmd.Err()
+ }
+ }
+
+ logger.Debugw(ctx, "key-deleted", log.Fields{"key": key})
+ return nil
+}
+
+func (c *RedisClient) DeleteWithPrefix(ctx context.Context, prefixKey string) error {
+ var keys []string
+ var err error
+
+ // Use ZRANGEBYLEX to get keys with prefix efficiently
+ if keys, err = c.getKeysWithPrefixFromSortedSet(ctx, prefixKey); err != nil {
+ return err
+ }
+
+ if len(keys) == 0 {
+ logger.Warn(ctx, "nothing-to-delete-from-kv", log.Fields{"key": prefixKey})
+ return nil
+ }
+
+ // Delete keys in batches using pipeline
+ entryCount := int64(0)
+ start := 0
+ length := len(keys)
+
+ for start < length {
+ end := start + c.pageSize
+ if end >= length {
+ end = length
+ }
+ keysToDelete := keys[start:end]
+
+ pipe := c.redisAPI.TxPipeline()
+
+ // Delete the actual key-value pairs
+ pipe.Del(ctx, keysToDelete...)
+
+ // Remove keys from sorted set
+ members := make([]interface{}, len(keysToDelete))
+ for i, k := range keysToDelete {
+ members[i] = k
+ }
+ pipe.ZRem(ctx, keysSetName, members...)
+
+ // Execute pipeline
+ cmds, err := pipe.Exec(ctx)
+ if err != nil {
+ logger.Errorw(ctx, "DeleteWithPrefix method failed", log.Fields{"prefixKey": prefixKey, "numOfMatchedKeys": len(keysToDelete), "err": err})
+ return err
+ }
+ // Check individual command errors
+ for _, cmd := range cmds {
+ if cmd.Err() != nil && cmd.Err() != redis.Nil {
+ logger.Warnw(ctx, "redis-pipeline-command-failed", log.Fields{"error": cmd.Err(), "key": prefixKey, "Cmd": cmd.String()})
+ return cmd.Err()
+ }
+ }
+
+ // First command is Del, get its result
+ if len(cmds) > 0 {
+ if delCmd, ok := cmds[0].(*redis.IntCmd); ok {
+ count, _ := delCmd.Result()
+ entryCount += count
+ }
+ }
+
+ start = end
+ }
+
+ logger.Debugf(ctx, "%d entries matching with the key prefix %s have been deleted successfully", entryCount, prefixKey)
+ return nil
+}
+
+func (c *RedisClient) GetWithPrefix(ctx context.Context, prefix string) (map[string]*KVPair, error) {
+ var err error
+ var keys []string
+ m := make(map[string]*KVPair)
+ var values []interface{}
+
+ // Use ZRANGEBYLEX to get keys with prefix efficiently
+ if keys, err = c.getKeysWithPrefixFromSortedSet(ctx, prefix); err != nil {
+ return nil, err
+ }
+
+ if len(keys) != 0 {
+ values, err = c.redisAPI.MGet(ctx, keys...).Result()
if err != nil {
return nil, err
}
- if cursor == 0 {
- // all data searched. break the loop
- logger.Debugw(ctx, "redis-scan-ended", log.Fields{"matchPrefix": matchPrefix, "cursor": cursor})
- cont = false
- }
- if len(keys) == 0 {
- // no matched data found in this cycle. Continue to search
- logger.Debugw(ctx, "redis-scan-no-data-found-continue", log.Fields{"matchPrefix": matchPrefix, "cursor": cursor})
- continue
- }
- allkeys = append(allkeys, keys...)
}
- return allkeys, nil
+
+ for i, key := range keys {
+ if valBytes, err := ToByte(values[i]); err == nil {
+ m[key] = NewKVPair(key, interface{}(valBytes), "", 0, 0)
+ }
+ }
+ return m, nil
+}
+
+func (c *RedisClient) GetWithPrefixKeysOnly(ctx context.Context, prefix string) ([]string, error) {
+ // Use ZRANGEBYLEX to get keys with prefix efficiently
+ keys, err := c.getKeysWithPrefixFromSortedSet(ctx, prefix)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get keys with prefix %s: %v", prefix, err)
+ }
+
+ if len(keys) == 0 {
+ logger.Debugw(ctx, "no-keys-found", log.Fields{"prefix": prefix})
+ }
+
+ return keys, nil
+}
+
+// Helper function to retrieve keys with prefix using ZRANGEBYLEX
+func (c *RedisClient) getKeysWithPrefixFromSortedSet(ctx context.Context, prefix string) ([]string, error) {
+ // ZRANGEBYLEX uses lexicographical ordering
+ // min: [prefix - includes prefix
+ // max: [prefix\xff - includes all keys starting with prefix
+ min := "[" + prefix
+ max := "[" + prefix + "\xff"
+
+ keys, err := c.redisAPI.ZRangeByLex(ctx, keysSetName, &redis.ZRangeBy{
+ Min: min,
+ Max: max,
+ }).Result()
+
+ if err != nil {
+ logger.Errorw(ctx, "failed-to-get-keys-with-prefix", log.Fields{"prefix": prefix, "error": err})
+ return nil, err
+ }
+
+ logger.Debugw(ctx, "keys-retrieved-with-prefix", log.Fields{"prefix": prefix, "count": len(keys)})
+ return keys, nil
}
func (c *RedisClient) KeyExists(ctx context.Context, key string) (bool, error) {
@@ -139,7 +288,8 @@
m := make(map[string]*KVPair)
var values []interface{}
- if keys, err = c.scanAllKeysWithPrefix(ctx, key); err != nil {
+ // Use ZRANGEBYLEX to get keys with prefix efficiently
+ if keys, err = c.getKeysWithPrefixFromSortedSet(ctx, key); err != nil {
return nil, err
}
@@ -157,49 +307,6 @@
return m, nil
}
-func (c *RedisClient) Delete(ctx context.Context, key string) error {
- // delete the key
- if _, err := c.redisAPI.Del(ctx, key).Result(); err != nil {
- logger.Errorw(ctx, "failed-to-delete-key", log.Fields{"key": key, "error": err})
- return err
- }
- logger.Debugw(ctx, "key(s)-deleted", log.Fields{"key": key})
- return nil
-}
-
-func (c *RedisClient) DeleteWithPrefix(ctx context.Context, prefixKey string) error {
- var keys []string
- var err error
- if keys, err = c.scanAllKeysWithPrefix(ctx, prefixKey); err != nil {
- return err
- }
- if len(keys) == 0 {
- logger.Warn(ctx, "nothing-to-delete-from-kv", log.Fields{"key": prefixKey})
- return nil
- }
- //call delete for keys
- entryCount := int64(0)
- start := 0
- pageSize := 5000
- length := len(keys)
- for start < length {
- end := start + pageSize
- if end >= length {
- end = length
- }
- keysToDelete := keys[start:end]
- count := int64(0)
- if count, err = c.redisAPI.Del(ctx, keysToDelete...).Result(); err != nil {
- logger.Errorw(ctx, "DeleteWithPrefix method failed", log.Fields{"prefixKey": prefixKey, "numOfMatchedKeys": len(keysToDelete), "err": err})
- return err
- }
- entryCount += count
- start = end
- }
- logger.Debugf(ctx, "%d entries matching with the key prefix %s have been deleted successfully", entryCount, prefixKey)
- return nil
-}
-
func (c *RedisClient) Reserve(ctx context.Context, key string, value interface{}, ttl time.Duration) (interface{}, error) {
var val string
var er error
@@ -428,41 +535,3 @@
logger.Errorw(ctx, "error-closing-client", log.Fields{"error": err})
}
}
-
-func (c *RedisClient) GetWithPrefix(ctx context.Context, prefix string) (map[string]*KVPair, error) {
- var err error
- var keys []string
- m := make(map[string]*KVPair)
- var values []interface{}
-
- if keys, err = c.scanAllKeysWithPrefix(ctx, prefix); err != nil {
- return nil, err
- }
-
- if len(keys) != 0 {
- values, err = c.redisAPI.MGet(ctx, keys...).Result()
- if err != nil {
- return nil, err
- }
- }
- for i, key := range keys {
- if valBytes, err := ToByte(values[i]); err == nil {
- m[key] = NewKVPair(key, interface{}(valBytes), "", 0, 0)
- }
- }
- return m, nil
-}
-
-func (c *RedisClient) GetWithPrefixKeysOnly(ctx context.Context, prefix string) ([]string, error) {
- // Use the scanAllKeysWithPrefix function to fetch keys matching the prefix
- keys, err := c.scanAllKeysWithPrefix(ctx, prefix)
- if err != nil {
- return nil, fmt.Errorf("failed to scan keys with prefix %s: %v", prefix, err)
- }
-
- if len(keys) == 0 {
- logger.Debugw(ctx, "no-keys-found", log.Fields{"prefix": prefix})
- }
-
- return keys, nil
-}
diff --git a/vendor/github.com/opencord/voltha-lib-go/v7/pkg/ponresourcemanager/ponresourcemanager.go b/vendor/github.com/opencord/voltha-lib-go/v7/pkg/ponresourcemanager/ponresourcemanager.go
index 0eec452..0008e00 100644
--- a/vendor/github.com/opencord/voltha-lib-go/v7/pkg/ponresourcemanager/ponresourcemanager.go
+++ b/vendor/github.com/opencord/voltha-lib-go/v7/pkg/ponresourcemanager/ponresourcemanager.go
@@ -192,6 +192,10 @@
return kvbackend
}
+// CloseKVClient closes the KV store connections held by this manager.
+// Note: The KV backends may be shared with other managers (e.g., TechProfileMgr).
+// The nil check and nil assignment ensure that duplicate close calls are safe —
+// once closed by one manager, the other will see nil and skip closing.
func (PONRMgr *PONResourceManager) CloseKVClient(ctx context.Context) {
if PONRMgr.KVStore != nil {
PONRMgr.KVStore.Client.Close(ctx)
@@ -204,20 +208,20 @@
}
// NewPONResourceManager creates a new PON resource manager.
-func NewPONResourceManager(ctx context.Context, Technology string, DeviceType string, DeviceID string, Backend string, Address string, basePathKvStore string) (*PONResourceManager, error) {
+func NewPONResourceManager(ctx context.Context, Technology string, DeviceType string, DeviceID string, Backend string, Address string, basePathKvStore string, ponrsrcmgr *db.Backend, ponmgrTech *db.Backend) (*PONResourceManager, error) {
var PONMgr PONResourceManager
PONMgr.Technology = Technology
PONMgr.DeviceType = DeviceType
PONMgr.DeviceID = DeviceID
PONMgr.Backend = Backend
PONMgr.Address = Address
- PONMgr.KVStore = SetKVClient(ctx, Technology, Backend, Address, false, basePathKvStore)
+ PONMgr.KVStore = ponrsrcmgr
if PONMgr.KVStore == nil {
logger.Error(ctx, "KV Client initilization failed")
return nil, errors.New("failed to init KV client")
}
// init kv client to read from the config path
- PONMgr.KVStoreForConfig = SetKVClient(ctx, Technology, Backend, Address, true, basePathKvStore)
+ PONMgr.KVStoreForConfig = ponmgrTech
if PONMgr.KVStoreForConfig == nil {
logger.Error(ctx, "KV Config Client initilization failed")
return nil, errors.New("failed to init KV Config client")
@@ -990,7 +994,7 @@
}
FlowIDPath := fmt.Sprintf(FLOW_ID_RESOURCE_MAP_PATH, PONRMgr.DeviceID, PONIntfONUID)
- if FlowIDs, err := PONRMgr.KVStore.List(ctx, FlowIDPath); err != nil {
+ if FlowIDs, err := PONRMgr.KVStore.List(ctx, FlowIDPath); err == nil {
for _, Flow := range FlowIDs {
FlowIDInfoPath := fmt.Sprintf(FLOW_ID_INFO_PATH, PONRMgr.DeviceID, PONIntfONUID, Flow.Value)
if err = PONRMgr.KVStore.Delete(ctx, FlowIDInfoPath); err != nil {
diff --git a/vendor/github.com/opencord/voltha-lib-go/v7/pkg/techprofile/tech_profile.go b/vendor/github.com/opencord/voltha-lib-go/v7/pkg/techprofile/tech_profile.go
index 1bfb964..089988d 100644
--- a/vendor/github.com/opencord/voltha-lib-go/v7/pkg/techprofile/tech_profile.go
+++ b/vendor/github.com/opencord/voltha-lib-go/v7/pkg/techprofile/tech_profile.go
@@ -178,6 +178,10 @@
*/
}
+// CloseKVClient closes the KV store connections held by this manager.
+// Note: The KV backends may be shared with other managers (e.g., PONResourceManager).
+// The nil check and nil assignment ensure that duplicate close calls are safe —
+// once closed by one manager, the other will see nil and skip closing.
func (t *TechProfileMgr) CloseKVClient(ctx context.Context) {
if t.config.KVBackend != nil {
t.config.KVBackend.Client.Close(ctx)
@@ -193,17 +197,17 @@
}
}
-func NewTechProfile(ctx context.Context, IntfId uint32, deviceId string, resourceMgr iPonResourceMgr, kvStoreType string, kvStoreAddress string, basePathKvStore string) (*TechProfileMgr, error) {
+func NewTechProfile(ctx context.Context, IntfId uint32, deviceId string, resourceMgr iPonResourceMgr, kvStoreType string, kvStoreAddress string, basePathKvStore string, TpDefault *db.Backend, Tprofiles *db.Backend, TpInstances *db.Backend) (*TechProfileMgr, error) {
var techprofileObj TechProfileMgr
logger.Debug(ctx, "initializing-techprofile-mananger ", log.Fields{"IntId": IntfId, "device-id": deviceId})
techprofileObj.config = NewTechProfileFlags(kvStoreType, kvStoreAddress, basePathKvStore)
- techprofileObj.config.KVBackend = techprofileObj.SetKVClient(ctx, techprofileObj.config.TPKVPathPrefix)
- techprofileObj.config.DefaultTpKVBackend = techprofileObj.SetKVClient(ctx, techprofileObj.config.defaultTpKvPathPrefix)
+ techprofileObj.config.KVBackend = Tprofiles
+ techprofileObj.config.DefaultTpKVBackend = TpDefault
if techprofileObj.config.KVBackend == nil {
logger.Error(ctx, "failed-to-initialize-backend")
return nil, errors.New("kv-backend-init-failed")
}
- techprofileObj.config.ResourceInstanceKVBacked = techprofileObj.SetKVClient(ctx, techprofileObj.config.ResourceInstanceKVPathPrefix)
+ techprofileObj.config.ResourceInstanceKVBacked = TpInstances
if techprofileObj.config.ResourceInstanceKVBacked == nil {
logger.Error(ctx, "failed-to-initialize-resource-instance-kv-backend")
return nil, errors.New("resource-instance-kv-backend-init-failed")
@@ -213,12 +217,12 @@
techprofileObj.eponTpInstanceMap = make(map[string]*tp_pb.EponTechProfileInstance)
techprofileObj.tpMap = make(map[uint32]*tp_pb.TechProfile)
techprofileObj.eponTpMap = make(map[uint32]*tp_pb.EponTechProfile)
- logger.Debug(ctx, "reconcile-tp-instance-cache-start")
+ logger.Debugw(ctx, "reconcile-tp-instance-cache-start", log.Fields{"IntId": IntfId, "device-id": deviceId})
if err := techprofileObj.reconcileTpInstancesToCache(ctx, IntfId, deviceId); err != nil {
logger.Errorw(ctx, "failed-to-reconcile-tp-instances", log.Fields{"err": err})
return nil, err
}
- logger.Debug(ctx, "reconcile-tp-instance-cache-end")
+ logger.Debugw(ctx, "reconcile-tp-instance-cache-end", log.Fields{"IntId": IntfId, "device-id": deviceId})
logger.Debug(ctx, "initializing-tech-profile-manager-object-success")
return &techprofileObj, nil
}
@@ -1388,6 +1392,7 @@
//Fetching the techprofile Keys from the KV Store.
tpkeys, _ := t.config.DefaultTpKVBackend.GetWithPrefixKeysOnly(ctx, tech)
+ logger.Debugw(ctx, "get-all-tech-profile-keys", log.Fields{"len(tpkeys)": len(tpkeys), "tech": tech, "deviceId": deviceId, "IntfId": IntfId})
// Extract the techprofile Ids from the keys
// The tpkeys will be of the format "service/voltha/technology_profiles/GPON/65"
@@ -1414,6 +1419,7 @@
for _, tpId := range tpIds {
prefix := fmt.Sprintf("%s/%d/olt-{%s}/pon-{%d}", tech, tpId, deviceId, IntfId)
kvPairs, _ := t.config.ResourceInstanceKVBacked.GetWithPrefix(newCtx, prefix)
+ logger.Debugw(ctx, "get-resource-instances-with-prefix", log.Fields{"prefix": prefix, "len(kvPairs)": len(kvPairs), "tech": tech, "deviceId": deviceId, "IntfId": IntfId})
//check if KvPairs is empty and if not then reconcile the techprofile instance
if len(kvPairs) > 0 {
for keyPath, kvPair := range kvPairs {
@@ -1467,10 +1473,7 @@
} else {
logger.Errorw(ctx, "error-converting-kv-pair-value-to-byte", log.Fields{"err": err})
}
-
}
-
- return nil
}
}
return nil
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 9f9374f..f7aa113 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -229,7 +229,7 @@
# github.com/olekukonko/tablewriter v0.0.5
## explicit; go 1.12
github.com/olekukonko/tablewriter
-# github.com/opencord/voltha-lib-go/v7 v7.8.4
+# github.com/opencord/voltha-lib-go/v7 v7.9.1
## explicit; go 1.25.5
github.com/opencord/voltha-lib-go/v7/pkg/config
github.com/opencord/voltha-lib-go/v7/pkg/db