exec: refactor content-based cache detection

This refactors the content-based cache to be that little bit tidier. In
addition to adding comments that explain *why* we're even bothering,
this restructures the code to avoid being unclear.

To explain the changes in a little more detail (since it's not
abundantly clear why this translation is valid), the initial condition
looks like:

	if (!m.Readonly || m.Dest == pb.RootMount) && m.Output != -1 {
		deps[m.Input].NoContentBasedHash = true

We can apply De Morgan's law recursively to invert the condition and the
result:

	deps[m.Input].NoContentBasedHash = true
	if (m.Readonly && m.Dest != pb.RootMount) || m.Output == -1 {
		deps[m.Input].NoContentBasedHash = false

With all the juggling of NoContentBasedCache, we invert the variable
name to be ContentBasedCache (and invert everywhere it's used as well):

	if (m.Readonly && m.Dest != pb.RootMount) || m.Output == -1 {
		deps[m.Input].ContentBasedHash = true

This reads a bit easier, but now we split this into two separate
branches for readability (and so we can comment each one in more detail
separately):

	if m.Readonly && m.Dest != pb.RootMount {
		deps[m.Input].ContentBasedHash = true
	}
	if m.Output == -1 {
		deps[m.Input].ContentBasedHash = true
	}

While this has been the behavior for ages, I think it makes sense to
deliberately this behavior slightly. It doesn't make sense to me that we
should only disallow read-only root mounts, but no-output root mounts
are allowed - there's no reason these shouldn't behave identically, by
splitting these out:

	if m.Readonly {
		deps[m.Input].ContentBasedHash = true
	}
	if m.Output == -1 {
		deps[m.Input].ContentBasedHash = true
	}

	if m.Dest == pb.RootMount {
		deps[m.Input].ContentBasedHash = false
	}

There is a small chance that this is a breaking change for some users,
however, 1. SkipOutput (-1) is very rare and not often used in the wild
(except for dockerfiles, where it's only used for non-root mounts), and 2.
will only cause a cache miss.

Signed-off-by: Justin Chadwell <me@jedevc.com>
This commit is contained in:
Justin Chadwell
2024-02-05 13:41:26 +00:00
parent b09a9f1afe
commit 98bfcf44ce

View File

@@ -212,7 +212,7 @@ func (e *ExecOp) CacheMap(ctx context.Context, g session.Group, index int) (*sol
}
cm.Deps[i].Selector = digest.FromBytes(bytes.Join(dgsts, []byte{0}))
}
if !dep.NoContentBasedHash {
if dep.ContentBasedHash {
cm.Deps[i].ComputeDigestFunc = opsutils.NewContentHashFunc(toSelectors(dedupePaths(dep.Selectors)))
}
cm.Deps[i].PreprocessFunc = unlazyResultFunc
@@ -275,8 +275,11 @@ func toSelectors(p []string) []opsutils.Selector {
}
type dep struct {
Selectors []string
NoContentBasedHash bool
Selectors []string
// ContentBasedHash enables content-based caching. This is used to ensure
// that all caching is done safely and efficiently.
ContentBasedHash bool
}
func (e *ExecOp) getMountDeps() ([]dep, error) {
@@ -292,9 +295,38 @@ func (e *ExecOp) getMountDeps() ([]dep, error) {
sel := path.Join("/", m.Selector)
deps[m.Input].Selectors = append(deps[m.Input].Selectors, sel)
if (!m.Readonly || m.Dest == pb.RootMount) && m.Output != -1 { // exclude read-only rootfs && read-write mounts
deps[m.Input].NoContentBasedHash = true
// Assume that we *cannot* perform content-based caching, and then
// enable it selectively only for cases where we want to
contentBasedCache := false
// Allow content-based cached where safe - these are enforced to avoid
// the following case:
// - A "snapshot" contains "foo/a.txt" and "bar/b.txt"
// - "RUN --mount from=snapshot,src=bar touch bar/c.txt" creates a new
// file in bar
// - If we run again, but this time "snapshot" contains a new
// "foo/sneaky.txt", the content-based cache matches the previous
// run, since we only select "bar"
// - But this cached result is incorrect - "foo/sneaky.txt" isn't in
// our cached result, but it is in our input.
if m.Output == pb.SkipOutput {
// if the mount has no outputs, it's safe to enable content-based
// caching, since it's guaranteed to not be used as an input for
// any future steps
contentBasedCache = true
} else if m.Readonly {
// if the mount is read-only, then it's also safe, since it can't
// be modified by the operation
contentBasedCache = true
}
if m.Dest == pb.RootMount {
// we explicitly choose to not implement it on the root mount,
// since this is likely very expensive (and not incredibly useful)
contentBasedCache = false
}
deps[m.Input].ContentBasedHash = contentBasedCache
}
return deps, nil
}