From cf50df1108fbac2089462f03286f30fe90dfe8e9 Mon Sep 17 00:00:00 2001 From: YuQiang Date: Wed, 20 Sep 2023 14:07:41 +0800 Subject: [PATCH] feat: handle different plantform in cache remote We can use manifestIndex to record different remote cache on different plantforms. And conversion will use cache which matches current plantfotm. Signed-off-by: YuQiang --- pkg/adapter/adapter.go | 3 - pkg/content/cache.go | 33 ++-- pkg/content/content.go | 4 + pkg/content/local.go | 15 +- pkg/content/provider.go | 8 +- pkg/converter/converter.go | 4 +- pkg/driver/nydus/nydus.go | 339 +++++++++++++++++++++++++++---------- 7 files changed, 282 insertions(+), 124 deletions(-) diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go index b6e22b22..7c7567ba 100644 --- a/pkg/adapter/adapter.go +++ b/pkg/adapter/adapter.go @@ -120,9 +120,6 @@ func (adp *LocalAdapter) Convert(ctx context.Context, source string) (*converter return nil, nil } } - if err = adp.content.NewRemoteCache(cacheRef); err != nil { - return nil, err - } adp.content.GcMutex.RLock() defer adp.content.GcMutex.RUnlock() metric, err := adp.cvt.Convert(ctx, source, target, cacheRef) diff --git a/pkg/content/cache.go b/pkg/content/cache.go index c98282c9..10d40ada 100644 --- a/pkg/content/cache.go +++ b/pkg/content/cache.go @@ -126,9 +126,9 @@ func (leaseCache *leaseCache) Len() int { } type RemoteCache struct { - // remoteCache is an LRU cache for caching target layer descriptors, the cache key is the source layer digest, + // remoteCache is a map for caching target layer descriptors, the cache key is the source layer digest, // and the cache value is the target layer descriptor after conversion. - remoteCache *lru.Cache[string, ocispec.Descriptor] + remoteCache map[string]ocispec.Descriptor // cacheRef is the remote cache reference. cacheRef string // host is a func to provide registry credential by host name. @@ -138,46 +138,43 @@ type RemoteCache struct { } func NewRemoteCache(cacheSize int, host remote.HostFunc) (*RemoteCache, error) { - remoteCache, err := lru.New[string, ocispec.Descriptor](cacheSize) - if err != nil { - return nil, err - } return &RemoteCache{ - remoteCache: remoteCache, + remoteCache: make(map[string]ocispec.Descriptor), host: host, cacheSize: cacheSize, }, nil } func (rc *RemoteCache) Values() []ocispec.Descriptor { - return rc.remoteCache.Values() + var values []ocispec.Descriptor + for _, desc := range rc.remoteCache { + values = append(values, desc) + } + return values } func (rc *RemoteCache) Get(key string) (ocispec.Descriptor, bool) { - return rc.remoteCache.Get(key) + value, ok := rc.remoteCache[key] + return value, ok } func (rc *RemoteCache) Add(key string, value ocispec.Descriptor) { - rc.remoteCache.Add(key, value) + rc.remoteCache[key] = value } func (rc *RemoteCache) Remove(key string) { - rc.remoteCache.Remove(key) + delete(rc.remoteCache, key) } -// Size returns the number of items in the cache. func (rc *RemoteCache) Size() int { - return rc.remoteCache.Len() - + return len(rc.remoteCache) } func (rc *RemoteCache) NewLRUCache(cacheSize int, cacheRef string) error { if rc != nil { - remoteCache, err := lru.New[string, ocispec.Descriptor](cacheSize) - if err != nil { - return err + for k := range rc.remoteCache { + delete(rc.remoteCache, k) } - rc.remoteCache = remoteCache rc.cacheRef = cacheRef } return nil diff --git a/pkg/content/content.go b/pkg/content/content.go index e61cdbcb..f12781f0 100644 --- a/pkg/content/content.go +++ b/pkg/content/content.go @@ -286,6 +286,8 @@ func (content *Content) Info(ctx context.Context, dgst digest.Digest) (ctrconten return info, err } +// Here we use the Update method to update or add a cache layer to the content store. When the remote cache is nil, +// updating to the content will return that the error NotFound, notifying that there is no cache implemented here. func (content *Content) Update(ctx context.Context, info ctrcontent.Info, fieldpaths ...string) (ctrcontent.Info, error) { if content.remoteCache != nil { sourceDesc, ok := info.Labels[nydusutils.LayerAnnotationNydusSourceDigest] @@ -300,6 +302,8 @@ func (content *Content) Update(ctx context.Context, info ctrcontent.Info, fieldp return info, nil } } + // containerd content store write labels to annotate some blobs belong to a same repo, + // cleaning labels is needed by GC if info.Labels != nil { info.Labels = nil } diff --git a/pkg/content/local.go b/pkg/content/local.go index 2582ba86..90088ec2 100644 --- a/pkg/content/local.go +++ b/pkg/content/local.go @@ -38,7 +38,6 @@ type LocalProvider struct { content *Content hosts remote.HostFunc platformMC platforms.MatchComparer - cacheRef string } func NewLocalProvider(cfg *config.Config, platformMC platforms.MatchComparer) (Provider, *Content, error) { @@ -130,14 +129,20 @@ func (pvd *LocalProvider) getImage(ref string) (*ocispec.Descriptor, error) { return nil, errdefs.ErrNotFound } -func (pvd *LocalProvider) SetCacheRef(ref string) { +func (pvd *LocalProvider) ClearCache(ref string) error { pvd.mutex.Lock() defer pvd.mutex.Unlock() - pvd.cacheRef = ref + if err := pvd.content.NewRemoteCache(ref); err != nil { + return errors.Wrap(err, "create new remote cache") + } + return nil } -func (pvd *LocalProvider) GetCacheRef() string { +func (pvd *LocalProvider) GetCacheInfo() (string, int) { pvd.mutex.Lock() defer pvd.mutex.Unlock() - return pvd.cacheRef + if pvd.content.remoteCache != nil { + return pvd.content.remoteCache.cacheRef, pvd.content.remoteCache.cacheSize + } + return "", 0 } diff --git a/pkg/content/provider.go b/pkg/content/provider.go index e11d4d57..bde797e3 100644 --- a/pkg/content/provider.go +++ b/pkg/content/provider.go @@ -42,8 +42,8 @@ type Provider interface { Image(ctx context.Context, ref string) (*ocispec.Descriptor, error) // ContentStore gets the content store object of containerd. ContentStore() content.Store - // SetCacheRef sets the cache reference of the source image. - SetCacheRef(ref string) - // GetCacheRef gets the cache reference of the source image. - GetCacheRef() string + // ClearCache clear the cache in content store and set new cache reference. + ClearCache(ref string) error + // GetCacheInfo gets the cache reference of the source image and cache size. + GetCacheInfo() (string, int) } diff --git a/pkg/converter/converter.go b/pkg/converter/converter.go index 5fe55c15..b07e46d6 100644 --- a/pkg/converter/converter.go +++ b/pkg/converter/converter.go @@ -122,7 +122,9 @@ func (cvt *Converter) Convert(ctx context.Context, source, target, cacheRef stri logger.Infof("converting image %s", source) start = time.Now() - cvt.provider.SetCacheRef(cacheRef) + if err := cvt.provider.ClearCache(cacheRef); err != nil { + return nil, errors.Wrap(err, "clear cache") + } desc, err := cvt.driver.Convert(ctx, cvt.provider, source) if err != nil { return nil, errors.Wrap(err, "convert image") diff --git a/pkg/driver/nydus/nydus.go b/pkg/driver/nydus/nydus.go index 7e8b7de0..5b05ff23 100644 --- a/pkg/driver/nydus/nydus.go +++ b/pkg/driver/nydus/nydus.go @@ -41,6 +41,7 @@ import ( nydusutils "github.com/goharbor/acceleration-service/pkg/driver/nydus/utils" "github.com/goharbor/acceleration-service/pkg/errdefs" "github.com/goharbor/acceleration-service/pkg/utils" + "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -230,11 +231,11 @@ func (d *Driver) Convert(ctx context.Context, provider accelcontent.Provider, so return nil, errors.Wrap(err, "get source image") } - cacheRef := provider.GetCacheRef() + cacheRef, _ := provider.GetCacheInfo() useRemoteCache := cacheRef != "" if useRemoteCache { logrus.Infof("remote cache image reference: %s", cacheRef) - if err := d.FetchRemoteCache(ctx, provider, cacheRef); err != nil { + if _, err := d.FetchRemoteCache(ctx, provider, cacheRef); err != nil { if errors.Is(err, errdefs.ErrNotSupport) { logrus.Warn("Content store does not support remote cache") } else { @@ -253,13 +254,15 @@ func (d *Driver) Convert(ctx context.Context, provider accelcontent.Provider, so if useRemoteCache { // Fetch the old remote cache before updating and pushing the new one to avoid conflict. - if err := d.FetchRemoteCache(ctx, provider, cacheRef); err != nil { + cacheDesc, err := d.FetchRemoteCache(ctx, provider, cacheRef) + if err != nil { return nil, errors.Wrap(err, "fetch remote cache") } - if err := d.UpdateRemoteCache(ctx, provider, *image, *desc); err != nil { + cacheIndex, err := d.UpdateRemoteCache(ctx, provider, cacheRef, image, desc, cacheDesc) + if err != nil { return nil, errors.Wrap(err, "update remote cache") } - if err := d.PushRemoteCache(ctx, provider, cacheRef); err != nil { + if err := d.PushRemoteCache(ctx, provider, cacheRef, cacheIndex); err != nil { return nil, errors.Wrap(err, "push remote cache") } } @@ -433,26 +436,26 @@ func (d *Driver) getChunkDict(ctx context.Context, provider accelcontent.Provide } // FetchRemoteCache fetch cache manifest from remote -func (d *Driver) FetchRemoteCache(ctx context.Context, pvd accelcontent.Provider, ref string) error { +func (d *Driver) FetchRemoteCache(ctx context.Context, pvd accelcontent.Provider, ref string) (*ocispec.Descriptor, error) { resolver, err := pvd.Resolver(ref) if err != nil { - return err + return nil, err } - rc := &containerd.RemoteContext{ - Resolver: resolver, + Resolver: resolver, + PlatformMatcher: d.platformMC, } name, desc, err := rc.Resolver.Resolve(ctx, ref) if err != nil { if errors.Is(err, containerdErrDefs.ErrNotFound) { // Remote cache may do not exist, just return nil - return nil + return nil, nil } - return err + return nil, err } fetcher, err := rc.Resolver.Fetcher(ctx, name) if err != nil { - return err + return nil, err } ir, err := fetcher.Fetch(ctx, desc) if err != nil { @@ -460,123 +463,273 @@ func (d *Driver) FetchRemoteCache(ctx context.Context, pvd accelcontent.Provider pvd.UsePlainHTTP() ir, err = fetcher.Fetch(ctx, desc) if err != nil { - return errors.Wrap(err, "try to pull remote cache") + return nil, errors.Wrap(err, "try to pull remote cache") } } else { - return errors.Wrap(err, "pull remote cache") + return nil, errors.Wrap(err, "pull remote cache") } } - - bytes, err := io.ReadAll(ir) + defer ir.Close() + mBytes, err := io.ReadAll(ir) if err != nil { - return errors.Wrap(err, "read remote cache to bytes") - } - - // TODO: handle manifest list for multiple platform. - manifest := ocispec.Manifest{} - if err = json.Unmarshal(bytes, &manifest); err != nil { - return err + return nil, errors.Wrap(err, "read remote cache bytes to manifest index") } cs := pvd.ContentStore() - for _, layer := range manifest.Layers { - if _, err := cs.Update(ctx, content.Info{ - Digest: layer.Digest, - Size: layer.Size, - Labels: layer.Annotations, - }); err != nil { - if errors.Is(err, containerdErrDefs.ErrNotFound) { - return errdefs.ErrNotSupport + switch desc.MediaType { + case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: + manifestIndex := ocispec.Index{} + if err = json.Unmarshal(mBytes, &manifestIndex); err != nil { + return nil, err + } + manifestIndexDesc, _, err := nydusutils.MarshalToDesc(manifestIndex, ocispec.MediaTypeImageIndex) + if err != nil { + return nil, errors.Wrap(err, "marshal remote cache manifest index") + } + if err = content.WriteBlob(ctx, cs, ref, bytes.NewReader(mBytes), *manifestIndexDesc); err != nil { + return nil, errors.Wrap(err, "write remote cache manifest index") + } + for _, manifest := range manifestIndex.Manifests { + mDesc := ocispec.Descriptor{ + MediaType: manifest.MediaType, + Digest: manifest.Digest, + Size: manifest.Size, + } + mir, err := fetcher.Fetch(ctx, mDesc) + if err != nil { + return nil, errors.Wrap(err, "fetch remote cache") } - return errors.Wrap(err, "update cache layer") + manifestBytes, err := io.ReadAll(mir) + if err != nil { + return nil, errors.Wrap(err, "read remote cache manifest") + } + if err = content.WriteBlob(ctx, cs, ref, bytes.NewReader(manifestBytes), mDesc); err != nil { + return nil, errors.Wrap(err, "write remote cache manifest") + } + } + + // Get manifests which matches specified platforms and put them into lru cache + matchDescs, err := utils.GetManifests(ctx, cs, *manifestIndexDesc, d.platformMC) + if err != nil { + return nil, errors.Wrap(err, "get remote cache manifest list") } + var targetManifests []ocispec.Manifest + for _, desc := range matchDescs { + targetManifest := ocispec.Manifest{} + _, err = utils.ReadJSON(ctx, cs, &targetManifest, desc) + if err != nil { + return nil, errors.Wrap(err, "read remote cache manifest") + } + targetManifests = append(targetManifests, targetManifest) + + } + for _, manifest := range targetManifests { + for _, layer := range manifest.Layers { + if _, err := cs.Update(ctx, content.Info{ + Digest: layer.Digest, + Size: layer.Size, + Labels: layer.Annotations, + }); err != nil { + if errors.Is(err, containerdErrDefs.ErrNotFound) { + return nil, errdefs.ErrNotSupport + } + return nil, errors.Wrap(err, "update cache layer") + } + } + } + return manifestIndexDesc, nil + default: + return nil, fmt.Errorf("unsupported cache image mediatype %s", desc.MediaType) + } +} + +// PushRemoteCache push cache manifest to remote +func (d *Driver) PushRemoteCache(ctx context.Context, pvd accelcontent.Provider, ref string, cacheIndex *ocispec.Index) error { + for _, manifest := range cacheIndex.Manifests { + if err := pvd.Push(ctx, manifest, ref); err != nil { + return err + } + } + manifestIndexDesc, manifestIndexBytes, err := nydusutils.MarshalToDesc(*cacheIndex, ocispec.MediaTypeImageIndex) + if err != nil { + return errors.Wrap(err, "marshal remote cache manifest index") + } + if err = content.WriteBlob(ctx, pvd.ContentStore(), ref, bytes.NewReader(manifestIndexBytes), *manifestIndexDesc); err != nil { + return errors.Wrap(err, "write remote cache manifest index") + } + if err = pvd.Push(ctx, *manifestIndexDesc, ref); err != nil { + return err } return nil } -// PushRemoteCache update cache manifest and push to remote -func (d *Driver) PushRemoteCache(ctx context.Context, pvd accelcontent.Provider, ref string) error { +// UpdateRemoteCache update cache layer from upper to lower +func (d *Driver) UpdateRemoteCache(ctx context.Context, provider accelcontent.Provider, ref string, orgDesc, newDesc, cacheDesc *ocispec.Descriptor) (*ocispec.Index, error) { + cs := provider.ContentStore() + cacheLayers := map[*platforms.Platform][]ocispec.Descriptor{} + + switch orgDesc.MediaType { + case ocispec.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest: + targetLayers, err := getNydusCacheLayers(ctx, cs, *orgDesc, *newDesc) + if err != nil { + return nil, err + } + // platform of original or new image maybe lost, get from config platform + platform, err := images.Platforms(ctx, cs, *orgDesc) + if err != nil { + return nil, err + } + cacheLayers[&platform[0]] = targetLayers + + case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: + orgManifests, err := utils.GetManifests(ctx, cs, *orgDesc, d.platformMC) + if err != nil { + return nil, errors.Wrap(err, "get original manifest list") + } + newManifests, err := utils.GetManifests(ctx, cs, *newDesc, d.platformMC) + if err != nil { + return nil, errors.Wrap(err, "get new manifest list") + } + for _, newManifestDesc := range newManifests { + targetLayers := []ocispec.Descriptor{} + newManiPlatforms, err := images.Platforms(ctx, cs, newManifestDesc) + if err != nil { + return nil, errors.Wrap(err, "get converted manifest platforms") + } + // find original manifest matches converted manifest's platform + matcher := platforms.NewMatcher(newManiPlatforms[0]) + for _, orgManifestDesc := range orgManifests { + orgManiPlatforms, err := images.Platforms(ctx, cs, orgManifestDesc) + if err != nil { + return nil, errors.Wrap(err, "get original manifest platforms") + } + + if matcher.Match(orgManiPlatforms[0]) { + targetLayers, err = getNydusCacheLayers(ctx, cs, orgManifestDesc, newManifestDesc) + if err != nil { + return nil, err + } + break + } + } + cacheLayers[newManifestDesc.Platform] = targetLayers + } + } + imageConfig := ocispec.ImageConfig{} imageConfigDesc, imageConfigBytes, err := nydusutils.MarshalToDesc(imageConfig, ocispec.MediaTypeImageConfig) if err != nil { - return errors.Wrap(err, "remote cache image config marshal failed") + return nil, errors.Wrap(err, "marshal remote cache image config") } - configReader := bytes.NewReader(imageConfigBytes) - if err = content.WriteBlob(ctx, pvd.ContentStore(), ref, configReader, *imageConfigDesc); err != nil { - return errors.Wrap(err, "remote cache image config write blob failed") + if err = content.WriteBlob(ctx, cs, ref, bytes.NewReader(imageConfigBytes), *imageConfigDesc); err != nil { + return nil, errors.Wrap(err, "write remote cahce image config") } - cs := pvd.ContentStore() - layers := []ocispec.Descriptor{} - if err = cs.Walk(ctx, func(info content.Info) error { - if _, ok := info.Labels[nydusutils.LayerAnnotationNydusSourceDigest]; ok { - layers = append(layers, ocispec.Descriptor{ - MediaType: nydusutils.MediaTypeNydusBlob, - Digest: info.Digest, - Size: info.Size, - Annotations: info.Labels, - }) - } - return nil - }); err != nil { - return errors.Wrap(err, "get remote cache layers failed") - } - - manifest := ocispec.Manifest{ + cacheIndex := ocispec.Index{ Versioned: specs.Versioned{ SchemaVersion: 2, }, - MediaType: ocispec.MediaTypeImageManifest, - Config: *imageConfigDesc, - Layers: layers, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{}, } - manifestDesc, manifestBytes, err := nydusutils.MarshalToDesc(manifest, ocispec.MediaTypeImageManifest) - if err != nil { - return errors.Wrap(err, "remote cache manifest marshal failed") - } - manifestReader := bytes.NewReader(manifestBytes) - if err = content.WriteBlob(ctx, pvd.ContentStore(), ref, manifestReader, *manifestDesc); err != nil { - return errors.Wrap(err, "remote cache write blob failed") + if cacheDesc != nil { + _, err = utils.ReadJSON(ctx, cs, &cacheIndex, *cacheDesc) + if err != nil { + return nil, errors.Wrap(err, "read cache manifest index") + } + for idx, maniDesc := range cacheIndex.Manifests { + matcher := platforms.NewMatcher(*maniDesc.Platform) + for platform, layers := range cacheLayers { + if matcher.Match(*platform) { + // append new cache layers to existed cache manifest + var manifest ocispec.Manifest + _, err = utils.ReadJSON(ctx, cs, &manifest, maniDesc) + if err != nil { + return nil, errors.Wrap(err, "read cache manifest") + } + _, cacheSize := provider.GetCacheInfo() + manifest.Layers = appendLayers(manifest.Layers, layers, cacheSize) + newManiDesc, err := utils.WriteJSON(ctx, cs, manifest, maniDesc, "", nil) + if err != nil { + return nil, errors.Wrap(err, "write cache manifest") + } + cacheIndex.Manifests[idx] = *newManiDesc + delete(cacheLayers, platform) + } + } + } } - if err = pvd.Push(ctx, *manifestDesc, ref); err != nil { - return err - } - return nil + // append new cache layers to new cache manifest + for platform, layers := range cacheLayers { + manifest := ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + MediaType: ocispec.MediaTypeImageManifest, + Config: *imageConfigDesc, + Layers: layers, + } + manifestDesc, err := utils.WriteJSON(ctx, cs, manifest, ocispec.Descriptor{}, "", nil) + if err != nil { + return nil, errors.Wrap(err, "write cache manifest") + } + cacheIndex.Manifests = append(cacheIndex.Manifests, ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: manifestDesc.Digest, + Size: manifestDesc.Size, + Platform: platform, + }) + } + return &cacheIndex, nil } -// UpdateRemoteCache update cache layer from upper to lower -func (d *Driver) UpdateRemoteCache(ctx context.Context, provider accelcontent.Provider, orgDesc ocispec.Descriptor, newDesc ocispec.Descriptor) error { - cs := provider.ContentStore() - - orgManifest := ocispec.Manifest{} - _, err := utils.ReadJSON(ctx, cs, &orgManifest, orgDesc) +// getNydusCacheLayers get converted layers of nydus image and corresponding source image layers from the descriptor +// from nydus image and source image +func getNydusCacheLayers(ctx context.Context, cs content.Store, sourceManiDesc, targetManiDesc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + sourceManifest := ocispec.Manifest{} + _, err := utils.ReadJSON(ctx, cs, &sourceManifest, sourceManiDesc) if err != nil { - return errors.Wrap(err, "read original manifest json") + return nil, errors.Wrap(err, "read original manifest json") } - newManifest := ocispec.Manifest{} - _, err = utils.ReadJSON(ctx, cs, &newManifest, newDesc) + targetManifest := ocispec.Manifest{} + _, err = utils.ReadJSON(ctx, cs, &targetManifest, targetManiDesc) if err != nil { - return errors.Wrap(err, "read new manifest json") + return nil, errors.Wrap(err, "read new manifest json") } - newLayers := newManifest.Layers[:len(newManifest.Layers)-1] + // the final layer of Layers is boostrap layer of nydus image, it doesn't have corresponding source image layer + targetLayers := targetManifest.Layers[:len(targetManifest.Layers)-1] - // Update label for each layer - for i, layer := range newLayers { - layer.Annotations[nydusutils.LayerAnnotationNydusSourceDigest] = orgManifest.Layers[i].Digest.String() + // Update cache to cacheLayers from upper to lower and update layer laebl + cacheLayers := []ocispec.Descriptor{} + for i := len(targetLayers) - 1; i >= 0; i-- { + layer := targetLayers[i] + // Update label for each layer + layer.Annotations[nydusutils.LayerAnnotationNydusSourceDigest] = sourceManifest.Layers[i].Digest.String() + cacheLayers = append(cacheLayers, layer) } - // Update cache to lru from upper to lower - for i := len(newLayers) - 1; i >= 0; i-- { - layer := newLayers[i] - if _, err := cs.Update(ctx, content.Info{ - Digest: layer.Digest, - Size: layer.Size, - Labels: layer.Annotations, - }); err != nil { - return errors.Wrap(err, "update cache layer") + return cacheLayers, nil +} + +// appendLayersappend new cache layers to cache manifest layers, if new layer already exists, moving existed layers to front, avoiding to add duplicated layers. +func appendLayers(orgDescs, newDescs []ocispec.Descriptor, size int) []ocispec.Descriptor { + moveFront := map[digest.Digest]bool{} + for _, desc := range orgDescs { + moveFront[desc.Digest] = true + } + mergedLayers := orgDescs + for _, desc := range newDescs { + if !moveFront[desc.Digest] { + mergedLayers = append(mergedLayers, desc) + if len(mergedLayers) >= size { + break + } } } - return nil + if len(mergedLayers) > size { + mergedLayers = mergedLayers[:size] + } + return mergedLayers }