client: add test case for multiple parallel builds.

It appears that multiple parallel builds suffer some cross talk between their
mount/snapshots e.g. this new case currently fails because iterations see files
in `/src/` which correspond to other iterations (i.e. iteration 1 might see a
file called "test2.txt"). The `ls -i` and `stat` output also confirms that
where tests see the same filename they are also apparently seeing the same
inode.

Note that each solve uses a `SolverOpt.SharedKey` of the (unique) source
directory so there should be no clashes. Use of `llb.sharedKeyHint()` is
avoided since using a unique one of these hides the issue (AIUI uniqueness of
`sharedKeyHint` is only required with a session, not between them).

The shell fragment within the test is a bit crazy, it tries to output as much
info about what it saw vs what was expected to aid debugging.

I have also been seeing cases in my own code where `cacheContext.checksum`
fails the `not found` test at the top, which seems like either the wrong thing
is mounted or it has gone away or otherwise been mutated unexpectedly. I had
been hoping that this test case would also reproduce this, but so far it has
not.

Signed-off-by: Ian Campbell <ijc@docker.com>
docker-18.09
Ian Campbell 2018-07-06 13:03:53 +01:00 committed by Tonis Tiigi
parent 4e6f270472
commit 0dd25df7e2
1 changed files with 72 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import (
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
@ -29,6 +30,7 @@ import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
func TestClientIntegration(t *testing.T) {
@ -55,6 +57,7 @@ func TestClientIntegration(t *testing.T) {
testSharedCacheMounts,
testLockedCacheMounts,
testDuplicateCacheMount,
testParallelBuilds,
})
}
@ -1346,3 +1349,72 @@ func tmpdir(appliers ...fstest.Applier) (string, error) {
}
return tmpdir, nil
}
func testParallelBuilds(t *testing.T, sb integration.Sandbox) {
t.Parallel()
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
eg, ctx := errgroup.WithContext(ctx)
for i := 0; i < 3; i++ {
i := i
eg.Go(func() error {
srcDir, err := ioutil.TempDir("", "buildkit")
require.NoError(t, err)
defer os.RemoveAll(srcDir)
fn := fmt.Sprintf("test%d.txt", i)
exp := fmt.Sprintf("This is iteration %d\n", i)
err = ioutil.WriteFile(filepath.Join(srcDir, fn), []byte(exp), 0666)
require.NoError(t, err)
src := llb.Local("source")
run := llb.Image("busybox:latest").Dir("/src").Run(
llb.ReadonlyRootFS(),
// Everything goes to stderr for coherent/ordered logging.
// The `ls` records inodes in order to more easily spot clashes.
llb.Args([]string{"/bin/sh", "-c", "set -x ; 1>&2 echo Start " + fn + " 1>&2; ls -ilRt /src /out 1>&2 ; cat " + fn + " 1>&2 ; cat test*.txt 1>&2 ; stat test*.txt 1>&2 ; /bin/cp -v " + fn + " /out/test.txt 1>&2 ; echo End " + fn + " 1>&2"}),
llb.AddMount("/src", src, llb.Readonly),
)
st := run.AddMount("/out", llb.Scratch())
def, err := st.Marshal()
require.NoError(t, err)
destDir, err := ioutil.TempDir("", "buildkit")
require.NoError(t, err)
defer os.RemoveAll(destDir)
t.Logf(`Iteration %d is copying %q to %q`, i, filepath.Join(srcDir, fn), filepath.Join(destDir, "test.txt"))
rsp, err := c.Solve(ctx, def, SolveOpt{
Exporter: ExporterLocal,
ExporterOutputDir: destDir,
SharedKey: srcDir,
LocalDirs: map[string]string{
"source": srcDir,
},
}, nil)
require.NoError(t, err)
for k, v := range rsp.ExporterResponse {
t.Logf("solve response: %s=%s", k, v)
}
act, err := ioutil.ReadFile(filepath.Join(destDir, "test.txt"))
require.NoError(t, err)
require.Equal(t, exp, string(act))
return nil
})
}
err = eg.Wait()
require.NoError(t, err)
}