Skip to content

Commit faaa1dd

Browse files
feat: GC on oci.Store.Delete (#653)
Part of #472, this PR implements recursive GC for `oci.Store`. A field `AutoGarbageCollection` of `oci.Store` is added, default value is true. Signed-off-by: Xiaoxuan Wang <wangxiaoxuan119@gmail.com>
1 parent 1d9ad6c commit faaa1dd

File tree

4 files changed

+593
-34
lines changed

4 files changed

+593
-34
lines changed

content/oci/oci.go

+87-18
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"oras.land/oras-go/v2/internal/descriptor"
3737
"oras.land/oras-go/v2/internal/graph"
3838
"oras.land/oras-go/v2/internal/resolver"
39+
"oras.land/oras-go/v2/registry"
3940
)
4041

4142
// Store implements `oras.Target`, and represents a content store
@@ -52,12 +53,26 @@ type Store struct {
5253
// to manually call SaveIndex() when needed.
5354
// - Default value: true.
5455
AutoSaveIndex bool
55-
root string
56-
indexPath string
57-
index *ocispec.Index
58-
storage *Storage
59-
tagResolver *resolver.Memory
60-
graph *graph.Memory
56+
57+
// AutoGC controls if the OCI store will automatically clean newly produced
58+
// dangling (unreferenced) blobs during Delete() operation. For example the
59+
// blobs whose manifests have been deleted. Manifests in index.json will not
60+
// be deleted.
61+
// - Default value: true.
62+
AutoGC bool
63+
64+
// AutoDeleteReferrers controls if the OCI store will automatically delete its
65+
// referrers when a manifest is deleted. When set to true, the referrers will
66+
// be deleted even if they exist in index.json.
67+
// - Default value: true.
68+
AutoDeleteReferrers bool
69+
70+
root string
71+
indexPath string
72+
index *ocispec.Index
73+
storage *Storage
74+
tagResolver *resolver.Memory
75+
graph *graph.Memory
6176

6277
// sync ensures that most operations can be done concurrently, while Delete
6378
// has the exclusive access to Store if a delete operation is underway. Operations
@@ -84,12 +99,14 @@ func NewWithContext(ctx context.Context, root string) (*Store, error) {
8499
}
85100

86101
store := &Store{
87-
AutoSaveIndex: true,
88-
root: rootAbs,
89-
indexPath: filepath.Join(rootAbs, ocispec.ImageIndexFile),
90-
storage: storage,
91-
tagResolver: resolver.NewMemory(),
92-
graph: graph.NewMemory(),
102+
AutoSaveIndex: true,
103+
AutoGC: true,
104+
AutoDeleteReferrers: true,
105+
root: rootAbs,
106+
indexPath: filepath.Join(rootAbs, ocispec.ImageIndexFile),
107+
storage: storage,
108+
tagResolver: resolver.NewMemory(),
109+
graph: graph.NewMemory(),
93110
}
94111

95112
if err := ensureDir(filepath.Join(rootAbs, ocispec.ImageBlobsDir)); err != nil {
@@ -143,11 +160,49 @@ func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, er
143160

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

171+
deleteQueue := []ocispec.Descriptor{target}
172+
for len(deleteQueue) > 0 {
173+
head := deleteQueue[0]
174+
deleteQueue = deleteQueue[1:]
175+
176+
// get referrers if applicable
177+
if s.AutoDeleteReferrers && descriptor.IsManifest(head) {
178+
referrers, err := registry.Referrers(ctx, &unsafeStore{s}, head, "")
179+
if err != nil {
180+
return err
181+
}
182+
deleteQueue = append(deleteQueue, referrers...)
183+
}
184+
185+
// delete the head of queue
186+
danglings, err := s.delete(ctx, head)
187+
if err != nil {
188+
return err
189+
}
190+
if s.AutoGC {
191+
for _, d := range danglings {
192+
// do not delete existing manifests in tagResolver
193+
_, err = s.tagResolver.Resolve(ctx, string(d.Digest))
194+
if errors.Is(err, errdef.ErrNotFound) {
195+
deleteQueue = append(deleteQueue, d)
196+
}
197+
}
198+
}
199+
}
200+
201+
return nil
202+
}
203+
204+
// delete deletes one node and returns the dangling nodes caused by the delete.
205+
func (s *Store) delete(ctx context.Context, target ocispec.Descriptor) ([]ocispec.Descriptor, error) {
151206
resolvers := s.tagResolver.Map()
152207
untagged := false
153208
for reference, desc := range resolvers {
@@ -156,16 +211,17 @@ func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error {
156211
untagged = true
157212
}
158213
}
159-
if err := s.graph.Remove(ctx, target); err != nil {
160-
return err
161-
}
214+
danglings := s.graph.Remove(target)
162215
if untagged && s.AutoSaveIndex {
163216
err := s.saveIndex()
164217
if err != nil {
165-
return err
218+
return nil, err
166219
}
167220
}
168-
return s.storage.Delete(ctx, target)
221+
if err := s.storage.Delete(ctx, target); err != nil {
222+
return nil, err
223+
}
224+
return danglings, nil
169225
}
170226

171227
// Tag tags a descriptor with a reference string.
@@ -398,6 +454,19 @@ func (s *Store) writeIndexFile() error {
398454
return os.WriteFile(s.indexPath, indexJSON, 0666)
399455
}
400456

457+
// unsafeStore is used to bypass lock restrictions in Delete.
458+
type unsafeStore struct {
459+
*Store
460+
}
461+
462+
func (s *unsafeStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
463+
return s.storage.Fetch(ctx, target)
464+
}
465+
466+
func (s *unsafeStore) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
467+
return s.graph.Predecessors(ctx, node)
468+
}
469+
401470
// validateReference validates ref.
402471
func validateReference(ref string) error {
403472
if ref == "" {

0 commit comments

Comments
 (0)