Skip to content

Commit aa8d9ef

Browse files
authored
Merge pull request #4624 from jedevc/control-mount-content-cache
ExecOp: update content-cache mount logic
2 parents 7c0d261 + ed2efe3 commit aa8d9ef

6 files changed

Lines changed: 517 additions & 225 deletions

File tree

client/llb/exec.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type mount struct {
4646
tmpfsOpt TmpfsInfo
4747
cacheSharing CacheMountSharingMode
4848
noOutput bool
49+
contentCache MountContentCache
4950
}
5051

5152
type ExecOp struct {
@@ -281,6 +282,9 @@ func (e *ExecOp) Marshal(ctx context.Context, c *Constraints) (digest.Digest, []
281282
} else if m.source != nil {
282283
addCap(&e.constraints, pb.CapExecMountBind)
283284
}
285+
if m.contentCache != MountContentCacheDefault {
286+
addCap(&e.constraints, pb.CapExecMountContentCache)
287+
}
284288
}
285289

286290
if len(e.secrets) > 0 {
@@ -366,6 +370,14 @@ func (e *ExecOp) Marshal(ctx context.Context, c *Constraints) (digest.Digest, []
366370
pm.CacheOpt.Sharing = pb.CacheSharingOpt_LOCKED
367371
}
368372
}
373+
switch m.contentCache {
374+
case MountContentCacheDefault:
375+
pm.ContentCache = pb.MountContentCache_DEFAULT
376+
case MountContentCacheOn:
377+
pm.ContentCache = pb.MountContentCache_ON
378+
case MountContentCacheOff:
379+
pm.ContentCache = pb.MountContentCache_OFF
380+
}
369381
if m.tmpfs {
370382
pm.MountType = pb.MountType_TMPFS
371383
pm.TmpfsOpt = &pb.TmpfsOpt{
@@ -492,6 +504,12 @@ func ForceNoOutput(m *mount) {
492504
m.noOutput = true
493505
}
494506

507+
func ContentCache(cache MountContentCache) MountOption {
508+
return func(m *mount) {
509+
m.contentCache = cache
510+
}
511+
}
512+
495513
func AsPersistentCacheDir(id string, sharing CacheMountSharingMode) MountOption {
496514
return func(m *mount) {
497515
m.cacheID = id
@@ -783,3 +801,11 @@ const (
783801
UlimitSigpending UlimitName = "sigpending"
784802
UlimitStack UlimitName = "stack"
785803
)
804+
805+
type MountContentCache int
806+
807+
const (
808+
MountContentCacheDefault MountContentCache = iota
809+
MountContentCacheOn
810+
MountContentCacheOff
811+
)

solver/llbsolver/ops/exec.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func (e *ExecOp) CacheMap(ctx context.Context, g session.Group, index int) (*sol
212212
}
213213
cm.Deps[i].Selector = digest.FromBytes(bytes.Join(dgsts, []byte{0}))
214214
}
215-
if !dep.NoContentBasedHash {
215+
if dep.ContentBasedHash {
216216
cm.Deps[i].ComputeDigestFunc = opsutils.NewContentHashFunc(toSelectors(dedupePaths(dep.Selectors)))
217217
}
218218
cm.Deps[i].PreprocessFunc = unlazyResultFunc
@@ -275,8 +275,11 @@ func toSelectors(p []string) []opsutils.Selector {
275275
}
276276

277277
type dep struct {
278-
Selectors []string
279-
NoContentBasedHash bool
278+
Selectors []string
279+
280+
// ContentBasedHash enables content-based caching. This is used to ensure
281+
// that all caching is done safely and efficiently.
282+
ContentBasedHash bool
280283
}
281284

282285
func (e *ExecOp) getMountDeps() ([]dep, error) {
@@ -292,9 +295,53 @@ func (e *ExecOp) getMountDeps() ([]dep, error) {
292295
sel := path.Join("/", m.Selector)
293296
deps[m.Input].Selectors = append(deps[m.Input].Selectors, sel)
294297

295-
if (!m.Readonly || m.Dest == pb.RootMount) && m.Output != -1 { // exclude read-only rootfs && read-write mounts
296-
deps[m.Input].NoContentBasedHash = true
298+
// Assume that we *cannot* perform content-based caching, and then
299+
// enable it selectively only for cases where we want to
300+
contentBasedCache := false
301+
302+
// Allow content-based cached where safe - these are enforced to avoid
303+
// the following case:
304+
// - A "snapshot" contains "foo/a.txt" and "bar/b.txt"
305+
// - "RUN --mount from=snapshot,src=bar touch bar/c.txt" creates a new
306+
// file in bar
307+
// - If we run again, but this time "snapshot" contains a new
308+
// "foo/sneaky.txt", the content-based cache matches the previous
309+
// run, since we only select "bar"
310+
// - But this cached result is incorrect - "foo/sneaky.txt" isn't in
311+
// our cached result, but it is in our input.
312+
if m.Output == pb.SkipOutput {
313+
// if the mount has no outputs, it's safe to enable content-based
314+
// caching, since it's guaranteed to not be used as an input for
315+
// any future steps
316+
contentBasedCache = true
317+
} else if m.Readonly {
318+
// if the mount is read-only, then it's also safe, since it can't
319+
// be modified by the operation
320+
contentBasedCache = true
321+
} else if sel == pb.RootMount {
322+
// if the mount mounts the entire source, then it's also safe,
323+
// since there are no unselected "sneaky" files
324+
contentBasedCache = true
325+
}
326+
327+
// Now apply the user-specified option.
328+
switch m.ContentCache {
329+
case pb.MountContentCache_OFF:
330+
contentBasedCache = false
331+
case pb.MountContentCache_ON:
332+
if !contentBasedCache {
333+
// If we can't enable cache for safety, then force-enabling it is invalid
334+
return nil, errors.Errorf("invalid mount cache content %v", m)
335+
}
336+
case pb.MountContentCache_DEFAULT:
337+
if m.Dest == pb.RootMount {
338+
// we explicitly choose to not implement it on the root mount,
339+
// since this is likely very expensive (and not incredibly useful)
340+
contentBasedCache = false
341+
}
297342
}
343+
344+
deps[m.Input].ContentBasedHash = contentBasedCache
298345
}
299346
return deps, nil
300347
}

0 commit comments

Comments
 (0)