Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: GC on oci.Store.Delete #653

Merged
merged 26 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 87 additions & 18 deletions content/oci/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/graph"
"oras.land/oras-go/v2/internal/resolver"
"oras.land/oras-go/v2/registry"
)

// Store implements `oras.Target`, and represents a content store
Expand All @@ -52,12 +53,26 @@
// to manually call SaveIndex() when needed.
// - Default value: true.
AutoSaveIndex bool
root string
indexPath string
index *ocispec.Index
storage *Storage
tagResolver *resolver.Memory
graph *graph.Memory

// AutoGC controls if the OCI store will automatically clean newly produced
// dangling (unreferenced) blobs during Delete() operation. For example the
// blobs whose manifests have been deleted. Manifests in index.json will not
// be deleted.
// - Default value: true.
AutoGC bool

// AutoDeleteReferrers controls if the OCI store will automatically delete its
// referrers when a manifest is deleted. When set to true, the referrers will
// be deleted even if they exist in index.json.
// - Default value: true.
AutoDeleteReferrers bool

root string
indexPath string
index *ocispec.Index
storage *Storage
tagResolver *resolver.Memory
graph *graph.Memory

// sync ensures that most operations can be done concurrently, while Delete
// has the exclusive access to Store if a delete operation is underway. Operations
Expand All @@ -84,12 +99,14 @@
}

store := &Store{
AutoSaveIndex: true,
root: rootAbs,
indexPath: filepath.Join(rootAbs, ocispec.ImageIndexFile),
storage: storage,
tagResolver: resolver.NewMemory(),
graph: graph.NewMemory(),
AutoSaveIndex: true,
AutoGC: true,
AutoDeleteReferrers: true,
root: rootAbs,
indexPath: filepath.Join(rootAbs, ocispec.ImageIndexFile),
storage: storage,
tagResolver: resolver.NewMemory(),
graph: graph.NewMemory(),
}

if err := ensureDir(filepath.Join(rootAbs, ocispec.ImageBlobsDir)); err != nil {
Expand Down Expand Up @@ -143,11 +160,49 @@

// Delete deletes the content matching the descriptor from the store. Delete may
// fail on certain systems (i.e. NTFS), if there is a process (i.e. an unclosed
// Reader) using target.
// Reader) using target. If s.AutoGC is set to true, Delete will recursively
// remove the dangling blobs caused by the current delete. If s.AutoDeleteReferrers
// is set to true, Delete will recursively remove the referrers of the manifests
// being deleted.
func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error {
s.sync.Lock()
defer s.sync.Unlock()

deleteQueue := []ocispec.Descriptor{target}
for len(deleteQueue) > 0 {
head := deleteQueue[0]
deleteQueue = deleteQueue[1:]

// get referrers if applicable
if s.AutoDeleteReferrers && descriptor.IsManifest(head) {
referrers, err := registry.Referrers(ctx, &unsafeStore{s}, head, "")
if err != nil {
return err
}

Check warning on line 181 in content/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

content/oci/oci.go#L180-L181

Added lines #L180 - L181 were not covered by tests
deleteQueue = append(deleteQueue, referrers...)
}

// delete the head of queue
danglings, err := s.delete(ctx, head)
if err != nil {
return err
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
}

Check warning on line 189 in content/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

content/oci/oci.go#L188-L189

Added lines #L188 - L189 were not covered by tests
if s.AutoGC {
for _, d := range danglings {
// do not delete existing manifests in tagResolver
_, err = s.tagResolver.Resolve(ctx, string(d.Digest))
if errors.Is(err, errdef.ErrNotFound) {
deleteQueue = append(deleteQueue, d)
}
}
}
}

return nil
}

// delete deletes one node and returns the dangling nodes caused by the delete.
func (s *Store) delete(ctx context.Context, target ocispec.Descriptor) ([]ocispec.Descriptor, error) {
resolvers := s.tagResolver.Map()
untagged := false
for reference, desc := range resolvers {
Expand All @@ -156,16 +211,17 @@
untagged = true
}
}
if err := s.graph.Remove(ctx, target); err != nil {
return err
}
danglings := s.graph.Remove(target)
if untagged && s.AutoSaveIndex {
err := s.saveIndex()
if err != nil {
return err
return nil, err

Check warning on line 218 in content/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

content/oci/oci.go#L218

Added line #L218 was not covered by tests
}
}
return s.storage.Delete(ctx, target)
if err := s.storage.Delete(ctx, target); err != nil {
return nil, err
}

Check warning on line 223 in content/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

content/oci/oci.go#L222-L223

Added lines #L222 - L223 were not covered by tests
return danglings, nil
}

// Tag tags a descriptor with a reference string.
Expand Down Expand Up @@ -398,6 +454,19 @@
return os.WriteFile(s.indexPath, indexJSON, 0666)
}

// unsafeStore is used to bypass lock restrictions in Delete.
type unsafeStore struct {
*Store
}

func (s *unsafeStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
return s.storage.Fetch(ctx, target)
}

func (s *unsafeStore) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
return s.graph.Predecessors(ctx, node)
}

// validateReference validates ref.
func validateReference(ref string) error {
if ref == "" {
Expand Down
Loading
Loading