@@ -21,21 +21,26 @@ type CacheChains struct {
21
21
visited map [interface {}]struct {}
22
22
}
23
23
24
+ var _ solver.CacheExporterTarget = & CacheChains {}
25
+
24
26
func (c * CacheChains ) Add (dgst digest.Digest ) solver.CacheExporterRecord {
25
27
if strings .HasPrefix (dgst .String (), "random:" ) {
28
+ // random digests will be different *every* run - so we shouldn't cache
29
+ // it, since there's a zero chance this random digest collides again
26
30
return & nopRecord {}
27
31
}
28
- it := & item {c : c , dgst : dgst , backlinks : map [* item ]struct {}{}}
32
+
33
+ it := & item {dgst : dgst , backlinks : map [* item ]struct {}{}}
29
34
c .items = append (c .items , it )
30
35
return it
31
36
}
32
37
33
- func (c * CacheChains ) Visit (v interface {} ) {
34
- c .visited [v ] = struct {}{}
38
+ func (c * CacheChains ) Visit (target any ) {
39
+ c .visited [target ] = struct {}{}
35
40
}
36
41
37
- func (c * CacheChains ) Visited (v interface {} ) bool {
38
- _ , ok := c .visited [v ]
42
+ func (c * CacheChains ) Visited (target any ) bool {
43
+ _ , ok := c .visited [target ]
39
44
return ok
40
45
}
41
46
@@ -76,6 +81,12 @@ func (c *CacheChains) normalize(ctx context.Context) error {
76
81
return nil
77
82
}
78
83
84
+ // Marshal converts the cache chains structure into a cache config and a
85
+ // collection of providers for reading the results from.
86
+ //
87
+ // Marshal aims to validate, normalize and sort the output to ensure a
88
+ // consistent digest (since cache configs are typically uploaded and stored in
89
+ // content-addressable OCI registries).
79
90
func (c * CacheChains ) Marshal (ctx context.Context ) (* CacheConfig , DescriptorProvider , error ) {
80
91
if err := c .normalize (ctx ); err != nil {
81
92
return nil , nil , err
@@ -109,19 +120,37 @@ type DescriptorProviderPair struct {
109
120
Provider content.Provider
110
121
}
111
122
123
+ // item is an implementation of a record in the cache chain. After validation,
124
+ // normalization and marshalling into the cache config, the item results form
125
+ // into the "layers", while the digests and the links form into the "records".
112
126
type item struct {
113
- c * CacheChains
127
+ // dgst is the unique identifier for each record.
128
+ // This *roughly* corresponds to an edge (vertex cachekey + index) in the
129
+ // solver - however, a single vertex can produce multiple unique cache keys
130
+ // (e.g. fast/slow), so it's a one-to-many relation.
114
131
dgst digest.Digest
115
132
133
+ // links are what connect records to each other (with an optional selector),
134
+ // organized by input index (which correspond to vertex inputs).
135
+ // We can have multiple links for each index, since *any* of these could be
136
+ // used to get to this item (e.g. we could retrieve by fast/slow key).
137
+ links []map [link ]struct {}
138
+
139
+ // backlinks are the inverse of a link - these don't actually get directly
140
+ // exported, but they're internally used to help efficiently navigate the
141
+ // graph.
142
+ backlinks map [* item ]struct {}
143
+ backlinksMu sync.Mutex
144
+
145
+ // result is the result of computing the edge - this is the target of the
146
+ // data we actually want to store in the cache chain.
116
147
result * solver.Remote
117
148
resultTime time.Time
118
149
119
- links []map [link ]struct {}
120
- backlinksMu sync.Mutex
121
- backlinks map [* item ]struct {}
122
- invalid bool
150
+ invalid bool
123
151
}
124
152
153
+ // link is a pointer to an item, with an optional selector.
125
154
type link struct {
126
155
src * item
127
156
selector string
@@ -170,25 +199,46 @@ func (c *item) LinkFrom(rec solver.CacheExporterRecord, index int, selector stri
170
199
src .backlinksMu .Unlock ()
171
200
}
172
201
202
+ // validate checks if an item is valid (i.e. each index has at least one link)
203
+ // and marks it as such.
204
+ //
205
+ // Essentially, if an index has no links, it means that this cache record is
206
+ // unreachable by the cache importer, so we should remove it. Once we've marked
207
+ // an item as invalid, we remove it from it's backlinks and check it's
208
+ // validity again - since now this linked item may be unreachable too.
173
209
func (c * item ) validate () {
210
+ if c .invalid {
211
+ // early exit, if the item is already invalid, we've already gone
212
+ // through the backlinks
213
+ return
214
+ }
215
+
174
216
for _ , m := range c .links {
217
+ // if an index has no links, there's no way to access this record, so
218
+ // mark it as invalid
175
219
if len (m ) == 0 {
176
220
c .invalid = true
177
- for bl := range c .backlinks {
178
- changed := false
179
- for _ , m := range bl .links {
180
- for l := range m {
181
- if l .src == c {
182
- delete (m , l )
183
- changed = true
184
- }
221
+ break
222
+ }
223
+ }
224
+
225
+ if c .invalid {
226
+ for bl := range c .backlinks {
227
+ // remove ourselves from the backlinked item
228
+ changed := false
229
+ for _ , m := range bl .links {
230
+ for l := range m {
231
+ if l .src == c {
232
+ delete (m , l )
233
+ changed = true
185
234
}
186
235
}
187
- if changed {
188
- bl .validate ()
189
- }
190
236
}
191
- return
237
+
238
+ // if we've removed ourselves, we need to check it again
239
+ if changed {
240
+ bl .validate ()
241
+ }
192
242
}
193
243
}
194
244
}
@@ -211,6 +261,7 @@ func (c *item) walkAllResults(fn func(i *item) error, visited map[*item]struct{}
211
261
return nil
212
262
}
213
263
264
+ // nopRecord is used to discard cache results that we're not interested in storing.
214
265
type nopRecord struct {
215
266
}
216
267
@@ -219,5 +270,3 @@ func (c *nopRecord) AddResult(_ digest.Digest, _ int, createdAt time.Time, resul
219
270
220
271
func (c * nopRecord ) LinkFrom (rec solver.CacheExporterRecord , index int , selector string ) {
221
272
}
222
-
223
- var _ solver.CacheExporterTarget = & CacheChains {}
0 commit comments