Repository: oras-project/oras-go (go)

Issue:
Improve the Performance of Referrers Tag Schema
The current implementation of fallback tag scheme of the referrers API is workable but inefficient. Each time pushing a manifest with a subject requires updating the index. We can improve the performance by combining the index updates into a single operation, using the same concurrency model as demonstrated in [shizhMSFT/quay](https://github.com/shizhMSFT/quay).

In addition, we can further improve the performance of checking the capability of the referrers API by combining those requests into a single operation.

Pull request:
perf: improve the performance of using referrers tag schema
Resolves #354 
Signed-off-by: Lixia (Sylvia) Lei <lixlei@microsoft.com>

Patch:
diff --git a/internal/syncutil/merge.go b/internal/syncutil/merge.go
new file mode 100644
index 0000000..4478899
--- /dev/null
+++ b/internal/syncutil/merge.go
@@ -0,0 +1,140 @@
+/*
+Copyright The ORAS Authors.
+Licensed 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 syncutil
+
+import "sync"
+
+// mergeStatus represents the merge status of an item.
+type mergeStatus struct {
+	// main indicates if items are being merged by the current go-routine.
+	main bool
+	// err represents the error of the merge operation.
+	err error
+}
+
+// Merge represents merge operations on items.
+// The state transfer is shown as below:
+//
+//	           +----------+
+//	           |  Start   +--------+-------------+
+//	           +----+-----+        |             |
+//	                |              |             |
+//	                v              v             v
+//	           +----+-----+   +----+----+   +----+----+
+//	   +-------+ Prepare  +<--+ Pending +-->+ Waiting |
+//	   |       +----+-----+   +---------+   +----+----+
+//	   |            |                            |
+//	   |            v                            |
+//	   |       + ---+---- +                      |
+//	On Error   | Resolve  |                      |
+//	   |       + ---+---- +                      |
+//	   |            |                            |
+//	   |            v                            |
+//	   |       +----+-----+                      |
+//	   +------>+ Complete +<---------------------+
+//	           +----+-----+
+//	                |
+//	                v
+//	           +----+-----+
+//	           |   End    |
+//	           +----------+
+type Merge[T any] struct {
+	lock          sync.Mutex
+	committed     bool
+	items         []T
+	status        chan mergeStatus
+	pending       []T
+	pendingStatus chan mergeStatus
+}
+
+// Do merges concurrent operations of items into a single call of prepare and
+// resolve.
+// If Do is called multiple times concurrently, only one of the calls will be
+// selected to invoke prepare and resolve.
+func (m *Merge[T]) Do(item T, prepare func() error, resolve func(items []T) error) error {
+	status := <-m.assign(item)
+	if status.main {
+		err := prepare()
+		items := m.commit()
+		if err == nil {
+			err = resolve(items)
+		}
+		m.complete(err)
+		return err
+	}
+	return status.err
+}
+
+// assign adds a new item into the item list.
+func (m *Merge[T]) assign(item T) <-chan mergeStatus {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+
+	if m.committed {
+		if m.pendingStatus == nil {
+			m.pendingStatus = make(chan mergeStatus, 1)
+		}
+		m.pending = append(m.pending, item)
+		return m.pendingStatus
+	}
+
+	if m.status == nil {
+		m.status = make(chan mergeStatus, 1)
+		m.status <- mergeStatus{main: true}
+	}
+	m.items = append(m.items, item)
+	return m.status
+}
+
+// commit closes the assignment window, and the assigned items will be ready
+// for resolve.
+func (m *Merge[T]) commit() []T {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+
+	m.committed = true
+	return m.items
+}
+
+// complete completes the previous merge, and moves the pending items to the
+// stage for the next merge.
+func (m *Merge[T]) complete(err error) {
+	// notify results
+	if err == nil {
+		close(m.status)
+	} else {
+		remaining := len(m.items) - 1
+		status := m.status
+		for remaining > 0 {
+			status <- mergeStatus{err: err}
+			remaining--
+		}
+	}
+
+	// move pending items to the stage
+	m.lock.Lock()
+	defer m.lock.Unlock()
+
+	m.committed = false
+	m.items = m.pending
+	m.status = m.pendingStatus
+	m.pending = nil
+	m.pendingStatus = nil
+
+	if m.status != nil {
+		m.status <- mergeStatus{main: true}
+	}
+}
diff --git a/internal/syncutil/pool.go b/internal/syncutil/pool.go
new file mode 100644
index 0000000..6fb4a69
--- /dev/null
+++ b/internal/syncutil/pool.go
@@ -0,0 +1,64 @@
+/*
+Copyright The ORAS Authors.
+Licensed 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 syncutil
+
+import "sync"
+
+// poolItem represents an item in Pool.
+type poolItem[T any] struct {
+	value    T
+	refCount int
+}
+
+// Pool is a scalable pool with items identified by keys.
+type Pool[T any] struct {
+	// New optionally specifies a function to generate a value when Get would
+	// otherwise return nil.
+	// It may not be changed concurrently with calls to Get.
+	New func() T
+
+	lock  sync.Mutex
+	items map[any]*poolItem[T]
+}
+
+// Get gets the value identified by key.
+// The caller should invoke the returned function after using the returned item.
+func (p *Pool[T]) Get(key any) (*T, func()) {
+	p.lock.Lock()
+	defer p.lock.Unlock()
+
+	item, ok := p.items[key]
+	if !ok {
+		if p.items == nil {
+			p.items = make(map[any]*poolItem[T])
+		}
+		item = &poolItem[T]{}
+		if p.New != nil {
+			item.value = p.New()
+		}
+		p.items[key] = item
+	}
+	item.refCount++
+
+	return &item.value, func() {
+		p.lock.Lock()
+		defer p.lock.Unlock()
+		item.refCount--
+		if item.refCount <= 0 {
+			delete(p.items, key)
+		}
+	}
+}
diff --git a/registry/remote/referrers.go b/registry/remote/referrers.go
new file mode 100644
index 0000000..ab307f7
--- /dev/null
+++ b/registry/remote/referrers.go
@@ -0,0 +1,191 @@
+/*
+Copyright The ORAS Authors.
+Licensed 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 remote
+
+import (
+	"errors"
+	"strings"
+
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"oras.land/oras-go/v2/content"
+	"oras.land/oras-go/v2/internal/descriptor"
+)
+
+// zeroDigest represents a digest that consists of zeros. zeroDigest is used
+// for pinging Referrers API.
+const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+
+// referrersState represents the state of Referrers API.
+type referrersState = int32
+
+const (
+	// referrersStateUnknown represents an unknown state of Referrers API.
+	referrersStateUnknown referrersState = iota
+	// referrersStateSupported represents that the repository is known to
+	// support Referrers API.
+	referrersStateSupported
+	// referrersStateUnsupported represents that the repository is known to
+	// not support Referrers API.
+	referrersStateUnsupported
+)
+
+// referrerOperation represents an operation on a referrer.
+type referrerOperation = int32
+
+const (
+	// referrerOperationAdd represents an addition operation on a referrer.
+	referrerOperationAdd referrerOperation = iota
+	// referrerOperationRemove represents a removal operation on a referrer.
+	referrerOperationRemove
+)
+
+// referrerChange represents a change on a referrer.
+type referrerChange struct {
+	referrer  ocispec.Descriptor
+	operation referrerOperation
+}
+
+var (
+	// ErrReferrersCapabilityAlreadySet is returned by SetReferrersCapability()
+	// when the Referrers API capability has been already set.
+	ErrReferrersCapabilityAlreadySet = errors.New("referrers capability cannot be changed once set")
+
+	// errNoReferrerUpdate is returned by applyReferrerChanges() when there
+	// is no any referrer update.
+	errNoReferrerUpdate = errors.New("no referrer update")
+)
+
+// buildReferrersTag builds the referrers tag for the given manifest descriptor.
+// Format: <algorithm>-<digest>
+// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#unavailable-referrers-api
+func buildReferrersTag(desc ocispec.Descriptor) string {
+	alg := desc.Digest.Algorithm().String()
+	encoded := desc.Digest.Encoded()
+	return alg + "-" + encoded
+}
+
+// isReferrersFilterApplied checks annotations to see if requested is in the
+// applied filter list.
+func isReferrersFilterApplied(annotations map[string]string, requested string) bool {
+	applied := annotations[ocispec.AnnotationReferrersFiltersApplied]
+	if applied == "" || requested == "" {
+		return false
+	}
+	filters := strings.Split(applied, ",")
+	for _, f := range filters {
+		if f == requested {
+			return true
+		}
+	}
+	return false
+}
+
+// filterReferrers filters a slice of referrers by artifactType in place.
+// The returned slice contains matching referrers.
+func filterReferrers(refs []ocispec.Descriptor, artifactType string) []ocispec.Descriptor {
+	if artifactType == "" {
+		return refs
+	}
+	var j int
+	for i, ref := range refs {
+		if ref.ArtifactType == artifactType {
+			if i != j {
+				refs[j] = ref
+			}
+			j++
+		}
+	}
+	return refs[:j]
+}
+
+// applyReferrerChanges applies referrerChanges on referrers and returns the
+// updated referrers.
+// Returns errNoReferrerUpdate if there is no any referrers updates.
+func applyReferrerChanges(referrers []ocispec.Descriptor, referrerChanges []referrerChange) ([]ocispec.Descriptor, error) {
+	referrersMap := make(map[descriptor.Descriptor]int, len(referrers)+len(referrerChanges))
+	updatedReferrers := make([]ocispec.Descriptor, 0, len(referrers)+len(referrerChanges))
+	var updateRequired bool
+	for _, r := range referrers {
+		if content.Equal(r, ocispec.Descriptor{}) {
+			// skip bad entry
+			updateRequired = true
+			continue
+		}
+		key := descriptor.FromOCI(r)
+		if _, ok := referrersMap[key]; ok {
+			// skip duplicates
+			updateRequired = true
+			continue
+		}
+		updatedReferrers = append(updatedReferrers, r)
+		referrersMap[key] = len(updatedReferrers) - 1
+	}
+
+	// apply changes
+	for _, change := range referrerChanges {
+		key := descriptor.FromOCI(change.referrer)
+		switch change.operation {
+		case referrerOperationAdd:
+			if _, ok := referrersMap[key]; !ok {
+				// add distinct referrers
+				updatedReferrers = append(updatedReferrers, change.referrer)
+				referrersMap[key] = len(updatedReferrers) - 1
+			}
+		case referrerOperationRemove:
+			if pos, ok := referrersMap[key]; ok {
+				// remove referrers that are already in the map
+				updatedReferrers[pos] = ocispec.Descriptor{}
+				delete(referrersMap, key)
+			}
+		}
+	}
+
+	// skip unnecessary update
+	if !updateRequired && len(referrersMap) == len(referrers) {
+		// if the result referrer map contains the same content as the
+		// original referrers, consider that there is no update on the
+		// referrers.
+		for _, r := range referrers {
+			key := descriptor.FromOCI(r)
+			if _, ok := referrersMap[key]; !ok {
+				updateRequired = true
+			}
+		}
+		if !updateRequired {
+			return nil, errNoReferrerUpdate
+		}
+	}
+
+	return removeEmptyDescriptors(updatedReferrers, len(referrersMap)), nil
+}
+
+// removeEmptyDescriptors in-place removes empty items from descs, given a hint
+// of the number of non-empty descriptors.
+func removeEmptyDescriptors(descs []ocispec.Descriptor, hint int) []ocispec.Descriptor {
+	j := 0
+	for i, r := range descs {
+		if !content.Equal(r, ocispec.Descriptor{}) {
+			if i > j {
+				descs[j] = r
+			}
+			j++
+		}
+		if j == hint {
+			break
+		}
+	}
+	return descs[:j]
+}
diff --git a/registry/remote/repository.go b/registry/remote/repository.go
index 271c77c..3345d93 100644
--- a/registry/remote/repository.go
+++ b/registry/remote/repository.go
@@ -38,40 +38,18 @@ import (
 	"oras.land/oras-go/v2/internal/httputil"
 	"oras.land/oras-go/v2/internal/ioutil"
 	"oras.land/oras-go/v2/internal/registryutil"
+	"oras.land/oras-go/v2/internal/syncutil"
 	"oras.land/oras-go/v2/registry"
 	"oras.land/oras-go/v2/registry/remote/auth"
 	"oras.land/oras-go/v2/registry/remote/errcode"
 	"oras.land/oras-go/v2/registry/remote/internal/errutil"
 )
 
-const (
-	// dockerContentDigestHeader - The Docker-Content-Digest header, if present
-	// on the response, returns the canonical digest of the uploaded blob.
-	// See https://docs.docker.com/registry/spec/api/#digest-header
-	// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull
-	dockerContentDigestHeader = "Docker-Content-Digest"
-	// zeroDigest represents a digest that consists of zeros. zeroDigest is used
-	// for pinging Referrers API.
-	zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
-)
-
-// referrersState represents the state of Referrers API.
-type referrersState = int32
-
-const (
-	// referrersStateUnknown represents an unknown state of Referrers API.
-	referrersStateUnknown referrersState = iota
-	// referrersStateSupported represents that the repository is known to
-	// support Referrers API
-	referrersStateSupported
-	// referrersStateUnsupported represents that the repository is known to
-	// not support Referrers API
-	referrersStateUnsupported
-)
-
-// ErrReferrersCapabilityAlreadySet is returned by SetReferrersCapability()
-// when the Referrers API capability has been already set.
-var ErrReferrersCapabilityAlreadySet = errors.New("referrers capability cannot be changed once set")
+// dockerContentDigestHeader - The Docker-Content-Digest header, if present
+// on the response, returns the canonical digest of the uploaded blob.
+// See https://docs.docker.com/registry/spec/api/#digest-header
+// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull
+const dockerContentDigestHeader = "Docker-Content-Digest"
 
 // Client is an interface for a HTTP client.
 type Client interface {
@@ -126,12 +104,13 @@ type Repository struct {
 	// default: referrersStateUnknown
 	referrersState referrersState
 
-	// referrersTagLocks maps a referrers tag to a lock.
-	referrersTagLocks sync.Map // map[string]*sync.Mutex
-
-	// referrersPingLock locks the pingReferrersAPI() method and allows only
+	// referrersPingLock locks the pingReferrers() method and allows only
 	// one go-routine to send the request.
 	referrersPingLock sync.Mutex
+
+	// referrersMergePool provides a way to manage concurrent updates to a
+	// referrers index tagged by referrers tag schema.
+	referrersMergePool syncutil.Pool[syncutil.Merge[referrerChange]]
 }
 
 // NewRepository creates a client to the remote repository identified by a
@@ -181,23 +160,6 @@ func (r *Repository) loadReferrersState() referrersState {
 	return atomic.LoadInt32(&r.referrersState)
 }
 
-// lockReferrersTag locks a referrers tag.
-func (r *Repository) lockReferrersTag(tag string) {
-	v, _ := r.referrersTagLocks.LoadOrStore(tag, &sync.Mutex{})
-	lock := v.(*sync.Mutex)
-	lock.Lock()
-}
-
-// unlockReferrersTag unlocks a referrers tag.
-func (r *Repository) unlockReferrersTag(tag string) {
-	v, ok := r.referrersTagLocks.Load(tag)
-	if !ok {
-		return
-	}
-	lock := v.(*sync.Mutex)
-	lock.Unlock()
-}
-
 // client returns an HTTP client used to access the remote repository.
 // A default HTTP client is return if the client is not configured.
 func (r *Repository) client() Client {
@@ -536,47 +498,56 @@ func (r *Repository) referrersFromIndex(ctx context.Context, referrersTag string
 	return desc, index.Manifests, nil
 }
 
-// buildReferrersTag builds the referrers tag for the given manifest descriptor.
-// Format: <algorithm>-<digest>
-// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#unavailable-referrers-api
-func buildReferrersTag(desc ocispec.Descriptor) string {
-	alg := desc.Digest.Algorithm().String()
-	encoded := desc.Digest.Encoded()
-	return alg + "-" + encoded
-}
+// pingReferrers returns true if the Referrers API is available for r.
+func (r *Repository) pingReferrers(ctx context.Context) (bool, error) {
+	switch r.loadReferrersState() {
+	case referrersStateSupported:
+		return true, nil
+	case referrersStateUnsupported:
+		return false, nil
+	}
 
-// isReferrersFilterApplied checks annotations to see if requested is in the
-// applied filter list.
-func isReferrersFilterApplied(annotations map[string]string, requested string) bool {
-	applied := annotations[ocispec.AnnotationReferrersFiltersApplied]
-	if applied == "" || requested == "" {
-		return false
-	}
-	filters := strings.Split(applied, ",")
-	for _, f := range filters {
-		if f == requested {
-			return true
-		}
+	// referrers state is unknown
+	// limit the rate of pinging referrers API
+	r.referrersPingLock.Lock()
+	defer r.referrersPingLock.Unlock()
+
+	switch r.loadReferrersState() {
+	case referrersStateSupported:
+		return true, nil
+	case referrersStateUnsupported:
+		return false, nil
 	}
-	return false
-}
 
-// filterReferrers filters a slice of referrers by artifactType in place.
-// The returned slice contains matching referrers.
-func filterReferrers(refs []ocispec.Descriptor, artifactType string) []ocispec.Descriptor {
-	if artifactType == "" {
-		return refs
-	}
-	var j int
-	for i, ref := range refs {
-		if ref.ArtifactType == artifactType {
-			if i != j {
-				refs[j] = ref
-			}
-			j++
+	ref := r.Reference
+	ref.Reference = zeroDigest
+	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
+
+	url := buildReferrersURL(r.PlainHTTP, ref, "")
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+	if err != nil {
+		return false, err
+	}
+	resp, err := r.client().Do(req)
+	if err != nil {
+		return false, err
+	}
+	defer resp.Body.Close()
+
+	switch resp.StatusCode {
+	case http.StatusOK:
+		r.SetReferrersCapability(true)
+		return true, nil
+	case http.StatusNotFound:
+		if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) {
+			// repository not found
+			return false, err
 		}
+		r.SetReferrersCapability(false)
+		return false, nil
+	default:
+		return false, errutil.ParseErrorResponse(resp)
 	}
-	return refs[:j]
 }
 
 // delete removes the content identified by the descriptor in the entity "blobs"
@@ -993,48 +964,7 @@ func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispe
 		// referrers API is available, no client-side indexing needed
 		return nil
 	}
-	return s.updateReferrersIndexForDelete(ctx, desc, subject)
-}
-
-// updateReferrersIndexForDelete updates the referrers index for desc referencing
-// subject on manifest delete.
-func (s *manifestStore) updateReferrersIndexForDelete(ctx context.Context, desc, subject ocispec.Descriptor) error {
-	// there can be multiple go-routines updating the referrers tag concurrently
-	referrersTag := buildReferrersTag(subject)
-	s.repo.lockReferrersTag(referrersTag)
-	defer s.repo.unlockReferrersTag(referrersTag)
-
-	oldIndexDesc, referrers, err := s.repo.referrersFromIndex(ctx, referrersTag)
-	if err != nil {
-		if errors.Is(err, errdef.ErrNotFound) {
-			// inconsistent indexing state: no old index found, skip update
-			return nil
-		}
-		return err
-	}
-	if len(referrers) == 0 {
-		// inconsistent indexing state: no referrers to the subject, skip update
-		return nil
-	}
-
-	var updatedReferrers []ocispec.Descriptor
-	for _, r := range referrers {
-		// remove the current entry
-		if !content.Equal(r, desc) {
-			updatedReferrers = append(updatedReferrers, r)
-		}
-	}
-	if len(updatedReferrers) > 0 {
-		newIndexDesc, newIndex, err := generateIndex(updatedReferrers)
-		if err != nil {
-			return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err)
-		}
-		if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil {
-			return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err)
-		}
-	}
-
-	return s.repo.delete(ctx, oldIndexDesc, true)
+	return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationRemove})
 }
 
 // Resolve resolves a reference to a descriptor.
@@ -1258,98 +1188,67 @@ func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec.
 		// referrers API is available, no client-side indexing needed
 		return nil
 	}
-	return s.updateReferrersIndexForPush(ctx, desc, subject)
+	return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationAdd})
 }
 
-// updateReferrersIndexForPush updates the referrers index for desc referencing
-// subject on manifest push.
-func (s *manifestStore) updateReferrersIndexForPush(ctx context.Context, desc, subject ocispec.Descriptor) error {
-	// there can be multiple go-routines updating the referrers tag concurrently
+// updateReferrersIndex updates the referrers index for desc referencing subject
+// on manifest push and manifest delete.
+// References:
+//   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject
+//   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests
+func (s *manifestStore) updateReferrersIndex(ctx context.Context, subject ocispec.Descriptor, change referrerChange) (err error) {
 	referrersTag := buildReferrersTag(subject)
-	s.repo.lockReferrersTag(referrersTag)
-	defer s.repo.unlockReferrersTag(referrersTag)
 
 	var skipDelete bool
-	oldIndexDesc, referrers, err := s.repo.referrersFromIndex(ctx, referrersTag)
-	if err != nil {
-		if !errors.Is(err, errdef.ErrNotFound) {
+	var oldIndexDesc ocispec.Descriptor
+	var referrers []ocispec.Descriptor
+	prepare := func() error {
+		// 1. pull the original referrers list using the referrers tag schema
+		var err error
+		oldIndexDesc, referrers, err = s.repo.referrersFromIndex(ctx, referrersTag)
+		if err != nil {
+			if errors.Is(err, errdef.ErrNotFound) {
+				// no old index found, skip delete
+				skipDelete = true
+				return nil
+			}
 			return err
 		}
-		// no old index found, skip delete
-		skipDelete = true
+		return nil
 	}
+	update := func(referrerChanges []referrerChange) error {
+		// 2. apply the referrer changes on the referrers list
+		updatedReferrers, err := applyReferrerChanges(referrers, referrerChanges)
+		if err != nil {
+			if err == errNoReferrerUpdate {
+				return nil
+			}
+			return err
+		}
 
-	for _, r := range referrers {
-		if content.Equal(r, desc) {
-			// desc is already in the referrers list, skip update
-			return nil
+		// 3. push the updated referrers list using referrers tag schema
+		if len(updatedReferrers) > 0 {
+			newIndexDesc, newIndex, err := generateIndex(updatedReferrers)
+			if err != nil {
+				return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err)
+			}
+			if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil {
+				return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err)
+			}
 		}
-	}
-	referrers = append(referrers, desc)
-	newIndexDesc, newIndex, err := generateIndex(referrers)
-	if err != nil {
-		return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err)
-	}
-	if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil {
-		return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err)
-	}
 
-	if skipDelete {
+		// 4. delete the dangling original referrers index
+		if !skipDelete {
+			if err := s.repo.delete(ctx, oldIndexDesc, true); err != nil {
+				return fmt.Errorf("failed to delete dangling referrers index %s for referrers tag %s: %w", oldIndexDesc.Digest.String(), referrersTag, err)
+			}
+		}
 		return nil
 	}
-	return s.repo.delete(ctx, oldIndexDesc, true)
-}
-
-// pingReferrers returns true if the Referrers API is available for r.
-func (r *Repository) pingReferrers(ctx context.Context) (bool, error) {
-	switch r.loadReferrersState() {
-	case referrersStateSupported:
-		return true, nil
-	case referrersStateUnsupported:
-		return false, nil
-	}
-
-	// referrers state is unknown
-	// limit the rate of pinging referrers API
-	r.referrersPingLock.Lock()
-	defer r.referrersPingLock.Unlock()
-
-	switch r.loadReferrersState() {
-	case referrersStateSupported:
-		return true, nil
-	case referrersStateUnsupported:
-		return false, nil
-	}
 
-	ref := r.Reference
-	ref.Reference = zeroDigest
-	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
-
-	url := buildReferrersURL(r.PlainHTTP, ref, "")
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
-	if err != nil {
-		return false, err
-	}
-	resp, err := r.client().Do(req)
-	if err != nil {
-		return false, err
-	}
-	defer resp.Body.Close()
-
-	switch resp.StatusCode {
-	case http.StatusOK:
-		r.SetReferrersCapability(true)
-		return true, nil
-	case http.StatusNotFound:
-		if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) {
-			// repository not found
-			return false, err
-		}
-		r.SetReferrersCapability(false)
-		return false, nil
-	default:
-		return false, errutil.ParseErrorResponse(resp)
-	}
+	merge, done := s.repo.referrersMergePool.Get(referrersTag)
+	defer done()
+	return merge.Do(change, prepare, update)
 }
 
 // ParseReference parses a reference to a fully qualified reference.


Test patch:
diff --git a/internal/syncutil/merge_test.go b/internal/syncutil/merge_test.go
new file mode 100644
index 0000000..caf451c
--- /dev/null
+++ b/internal/syncutil/merge_test.go
@@ -0,0 +1,64 @@
+/*
+Copyright The ORAS Authors.
+Licensed 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 syncutil
+
+import (
+	"context"
+	"testing"
+
+	"golang.org/x/sync/errgroup"
+)
+
+func TestMerge(t *testing.T) {
+	var merge Merge[int]
+
+	// generate expected result
+	size := 100
+	factor := -1
+	var expected int
+	for i := 1; i < size; i++ {
+		expected += i * factor
+	}
+
+	// test merge
+	ctx := context.Background()
+	eg, _ := errgroup.WithContext(ctx)
+	var result int
+	for i := 1; i < size; i++ {
+		eg.Go(func(num int) func() error {
+			return func() error {
+				var f int
+				getFactor := func() error {
+					f = factor
+					return nil
+				}
+				calculate := func(items []int) error {
+					for _, item := range items {
+						result += item * f
+					}
+					return nil
+				}
+				return merge.Do(num, getFactor, calculate)
+			}
+		}(i))
+	}
+	if err := eg.Wait(); err != nil {
+		t.Errorf("Merge.Do() error = %v, wantErr %v", err, nil)
+	}
+	if result != expected {
+		t.Errorf("Merge.Do() = %v, want %v", result, expected)
+	}
+}
diff --git a/internal/syncutil/pool_test.go b/internal/syncutil/pool_test.go
new file mode 100644
index 0000000..82c494f
--- /dev/null
+++ b/internal/syncutil/pool_test.go
@@ -0,0 +1,69 @@
+/*
+Copyright The ORAS Authors.
+Licensed 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 syncutil
+
+import (
+	"sync"
+	"sync/atomic"
+	"testing"
+)
+
+func TestPool(t *testing.T) {
+	var pool Pool[int64]
+	numbers := [][]int{
+		{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+		{-1, -2, -3, -4, -5, -6, -7, -8, -9, -10},
+	}
+
+	// generate expected result
+	expected := make([]int, len(numbers))
+	for i, nums := range numbers {
+		for _, num := range nums {
+			expected[i] += num
+		}
+	}
+
+	// test pool
+	for i, nums := range numbers {
+		val, done := pool.Get(i)
+		*val = 0
+		var wg sync.WaitGroup
+		for _, num := range nums {
+			wg.Add(1)
+			go func(n int) {
+				defer wg.Done()
+				val, done := pool.Get(i)
+				defer done()
+				atomic.AddInt64(val, int64(n))
+			}(num)
+		}
+		wg.Wait()
+		item := pool.items[i]
+		if got := item.value; got != int64(expected[i]) {
+			t.Errorf("Pool.Get(%v).value = %v, want %v", i, got, expected[i])
+		}
+		if got := item.refCount; got != 1 {
+			t.Errorf("Pool.Get(%v).refCount = %v, want %v", i, got, 1)
+		}
+
+		// item should be cleaned up after done
+		done()
+		got := pool.items[i]
+		if got != nil {
+			t.Errorf("Pool.Get(%v) = %v, want %v", i, got, nil)
+		}
+	}
+}
diff --git a/registry/remote/referrers_test.go b/registry/remote/referrers_test.go
new file mode 100644
index 0000000..e06bdfc
--- /dev/null
+++ b/registry/remote/referrers_test.go
@@ -0,0 +1,590 @@
+/*
+Copyright The ORAS Authors.
+Licensed 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 remote
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/opencontainers/go-digest"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+func Test_buildReferrersTag(t *testing.T) {
+	tests := []struct {
+		name string
+		desc ocispec.Descriptor
+		want string
+	}{
+		{
+			name: "zero digest",
+			desc: ocispec.Descriptor{
+				Digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
+			},
+			want: "sha256-0000000000000000000000000000000000000000000000000000000000000000",
+		},
+		{
+			name: "sha256",
+			desc: ocispec.Descriptor{
+				Digest: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
+			},
+			want: "sha256-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
+		},
+		{
+			name: "sha512",
+			desc: ocispec.Descriptor{
+				Digest: "sha512:ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
+			},
+			want: "sha512-ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := buildReferrersTag(tt.desc); got != tt.want {
+				t.Errorf("getReferrersTag() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_isReferrersFilterApplied(t *testing.T) {
+	tests := []struct {
+		name        string
+		annotations map[string]string
+		requested   string
+		want        bool
+	}{
+		{
+			name:        "single filter applied, specified filter matches",
+			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "artifactType"},
+			requested:   "artifactType",
+			want:        true,
+		},
+		{
+			name:        "single filter applied, specified filter does not match",
+			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "foo"},
+			requested:   "artifactType",
+			want:        false,
+		},
+		{
+			name:        "multiple filters applied, specified filter matches",
+			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "foo,artifactType"},
+			requested:   "artifactType",
+			want:        true,
+		},
+		{
+			name:        "multiple filters applied, specified filter does not match",
+			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "foo,bar"},
+			requested:   "artifactType",
+			want:        false,
+		},
+		{
+			name:        "single filter applied, specified filter empty",
+			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "foo"},
+			requested:   "",
+			want:        false,
+		},
+		{
+			name:        "no filter applied",
+			annotations: map[string]string{},
+			requested:   "artifactType",
+			want:        false,
+		},
+		{
+			name:        "empty filter applied",
+			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: ""},
+			requested:   "artifactType",
+			want:        false,
+		},
+		{
+			name:        "no filter applied, specified filter empty",
+			annotations: map[string]string{},
+			requested:   "",
+			want:        false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := isReferrersFilterApplied(tt.annotations, tt.requested); got != tt.want {
+				t.Errorf("isReferrersFilterApplied() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_filterReferrers(t *testing.T) {
+	refs := []ocispec.Descriptor{
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         1,
+			Digest:       digest.FromString("1"),
+			ArtifactType: "application/vnd.test",
+		},
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         2,
+			Digest:       digest.FromString("2"),
+			ArtifactType: "application/vnd.foo",
+		},
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         3,
+			Digest:       digest.FromString("3"),
+			ArtifactType: "application/vnd.bar",
+		},
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         4,
+			Digest:       digest.FromString("4"),
+			ArtifactType: "application/vnd.test",
+		},
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         5,
+			Digest:       digest.FromString("5"),
+			ArtifactType: "application/vnd.baz",
+		},
+	}
+	got := filterReferrers(refs, "application/vnd.test")
+	want := []ocispec.Descriptor{
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         1,
+			Digest:       digest.FromString("1"),
+			ArtifactType: "application/vnd.test",
+		},
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         4,
+			Digest:       digest.FromString("4"),
+			ArtifactType: "application/vnd.test",
+		},
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("filterReferrers() = %v, want %v", got, want)
+	}
+}
+
+func Test_filterReferrers_allMatch(t *testing.T) {
+	refs := []ocispec.Descriptor{
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         1,
+			Digest:       digest.FromString("1"),
+			ArtifactType: "application/vnd.test",
+		},
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         4,
+			Digest:       digest.FromString("2"),
+			ArtifactType: "application/vnd.test",
+		},
+		{
+			MediaType:    ocispec.MediaTypeArtifactManifest,
+			Size:         5,
+			Digest:       digest.FromString("3"),
+			ArtifactType: "application/vnd.test",
+		},
+	}
+	got := filterReferrers(refs, "application/vnd.test")
+	if !reflect.DeepEqual(got, refs) {
+		t.Errorf("filterReferrers() = %v, want %v", got, refs)
+	}
+}
+
+func Test_applyReferrerChanges(t *testing.T) {
+	descs := []ocispec.Descriptor{
+		{
+			MediaType:    ocispec.MediaTypeDescriptor,
+			Digest:       "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+			Size:         3,
+			ArtifactType: "foo",
+			Annotations:  map[string]string{"name": "foo"},
+		},
+		{
+			MediaType:    ocispec.MediaTypeDescriptor,
+			Digest:       "sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9",
+			Size:         3,
+			ArtifactType: "bar",
+			Annotations:  map[string]string{"name": "bar"},
+		},
+		{
+			MediaType:    ocispec.MediaTypeDescriptor,
+			Digest:       "sha256:baa5a0964d3320fbc0c6a922140453c8513ea24ab8fd0577034804a967248096",
+			Size:         3,
+			ArtifactType: "baz",
+			Annotations:  map[string]string{"name": "baz"},
+		},
+		{
+			MediaType:    ocispec.MediaTypeDescriptor,
+			Digest:       "sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
+			Size:         5,
+			ArtifactType: "hello",
+			Annotations:  map[string]string{"name": "hello"},
+		},
+		{
+			MediaType:    ocispec.MediaTypeDescriptor,
+			Digest:       "sha256:82e35a63ceba37e9646434c5dd412ea577147f1e4a41ccde1614253187e3dbf9",
+			Size:         7,
+			ArtifactType: "goodbye",
+			Annotations:  map[string]string{"name": "goodbye"},
+		},
+	}
+
+	tests := []struct {
+		name            string
+		referrers       []ocispec.Descriptor
+		referrerChanges []referrerChange
+		want            []ocispec.Descriptor
+		wantErr         error
+	}{
+		{
+			name:      "add to an empty list",
+			referrers: []ocispec.Descriptor{},
+			referrerChanges: []referrerChange{
+				{descs[0], referrerOperationAdd}, // add new
+				{descs[1], referrerOperationAdd}, // add new
+				{descs[2], referrerOperationAdd}, // add new
+			},
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			wantErr: nil,
+		},
+		{
+			name: "add to a non-empty list",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+			},
+			referrerChanges: []referrerChange{
+				{descs[2], referrerOperationAdd}, // add new
+				{descs[1], referrerOperationAdd}, // add existing
+				{descs[1], referrerOperationAdd}, // add duplicate existing
+				{descs[3], referrerOperationAdd}, // add new
+				{descs[2], referrerOperationAdd}, // add duplicate new
+			},
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+				descs[3],
+			},
+			wantErr: nil,
+		},
+		{
+			name: "partially remove",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			referrerChanges: []referrerChange{
+				{descs[2], referrerOperationRemove}, // remove existing
+				{descs[1], referrerOperationRemove}, // remove existing
+				{descs[3], referrerOperationRemove}, // remove non-existing
+				{descs[2], referrerOperationRemove}, // remove duplicate existing
+				{descs[4], referrerOperationRemove}, // remove non-existing
+			},
+			want: []ocispec.Descriptor{
+				descs[0],
+			},
+			wantErr: nil,
+		},
+		{
+			name: "remove all",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			referrerChanges: []referrerChange{
+				{descs[2], referrerOperationRemove}, // remove existing
+				{descs[0], referrerOperationRemove}, // remove existing
+				{descs[1], referrerOperationRemove}, // remove existing
+			},
+			want:    []ocispec.Descriptor{},
+			wantErr: nil,
+		},
+		{
+			name: "add a new one and remove it",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			referrerChanges: []referrerChange{
+				{descs[1], referrerOperationAdd},    // add existing
+				{descs[3], referrerOperationAdd},    // add new
+				{descs[3], referrerOperationAdd},    // add duplicate new
+				{descs[3], referrerOperationRemove}, // remove new
+				{descs[4], referrerOperationAdd},    // add new
+			},
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+				descs[4],
+			},
+			wantErr: nil,
+		},
+		{
+			name: "remove a new one and add it back",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			referrerChanges: []referrerChange{
+				{descs[1], referrerOperationAdd},    // add existing
+				{descs[3], referrerOperationAdd},    // add new
+				{descs[3], referrerOperationRemove}, // remove new,
+				{descs[3], referrerOperationAdd},    // add new back
+				{descs[4], referrerOperationAdd},    // add new
+			},
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+				descs[3],
+				descs[4],
+			},
+			wantErr: nil,
+		},
+		{
+			name: "remove an existing one and add it back",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			referrerChanges: []referrerChange{
+				{descs[2], referrerOperationRemove}, // remove existing
+				{descs[3], referrerOperationAdd},    // add new
+				{descs[2], referrerOperationAdd},    // add existing back
+			},
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[3],
+				descs[2],
+			},
+			wantErr: nil,
+		},
+		{
+			name: "list containing duplicate entries",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[0], // duplicate
+				descs[2],
+				descs[3],
+				descs[1], // duplicate
+			},
+			referrerChanges: []referrerChange{
+				{descs[2], referrerOperationAdd},    // add new
+				{descs[2], referrerOperationAdd},    // add duplicate new
+				{descs[3], referrerOperationRemove}, // remove existing
+			},
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			wantErr: nil,
+		},
+		{
+			name: "list containing bad entries",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				{},
+				descs[1],
+			},
+			referrerChanges: []referrerChange{
+				{descs[2], referrerOperationAdd},    // add new
+				{descs[1], referrerOperationRemove}, // remove existing
+			},
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[2],
+			},
+			wantErr: nil,
+		},
+		{
+			name: "no update: same order",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			referrerChanges: []referrerChange{
+				{descs[3], referrerOperationAdd},    // add new
+				{descs[2], referrerOperationRemove}, // remove existing
+				{descs[4], referrerOperationAdd},    // add new
+				{descs[4], referrerOperationRemove}, // remove new
+				{descs[2], referrerOperationAdd},    // add existing back
+				{descs[3], referrerOperationRemove}, // remove new
+			},
+			want:    nil,
+			wantErr: errNoReferrerUpdate,
+		},
+		{
+			name: "no update: different order",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+			referrerChanges: []referrerChange{
+				{descs[2], referrerOperationRemove}, // remove existing
+				{descs[0], referrerOperationRemove}, // remove existing
+				{descs[0], referrerOperationAdd},    // add existing back
+				{descs[2], referrerOperationAdd},    // add existing back
+			},
+			want:    nil,
+			wantErr: errNoReferrerUpdate, // internal result: 2, 1, 0
+		},
+		{
+			name: "no update: list containing duplicate entries",
+			referrers: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[0], // duplicate
+				descs[2],
+				descs[1], // duplicate
+			},
+			referrerChanges: []referrerChange{
+				{descs[2], referrerOperationRemove}, // remove existing
+				{descs[0], referrerOperationRemove}, // remove existing
+				{descs[0], referrerOperationAdd},    // add existing back
+				{descs[2], referrerOperationAdd},    // add existing back
+			},
+			want: []ocispec.Descriptor{
+				descs[1],
+				descs[0],
+				descs[2],
+			},
+			wantErr: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := applyReferrerChanges(tt.referrers, tt.referrerChanges)
+			if err != tt.wantErr {
+				t.Errorf("applyReferrerChanges() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("applyReferrerChanges() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_removeEmptyDescriptors(t *testing.T) {
+	descs := []ocispec.Descriptor{
+		{
+			MediaType:    ocispec.MediaTypeDescriptor,
+			Digest:       "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+			Size:         3,
+			ArtifactType: "foo",
+			Annotations:  map[string]string{"name": "foo"},
+		},
+		{
+			MediaType:    ocispec.MediaTypeDescriptor,
+			Digest:       "sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9",
+			Size:         3,
+			ArtifactType: "bar",
+			Annotations:  map[string]string{"name": "bar"},
+		},
+		{
+			MediaType:    ocispec.MediaTypeDescriptor,
+			Digest:       "sha256:baa5a0964d3320fbc0c6a922140453c8513ea24ab8fd0577034804a967248096",
+			Size:         3,
+			ArtifactType: "baz",
+			Annotations:  map[string]string{"name": "baz"},
+		},
+	}
+	tests := []struct {
+		name  string
+		descs []ocispec.Descriptor
+		hint  int
+		want  []ocispec.Descriptor
+	}{
+		{
+			name:  "empty list",
+			descs: []ocispec.Descriptor{},
+			hint:  0,
+			want:  []ocispec.Descriptor{},
+		},
+		{
+			name:  "all non-empty",
+			descs: descs,
+			hint:  len(descs),
+			want:  descs,
+		},
+		{
+			name: "all empty",
+			descs: []ocispec.Descriptor{
+				{},
+				{},
+				{},
+			},
+			hint: 0,
+			want: []ocispec.Descriptor{},
+		},
+		{
+			name: "empty rear",
+			descs: []ocispec.Descriptor{
+				descs[0],
+				{},
+				descs[2],
+				{},
+				{},
+			},
+			hint: 2,
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[2],
+			},
+		},
+		{
+			name: "empty head",
+			descs: []ocispec.Descriptor{
+				{},
+				descs[0],
+				descs[1],
+				{},
+				{},
+				descs[2],
+			},
+			hint: 3,
+			want: []ocispec.Descriptor{
+				descs[0],
+				descs[1],
+				descs[2],
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := removeEmptyDescriptors(tt.descs, tt.hint); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("removeEmptyDescriptors() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/registry/remote/repository_test.go b/registry/remote/repository_test.go
index 86da2fd..b6936e0 100644
--- a/registry/remote/repository_test.go
+++ b/registry/remote/repository_test.go
@@ -1884,86 +1884,6 @@ func TestRepository_Referrers_TagSchemaFallback_ClientFiltering(t *testing.T) {
 	}
 }
 
-func Test_filterReferrers(t *testing.T) {
-	refs := []ocispec.Descriptor{
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         1,
-			Digest:       digest.FromString("1"),
-			ArtifactType: "application/vnd.test",
-		},
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         2,
-			Digest:       digest.FromString("2"),
-			ArtifactType: "application/vnd.foo",
-		},
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         3,
-			Digest:       digest.FromString("3"),
-			ArtifactType: "application/vnd.bar",
-		},
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         4,
-			Digest:       digest.FromString("4"),
-			ArtifactType: "application/vnd.test",
-		},
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         5,
-			Digest:       digest.FromString("5"),
-			ArtifactType: "application/vnd.baz",
-		},
-	}
-	got := filterReferrers(refs, "application/vnd.test")
-	want := []ocispec.Descriptor{
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         1,
-			Digest:       digest.FromString("1"),
-			ArtifactType: "application/vnd.test",
-		},
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         4,
-			Digest:       digest.FromString("4"),
-			ArtifactType: "application/vnd.test",
-		},
-	}
-	if !reflect.DeepEqual(got, want) {
-		t.Errorf("filterReferrers() = %v, want %v", got, want)
-	}
-}
-
-func Test_filterReferrers_allMatch(t *testing.T) {
-	refs := []ocispec.Descriptor{
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         1,
-			Digest:       digest.FromString("1"),
-			ArtifactType: "application/vnd.test",
-		},
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         4,
-			Digest:       digest.FromString("2"),
-			ArtifactType: "application/vnd.test",
-		},
-		{
-			MediaType:    ocispec.MediaTypeArtifactManifest,
-			Size:         5,
-			Digest:       digest.FromString("3"),
-			ArtifactType: "application/vnd.test",
-		},
-	}
-	got := filterReferrers(refs, "application/vnd.test")
-	if !reflect.DeepEqual(got, refs) {
-		t.Errorf("filterReferrers() = %v, want %v", got, refs)
-	}
-}
-
 func Test_BlobStore_Fetch(t *testing.T) {
 	blob := []byte("hello world")
 	blobDesc := ocispec.Descriptor{
@@ -3795,6 +3715,71 @@ func Test_ManifestStore_Delete_ReferrersAPIUnavailable_InconsistentIndex(t *test
 	if state := repo.loadReferrersState(); state != referrersStateUnsupported {
 		t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
 	}
+
+	// test inconsistent state: current referrer is not in referrers list
+	manifestDeleted = true
+	ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch {
+		case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
+			manifestDeleted = true
+			// no "Docker-Content-Digest" header for manifest deletion
+			w.WriteHeader(http.StatusAccepted)
+		case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
+			if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) {
+				t.Errorf("manifest not convertable: %s", accept)
+				w.WriteHeader(http.StatusBadRequest)
+				return
+			}
+			w.Header().Set("Content-Type", artifactDesc.MediaType)
+			w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
+			if _, err := w.Write(artifactJSON); err != nil {
+				t.Errorf("failed to write %q: %v", r.URL, err)
+			}
+		case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
+			w.WriteHeader(http.StatusNotFound)
+		case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
+			result := ocispec.Index{
+				Versioned: specs.Versioned{
+					SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
+				},
+				MediaType: ocispec.MediaTypeImageIndex,
+				Manifests: []ocispec.Descriptor{
+					content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, []byte("whaterver")),
+				},
+			}
+			if err := json.NewEncoder(w).Encode(result); err != nil {
+				t.Errorf("failed to write response: %v", err)
+			}
+		default:
+			t.Errorf("unexpected access: %s %s", r.Method, r.URL)
+			w.WriteHeader(http.StatusNotFound)
+		}
+	}))
+	defer ts.Close()
+	uri, err = url.Parse(ts.URL)
+	if err != nil {
+		t.Fatalf("invalid test http server: %v", err)
+	}
+	repo, err = NewRepository(uri.Host + "/test")
+	if err != nil {
+		t.Fatalf("NewRepository() error = %v", err)
+	}
+	repo.PlainHTTP = true
+	store = repo.Manifests()
+	ctx = context.Background()
+	if state := repo.loadReferrersState(); state != referrersStateUnknown {
+		t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
+	}
+	err = store.Delete(ctx, artifactDesc)
+	if err != nil {
+		t.Fatalf("Manifests.Delete() error = %v", err)
+	}
+	if !manifestDeleted {
+		t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
+	}
+	if state := repo.loadReferrersState(); state != referrersStateUnsupported {
+		t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
+	}
 }
 
 func Test_ManifestStore_Resolve(t *testing.T) {
@@ -5194,108 +5179,6 @@ func TestRepository_SetReferrersCapability(t *testing.T) {
 	}
 }
 
-func Test_isReferrersFilterApplied(t *testing.T) {
-	tests := []struct {
-		name        string
-		annotations map[string]string
-		requested   string
-		want        bool
-	}{
-		{
-			name:        "single filter applied, specified filter matches",
-			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "artifactType"},
-			requested:   "artifactType",
-			want:        true,
-		},
-		{
-			name:        "single filter applied, specified filter does not match",
-			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "foo"},
-			requested:   "artifactType",
-			want:        false,
-		},
-		{
-			name:        "multiple filters applied, specified filter matches",
-			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "foo,artifactType"},
-			requested:   "artifactType",
-			want:        true,
-		},
-		{
-			name:        "multiple filters applied, specified filter does not match",
-			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "foo,bar"},
-			requested:   "artifactType",
-			want:        false,
-		},
-		{
-			name:        "single filter applied, specified filter empty",
-			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: "foo"},
-			requested:   "",
-			want:        false,
-		},
-		{
-			name:        "no filter applied",
-			annotations: map[string]string{},
-			requested:   "artifactType",
-			want:        false,
-		},
-		{
-			name:        "empty filter applied",
-			annotations: map[string]string{ocispec.AnnotationReferrersFiltersApplied: ""},
-			requested:   "artifactType",
-			want:        false,
-		},
-		{
-			name:        "no filter applied, specified filter empty",
-			annotations: map[string]string{},
-			requested:   "",
-			want:        false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := isReferrersFilterApplied(tt.annotations, tt.requested); got != tt.want {
-				t.Errorf("isReferrersFilterApplied() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-func Test_getReferrersTag(t *testing.T) {
-	tests := []struct {
-		name string
-		desc ocispec.Descriptor
-		want string
-	}{
-		{
-			name: "zero digest",
-			desc: ocispec.Descriptor{
-				Digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
-			},
-			want: "sha256-0000000000000000000000000000000000000000000000000000000000000000",
-		},
-		{
-			name: "sha256",
-			desc: ocispec.Descriptor{
-				Digest: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
-			},
-			want: "sha256-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
-		},
-		{
-			name: "sha512",
-			desc: ocispec.Descriptor{
-				Digest: "sha512:ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
-			},
-			want: "sha512-ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := buildReferrersTag(tt.desc); got != tt.want {
-				t.Errorf("getReferrersTag() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
 func Test_generateIndex(t *testing.T) {
 	referrer_1 := ocispec.Artifact{
 		MediaType:    ocispec.MediaTypeArtifactManifest,