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

  1. Re-list the blob by digest; if it no longer exists, treat Touch as success (idempotent no-op) and continue the sweep.
  2. Ensure only one GC execution runs at a time (Harbor enforces this for its own GC; make custom jobs respect it too).
  3. 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

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.