Skip to content
This repository was archived by the owner on Jul 4, 2026. It is now read-only.

Commit de079d2

Browse files
committed
fix: make digest delete retryable after partial tag unlink failure
DeleteManifestByDigest deleted the _digests copy first and then unlinked matching tags. If tag unlinking failed partway, the digest file was already gone — a retry hit the Exists/ErrNotFound early-exit and never touched the surviving tag-addressed manifests. The claim that catalog sync would reap those survivors was wrong: ManifestJSON reads the tag file from object store directly, sync removes DB rows not object store files, so any remaining tag file stayed fully reachable. Unlink tags first, delete the digest copy last. unlinkTagsByDigest is idempotent (object-store Delete swallows not-found, catalog removeCatalogEntry is a no-op on missing entries), so a retry re-enters, re-enumerates surviving tags, and resumes cleanup from wherever the previous attempt failed.
1 parent 9ba2f4f commit de079d2

1 file changed

Lines changed: 9 additions & 7 deletions

File tree

registry/registry.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ func (r *Registry) DeleteManifest(ctx context.Context, name, tag string) error {
137137
// Returns ErrNotFound when the digest copy does not exist, so callers can
138138
// surface the canonical 404 instead of a silent 202 after the object store
139139
// swallows the missing-object error on Delete.
140+
//
141+
// Unlinking runs BEFORE the digest copy is removed so the operation stays
142+
// retryable: a partial failure leaves the digest file in place, and a retry
143+
// re-enters this function, re-lists surviving tags, and resumes cleanup.
144+
// unlinkTagsByDigest is itself idempotent — object-store Delete swallows
145+
// not-found and removeCatalogEntry is a no-op on missing entries.
140146
func (r *Registry) DeleteManifestByDigest(ctx context.Context, name, digest string) error {
141147
key := manifestDigestKey(name, digest)
142148
exists, err := r.client.Exists(ctx, key)
@@ -146,16 +152,12 @@ func (r *Registry) DeleteManifestByDigest(ctx context.Context, name, digest stri
146152
if !exists {
147153
return fmt.Errorf("manifest %s@%s: %w", name, digest, objectstore.ErrNotFound)
148154
}
149-
if err := r.client.Delete(ctx, key); err != nil {
150-
return fmt.Errorf("delete manifest %s@%s: %w", name, digest, err)
151-
}
152-
// Tag unlinking runs after the digest copy is gone. If it fails partway,
153-
// the digest-delete itself already succeeded; any remaining tag rows will
154-
// be reaped by the next catalog sync or a later tag-targeted delete. We
155-
// don't roll the digest copy back into the store on partial failure.
156155
if err := r.unlinkTagsByDigest(ctx, name, digest); err != nil {
157156
return fmt.Errorf("unlink tags for %s@%s: %w", name, digest, err)
158157
}
158+
if err := r.client.Delete(ctx, key); err != nil {
159+
return fmt.Errorf("delete manifest %s@%s: %w", name, digest, err)
160+
}
159161
return nil
160162
}
161163

0 commit comments

Comments
 (0)