From 0dd25df7e2c1de3133d4adfa07e3c071f4aef360 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Fri, 6 Jul 2018 13:03:53 +0100 Subject: [PATCH] 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 --- client/client_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/client/client_test.go b/client/client_test.go index 8f55dd39..f353c879 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -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) +}