Skip to content

Implement lazy loading for chaincode lifecycle cache #5258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions core/chaincode/lifecycle/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
Definition *ChaincodeDefinition
Approved bool
InstallInfo *ChaincodeInstallInfo
PackageID string // <-- Add this field

// Hashes is the list of hashed keys in the implicit collection referring to this definition.
// These hashes are determined by the current sequence number of chaincode definition. When dirty,
Expand Down Expand Up @@ -86,6 +87,9 @@
MetadataHandler MetadataHandler

chaincodeCustodian *ChaincodeCustodian

// lazyLoadEnabled controls whether chaincode info is loaded on-demand instead of pre-initialized
lazyLoadEnabled bool
}

type LocalChaincode struct {
Expand Down Expand Up @@ -123,7 +127,7 @@
return references
}

func NewCache(resources *Resources, myOrgMSPID string, metadataManager MetadataHandler, custodian *ChaincodeCustodian, ebMetadata *externalbuilder.MetadataProvider) *Cache {
func NewCache(resources *Resources, myOrgMSPID string, metadataManager MetadataHandler, custodian *ChaincodeCustodian, ebMetadata *externalbuilder.MetadataProvider, lazyLoadEnabled bool) *Cache {
return &Cache{
chaincodeCustodian: custodian,
definedChaincodes: map[string]*ChannelCache{},
Expand All @@ -132,6 +136,7 @@
MyOrgMSPID: myOrgMSPID,
eventBroker: NewEventBroker(resources.ChaincodeStore, resources.PackageParser, ebMetadata),
MetadataHandler: metadataManager,
lazyLoadEnabled: lazyLoadEnabled,
}
}

Expand All @@ -140,6 +145,12 @@
// this would be part of the constructor, but, we cannot rely on the chaincode store being created
// before the cache is created.
func (c *Cache) InitializeLocalChaincodes() error {
// Skip initialization if lazy loading is enabled
if c.lazyLoadEnabled {
logger.Infof("Skipping pre-initialization of local chaincodes due to lazy loading being enabled")
return nil
}

c.mutex.Lock()
defer c.mutex.Unlock()
ccPackages, err := c.Resources.ChaincodeStore.ListInstalledChaincodes()
Expand Down Expand Up @@ -180,6 +191,37 @@
return nil
}

// loadChaincodeInfoOnDemand loads chaincode info for a given packageID when lazy loading is enabled
func (c *Cache) loadChaincodeInfoOnDemand(packageID string) (*ChaincodeInstallInfo, error) {
if !c.lazyLoadEnabled {
return nil, nil
}

// Try to load the chaincode package
ccPackageBytes, err := c.Resources.ChaincodeStore.Load(packageID)
if err != nil {
if _, ok := err.(*persistence.CodePackageNotFoundErr); ok {
// Chaincode not found, return nil to indicate it's not installed
return nil, nil
}
return nil, errors.WithMessagef(err, "could not load chaincode with package ID '%s'", packageID)
}

// Parse the chaincode package
parsedCCPackage, err := c.Resources.PackageParser.Parse(ccPackageBytes)
if err != nil {
return nil, errors.WithMessagef(err, "could not parse chaincode with package ID '%s'", packageID)
}

// Create and return the install info
return &ChaincodeInstallInfo{
PackageID: packageID,
Type: parsedCCPackage.Metadata.Type,
Path: parsedCCPackage.Metadata.Path,
Label: parsedCCPackage.Metadata.Label,
}, nil
}

// Name returns the name of the listener
func (c *Cache) Name() string {
return "lifecycle cache listener"
Expand Down Expand Up @@ -347,17 +389,56 @@
}

c.mutex.RLock()
defer c.mutex.RUnlock()
channelChaincodes, ok := c.definedChaincodes[channelID]
if !ok {
c.mutex.RUnlock()
return nil, errors.Errorf("unknown channel '%s'", channelID)
}

cachedChaincode, ok := channelChaincodes.Chaincodes[name]
if !ok {
c.mutex.RUnlock()
return nil, errors.Errorf("unknown chaincode '%s' for channel '%s'", name, channelID)
}

// If InstallInfo is nil and lazy loading is enabled, try to load it on-demand
if cachedChaincode.InstallInfo == nil && c.lazyLoadEnabled {
if cachedChaincode.PackageID == "" {
logger.Warnf("[ChaincodeInfo] PackageID is empty for chaincode '%s' on channel '%s' (lazy loading enabled)", name, channelID)
} else {
logger.Warnf("[ChaincodeInfo] Attempting lazy load for chaincode '%s' on channel '%s' with PackageID '%s'", name, channelID, cachedChaincode.PackageID)
}
}

c.mutex.RUnlock()

// We need to acquire a write lock to update the cache
c.mutex.Lock()
defer c.mutex.Unlock()

// Re-check the condition after acquiring write lock
if cachedChaincode.InstallInfo == nil && cachedChaincode.PackageID != "" {
// Load the chaincode info on-demand using the stored PackageID
installInfo, err := c.loadChaincodeInfoOnDemand(cachedChaincode.PackageID)
if err != nil {
logger.Debugf("Could not load chaincode info for package ID '%s': %v", cachedChaincode.PackageID, err)
} else if installInfo != nil {
// Update the cache with the loaded install info
cachedChaincode.InstallInfo = installInfo

// Also update the localChaincode entry if it exists
encodedCCHash := protoutil.MarshalOrPanic(&lb.StateData{
Type: &lb.StateData_String_{String_: cachedChaincode.PackageID},
})
hashOfCCHash := string(util.ComputeSHA256(encodedCCHash))

Check failure on line 433 in core/chaincode/lifecycle/cache.go

View workflow job for this annotation

GitHub Actions / Basic Checks

m[string(key)] would be more efficient than k := string(key); m[k] (SA6001)
if localChaincode, exists := c.localChaincodes[hashOfCCHash]; exists {
localChaincode.Info = installInfo
}

logger.Warnf("Lazy loaded chaincode info for '%s' on channel '%s' with package ID '%s'", name, channelID, installInfo.PackageID)
}
}

return &LocalChaincodeInfo{
Definition: cachedChaincode.Definition,
InstallInfo: cachedChaincode.InstallInfo,
Expand Down Expand Up @@ -559,6 +640,19 @@
continue
}

// After confirming isLocalPackage, extract the actual PackageID from the private state and store it in the cache
packageID, err := c.Resources.Serializer.DeserializeFieldAsString(ChaincodeSourcesName, privateName, "PackageID", publicState)
if err != nil {
return errors.WithMessagef(err, "could not deserialize PackageID for '%s' on channel '%s'", name, channelID)
}
cachedChaincode.PackageID = packageID

if packageID == "" {
logger.Warnf("[update] Extracted PackageID is empty for chaincode '%s' on channel '%s' (privateName: %s)", name, channelID, privateName)
} else {
logger.Warnf("[update] Extracted PackageID '%s' for chaincode '%s' on channel '%s' (privateName: %s)", packageID, name, channelID, privateName)
}

cachedChaincode.InstallInfo = localChaincode.Info
if localChaincode.Info != nil {
logger.Infof("Chaincode with package ID '%s' now available on channel %s for chaincode definition %s:%s", localChaincode.Info.PackageID, channelID, name, cachedChaincode.Definition.EndorsementInfo.Version)
Expand Down
154 changes: 153 additions & 1 deletion core/chaincode/lifecycle/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ var _ = Describe("Cache", func() {
chaincodeCustodian = lifecycle.NewChaincodeCustodian()

var err error
c = lifecycle.NewCache(resources, "my-mspid", fakeMetadataHandler, chaincodeCustodian, &externalbuilder.MetadataProvider{})
c = lifecycle.NewCache(resources, "my-mspid", fakeMetadataHandler, chaincodeCustodian, &externalbuilder.MetadataProvider{}, false)
Expect(err).NotTo(HaveOccurred())

channelCache = &lifecycle.ChannelCache{
Expand Down Expand Up @@ -398,6 +398,158 @@ var _ = Describe("Cache", func() {
})
})

Describe("Lazy Loading", func() {
var lazyCache *lifecycle.Cache

BeforeEach(func() {
lazyCache = lifecycle.NewCache(resources, "my-mspid", fakeMetadataHandler, chaincodeCustodian, &externalbuilder.MetadataProvider{}, true)
})

AfterEach(func() {
chaincodeCustodian.Close()
})

Describe("InitializeLocalChaincodes with lazy loading enabled", func() {
It("skips pre-initialization when lazy loading is enabled", func() {
err := lazyCache.InitializeLocalChaincodes()
Expect(err).NotTo(HaveOccurred())

// Verify that ListInstalledChaincodes was not called
Expect(fakeCCStore.ListInstalledChaincodesCallCount()).To(Equal(0))
})

It("logs that lazy loading is enabled", func() {
err := lazyCache.InitializeLocalChaincodes()
Expect(err).NotTo(HaveOccurred())
// Note: In a real test environment, we would verify the log message
// For now, we just verify the method completes successfully
})
})

Describe("Cache behavior with lazy loading enabled", func() {
It("initializes cache without pre-loading chaincodes", func() {
// Verify that the cache is created successfully
Expect(lazyCache).NotTo(BeNil())

// Verify that no chaincodes are pre-loaded
installedChaincodes := lazyCache.ListInstalledChaincodes()
Expect(installedChaincodes).To(HaveLen(0))
})

It("handles chaincode installation events normally", func() {
// Install a chaincode after cache creation
lazyCache.HandleChaincodeInstalled(&persistence.ChaincodePackageMetadata{
Type: "golang",
Path: "github.com/example/chaincode",
Label: "test-label",
}, "test-package-id")

// Verify the chaincode is now available
installedChaincodes := lazyCache.ListInstalledChaincodes()
Expect(installedChaincodes).To(HaveLen(1))
Expect(installedChaincodes[0].PackageID).To(Equal("test-package-id"))
})
})

Describe("ListInstalledChaincodes with lazy loading", func() {
It("returns only installed chaincodes when lazy loading is enabled", func() {
// Set up local chaincodes with some having Info and some not
localChaincodes := map[string]*lifecycle.LocalChaincode{
"hash1": {
Info: &lifecycle.ChaincodeInstallInfo{
PackageID: "installed-package-1",
Label: "label1",
},
References: map[string]map[string]*lifecycle.CachedChaincodeDefinition{},
},
"hash2": {
Info: nil, // Not installed
References: map[string]map[string]*lifecycle.CachedChaincodeDefinition{},
},
"hash3": {
Info: &lifecycle.ChaincodeInstallInfo{
PackageID: "installed-package-2",
Label: "label2",
},
References: map[string]map[string]*lifecycle.CachedChaincodeDefinition{},
},
}

lifecycle.SetLocalChaincodesMap(lazyCache, localChaincodes)

installedChaincodes := lazyCache.ListInstalledChaincodes()
Expect(installedChaincodes).To(HaveLen(2))
Expect(installedChaincodes[0].PackageID).To(Equal("installed-package-1"))
Expect(installedChaincodes[1].PackageID).To(Equal("installed-package-2"))
})
})

Describe("GetInstalledChaincode with lazy loading", func() {
It("returns installed chaincode when found", func() {
localChaincodes := map[string]*lifecycle.LocalChaincode{
"hash1": {
Info: &lifecycle.ChaincodeInstallInfo{
PackageID: "test-package",
Label: "test-label",
},
References: map[string]map[string]*lifecycle.CachedChaincodeDefinition{},
},
}

lifecycle.SetLocalChaincodesMap(lazyCache, localChaincodes)

installedChaincode, err := lazyCache.GetInstalledChaincode("test-package")
Expect(err).NotTo(HaveOccurred())
Expect(installedChaincode.PackageID).To(Equal("test-package"))
Expect(installedChaincode.Label).To(Equal("test-label"))
})

It("returns error when chaincode is not installed", func() {
localChaincodes := map[string]*lifecycle.LocalChaincode{
"hash1": {
Info: nil, // Not installed
References: map[string]map[string]*lifecycle.CachedChaincodeDefinition{},
},
}

lifecycle.SetLocalChaincodesMap(lazyCache, localChaincodes)

installedChaincode, err := lazyCache.GetInstalledChaincode("non-existent-package")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("could not find chaincode with package id 'non-existent-package'"))
Expect(installedChaincode).To(BeNil())
})
})

Describe("Performance comparison between lazy and non-lazy loading", func() {
It("demonstrates that lazy loading skips expensive operations at startup", func() {
// Create a cache with lazy loading disabled (default behavior)
nonLazyCache := lifecycle.NewCache(resources, "my-mspid", fakeMetadataHandler, chaincodeCustodian, &externalbuilder.MetadataProvider{}, false)

// Create a new mock for the lazy cache test
newFakeCCStore := &mock.ChaincodeStore{}
newFakeCCStore.ListInstalledChaincodesReturns(nil, nil)

// Create a new lazy cache with the new mock
newLazyCache := lifecycle.NewCache(&lifecycle.Resources{
ChaincodeStore: newFakeCCStore,
PackageParser: resources.PackageParser,
Serializer: resources.Serializer,
}, "my-mspid", fakeMetadataHandler, chaincodeCustodian, &externalbuilder.MetadataProvider{}, true)

// Initialize non-lazy cache - this should call ListInstalledChaincodes
err := nonLazyCache.InitializeLocalChaincodes()
Expect(err).NotTo(HaveOccurred())
Expect(fakeCCStore.ListInstalledChaincodesCallCount()).To(Equal(1))

// Initialize lazy cache - this should NOT call ListInstalledChaincodes
err = newLazyCache.InitializeLocalChaincodes()
Expect(err).NotTo(HaveOccurred())
Expect(newFakeCCStore.ListInstalledChaincodesCallCount()).To(Equal(0))
})
})
})

Describe("Initialize", func() {
BeforeEach(func() {
err := resources.Serializer.Serialize(lifecycle.NamespacesName, "chaincode-name", &lifecycle.ChaincodeDefinition{
Expand Down
10 changes: 10 additions & 0 deletions core/peer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ type Config struct {
// interact with fabric networks

GatewayOptions gatewayconfig.Options

// ----- Lifecycle config -----

// LifecycleLazyLoadEnabled enables lazy loading of chaincode packages instead of
// pre-initializing all installed chaincodes at startup. This can improve startup
// time when there are many installed chaincodes, but may cause slight delays
// when chaincodes are first accessed.
LifecycleLazyLoadEnabled bool
}

// GlobalConfig obtains a set of configuration from viper, build and returns
Expand Down Expand Up @@ -327,6 +335,8 @@ func (c *Config) load() error {
c.StatsdWriteInterval = viper.GetDuration("metrics.statsd.writeInterval")
c.StatsdPrefix = viper.GetString("metrics.statsd.prefix")

c.LifecycleLazyLoadEnabled = viper.GetBool("peer.lifecycle.lazyLoadEnabled")

c.DockerCert = config.GetPath("vm.docker.tls.cert.file")
c.DockerKey = config.GetPath("vm.docker.tls.key.file")
c.DockerCA = config.GetPath("vm.docker.tls.ca.file")
Expand Down
Loading
Loading