goharbor/harbor · error · lib/errors.Error
NOT_FOUND
NOT_FOUND
Error message
no blob item is updated to StatusNone, id:%d, digest:%s
What it means
Touch() sets blob.Status = StatusNone and issues an UPDATE via UpdateBlobStatus. The manager returns the number of affected rows; when that count is 0 (no row matches the blob's ID/digest/status transition) Harbor returns NOT_FOUND including the blob's id and digest. It means the state-machine transition 'to StatusNone' matched nothing - almost always a concurrently deleted or already-removed blob.
Source
Thrown at src/controller/blob/controller.go:366
if err != nil {
if err == redis.Nil {
return 0, nil
}
return 0, err
}
return size, nil
}
func (c *controller) Touch(ctx context.Context, blob *blob.Blob) error {
blob.Status = blob_models.StatusNone
count, err := c.blobMgr.UpdateBlobStatus(ctx, blob)
if err != nil {
return err
}
if count == 0 {
return errors.New(nil).WithMessagef("no blob item is updated to StatusNone, id:%d, digest:%s", blob.ID, blob.Digest).WithCode(errors.NotFoundCode)
}
return nil
}
func (c *controller) Fail(ctx context.Context, blob *blob.Blob) error {
blob.Status = blob_models.StatusDeleteFailed
count, err := c.blobMgr.UpdateBlobStatus(ctx, blob)
if err != nil {
return err
}
if count == 0 {
return errors.New(nil).WithMessagef("no blob item is updated to StatusDeleteFailed, id:%d, digest:%s", blob.ID, blob.Digest).WithCode(errors.NotFoundCode)
}
return nil
}
func (c *controller) Update(ctx context.Context, blob *blob.Blob) error {
return c.blobMgr.Update(ctx, blob)View on GitHub (pinned to 7b2fd08cc5)
Solutions
- Re-list the blob by digest; if it no longer exists, treat Touch as success (idempotent no-op) and continue the sweep.
- Ensure only one GC execution runs at a time (Harbor enforces this for its own GC; make custom jobs respect it too).
- If the row still exists, reload it and check whether its status already advanced, then retry the transition.
Example fix
// before
if err := c.Touch(ctx, b); err != nil { return err }
// after
if err := c.Touch(ctx, b); err != nil {
if liberrors.IsErr(err, liberrors.NotFoundCode) {
// already removed by a concurrent run - not an error
return nil
}
return err
} Defensive patterns
Strategy: fallback
Validate before calling
// before Touch, confirm the blob still exists:
exists, err := blobCtl.Exists(ctx, b.Digest)
if err == nil && !exists { return nil /* nothing to touch */ } Try / catch
if err := c.Touch(ctx, b); err != nil {
if liberrors.IsErr(err, liberrors.NotFoundCode) {
// row gone (concurrent delete): treat as done, keep sweeping
return nil
}
return err
} Prevention
- Make blob-state transitions idempotent; NotFound on Touch means already removed.
- Serialize GC executions so blobs are not deleted under an active sweep.
- Log blob id+digest on this path to identify overlapping cleaners.
When it happens
Trigger: GC or artifact-deletion flow calls Touch for a blob that another concurrent GC run already deleted; a retried job touches a blob whose row was removed between the earlier List and this UPDATE; the DB was manually pruned.
Common situations: Overlapping GC executions, retry of a failed delete job after cleanup completed, long-running jobs holding stale blob references.
Related errors
AI-assisted analysis of goharbor/harbor@7b2fd08cc5 (2026-08-16).
Data as JSON: /api/errors/35d1ca315ebba1f1.
Report an issue: GitHub.