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

Commit 34e06f4

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 34e06f4

3 files changed

Lines changed: 14 additions & 8 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.25.0
44

55
require (
66
github.com/DATA-DOG/go-sqlmock v1.5.2
7-
github.com/cocoonstack/cocoon-common v0.1.9
7+
github.com/cocoonstack/cocoon-common v0.1.10-0.20260423175920-c8cd308978cb
88
github.com/go-sql-driver/mysql v1.9.3
99
github.com/google/uuid v1.6.0
1010
github.com/gorilla/mux v1.8.1

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5w
2727
github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
2828
github.com/cocoonstack/cocoon-common v0.1.9 h1:WRPhYzO+X8fkpaOHRi+DzYOLinLbiCInaUyc8vyqrSg=
2929
github.com/cocoonstack/cocoon-common v0.1.9/go.mod h1:cjBGOeZvsw2J5X7RIQJ44PJx0QiwgypEJVdd5V38arU=
30+
github.com/cocoonstack/cocoon-common v0.1.10-0.20260423173325-d4fd094bb36e h1:0iN1aegxOHpwp5eWm5AzbtaGruV0f6c3H5uXHSXzybs=
31+
github.com/cocoonstack/cocoon-common v0.1.10-0.20260423173325-d4fd094bb36e/go.mod h1:cjBGOeZvsw2J5X7RIQJ44PJx0QiwgypEJVdd5V38arU=
32+
github.com/cocoonstack/cocoon-common v0.1.10-0.20260423175920-c8cd308978cb h1:s31sByTK+6ik1BOaAirpKZ1O9q3OSzT8kAiECci5T0k=
33+
github.com/cocoonstack/cocoon-common v0.1.10-0.20260423175920-c8cd308978cb/go.mod h1:cjBGOeZvsw2J5X7RIQJ44PJx0QiwgypEJVdd5V38arU=
3034
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
3135
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
3236
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=

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)