diff --git a/go/test/endtoend/docker/vttestserver.go b/go/test/endtoend/docker/vttestserver.go new file mode 100644 index 00000000000..e6bc93d4e6c --- /dev/null +++ b/go/test/endtoend/docker/vttestserver.go @@ -0,0 +1,165 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package docker + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path" + "strconv" + "strings" + "time" + + "vitess.io/vitess/go/vt/log" +) + +const ( + vttestserverMysql57image = "vttestserver-e2etest/mysql57" + vttestserverMysql80image = "vttestserver-e2etest/mysql80" +) + +type vttestserver struct { + dockerImage string + keyspaces []string + numShards []int + mysqlMaxConnecetions int + port int +} + +func newVttestserver(dockerImage string, keyspaces []string, numShards []int, mysqlMaxConnections, port int) *vttestserver { + return &vttestserver{ + dockerImage: dockerImage, + keyspaces: keyspaces, + numShards: numShards, + mysqlMaxConnecetions: mysqlMaxConnections, + port: port, + } +} + +func (v *vttestserver) teardown() { + cmd := exec.Command("docker", "rm", "--force", "vttestserver-end2end-test") + err := cmd.Run() + if err != nil { + log.Errorf("docker teardown failed :- %s", err.Error()) + } +} + +// startDockerImage starts the docker image for the vttestserver +func (v *vttestserver) startDockerImage() error { + cmd := exec.Command("docker", "run") + cmd.Args = append(cmd.Args, "--name=vttestserver-end2end-test") + cmd.Args = append(cmd.Args, "-p", fmt.Sprintf("%d:33577", v.port)) + cmd.Args = append(cmd.Args, "-e", "PORT=33574") + cmd.Args = append(cmd.Args, "-e", fmt.Sprintf("KEYSPACES=%s", strings.Join(v.keyspaces, ","))) + cmd.Args = append(cmd.Args, "-e", fmt.Sprintf("NUM_SHARDS=%s", strings.Join(convertToStringSlice(v.numShards), ","))) + cmd.Args = append(cmd.Args, "-e", "MYSQL_BIND_HOST=0.0.0.0") + cmd.Args = append(cmd.Args, "-e", fmt.Sprintf("MYSQL_MAX_CONNECTIONS=%d", v.mysqlMaxConnecetions)) + cmd.Args = append(cmd.Args, "--health-cmd", "mysqladmin ping -h127.0.0.1 -P33577") + cmd.Args = append(cmd.Args, "--health-interval=5s") + cmd.Args = append(cmd.Args, "--health-timeout=2s") + cmd.Args = append(cmd.Args, "--health-retries=5") + cmd.Args = append(cmd.Args, v.dockerImage) + + err := cmd.Start() + if err != nil { + return err + } + return nil +} + +// dockerStatus is a struct used to unmarshal json output from `docker inspect` +type dockerStatus struct { + State struct { + Health struct { + Status string + } + } +} + +// waitUntilDockerHealthy waits until the docker image is healthy. It takes in as argument the amount of seconds to wait before timeout +func (v *vttestserver) waitUntilDockerHealthy(timeoutDelay int) error { + timeOut := time.After(time.Duration(timeoutDelay) * time.Second) + + for { + select { + case <-timeOut: + // return error due to timeout + return fmt.Errorf("timed out waiting for docker image to start") + case <-time.After(time.Second): + cmd := exec.Command("docker", "inspect", "vttestserver-end2end-test") + out, err := cmd.Output() + if err != nil { + return err + } + var x []dockerStatus + err = json.Unmarshal(out, &x) + if err != nil { + return err + } + if len(x) > 0 { + status := x[0].State.Health.Status + if status == "healthy" { + return nil + } + } + } + } +} + +// convertToStringSlice converts an integer slice to string slice +func convertToStringSlice(intSlice []int) []string { + var stringSlice []string + for _, val := range intSlice { + str := strconv.Itoa(val) + stringSlice = append(stringSlice, str) + } + return stringSlice +} + +//makeVttestserverDockerImages creates the vttestserver docker images for both MySQL57 and MySQL80 +func makeVttestserverDockerImages() error { + mainVitessPath := path.Join(os.Getenv("PWD"), "../../../..") + dockerFilePath := path.Join(mainVitessPath, "docker/vttestserver/Dockerfile.mysql57") + cmd57 := exec.Command("docker", "build", "-f", dockerFilePath, "-t", vttestserverMysql57image, ".") + cmd57.Dir = mainVitessPath + err := cmd57.Start() + if err != nil { + return err + } + + dockerFilePath = path.Join(mainVitessPath, "docker/vttestserver/Dockerfile.mysql80") + cmd80 := exec.Command("docker", "build", "-f", dockerFilePath, "-t", vttestserverMysql80image, ".") + cmd80.Dir = mainVitessPath + err = cmd80.Start() + if err != nil { + return err + } + + err = cmd57.Wait() + if err != nil { + return err + } + + err = cmd80.Wait() + if err != nil { + return err + } + + return nil +} diff --git a/go/test/endtoend/docker/vttestserver_test.go b/go/test/endtoend/docker/vttestserver_test.go new file mode 100644 index 00000000000..797e8f2e5c7 --- /dev/null +++ b/go/test/endtoend/docker/vttestserver_test.go @@ -0,0 +1,196 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package docker + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + + "vitess.io/vitess/go/sqltypes" + + "vitess.io/vitess/go/mysql" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + exitCode := func() int { + err := makeVttestserverDockerImages() + if err != nil { + return 1 + } + return m.Run() + }() + os.Exit(exitCode) +} + +func TestUnsharded(t *testing.T) { + dockerImages := []string{vttestserverMysql57image, vttestserverMysql80image} + for _, image := range dockerImages { + t.Run(image, func(t *testing.T) { + vtest := newVttestserver(image, []string{"unsharded_ks"}, []int{1}, 1000, 33577) + err := vtest.startDockerImage() + require.NoError(t, err) + defer vtest.teardown() + + // wait for the docker to be setup + err = vtest.waitUntilDockerHealthy(10) + require.NoError(t, err) + + ctx := context.Background() + vttestParams := mysql.ConnParams{ + Host: "localhost", + Port: vtest.port, + } + conn, err := mysql.Connect(ctx, &vttestParams) + require.NoError(t, err) + defer conn.Close() + assertMatches(t, conn, "show databases", `[[VARCHAR("unsharded_ks")] [VARCHAR("information_schema")] [VARCHAR("mysql")] [VARCHAR("sys")] [VARCHAR("performance_schema")]]`) + _, err = execute(t, conn, "create table unsharded_ks.t1(id int)") + require.NoError(t, err) + _, err = execute(t, conn, "insert into unsharded_ks.t1(id) values (10),(20),(30)") + require.NoError(t, err) + assertMatches(t, conn, "select * from unsharded_ks.t1", `[[INT32(10)] [INT32(20)] [INT32(30)]]`) + }) + } +} + +func TestSharded(t *testing.T) { + dockerImages := []string{vttestserverMysql57image, vttestserverMysql80image} + for _, image := range dockerImages { + t.Run(image, func(t *testing.T) { + vtest := newVttestserver(image, []string{"ks"}, []int{2}, 1000, 33577) + err := vtest.startDockerImage() + require.NoError(t, err) + defer vtest.teardown() + + // wait for the docker to be setup + err = vtest.waitUntilDockerHealthy(10) + require.NoError(t, err) + + ctx := context.Background() + vttestParams := mysql.ConnParams{ + Host: "localhost", + Port: vtest.port, + } + conn, err := mysql.Connect(ctx, &vttestParams) + require.NoError(t, err) + defer conn.Close() + assertMatches(t, conn, "show databases", `[[VARCHAR("ks")] [VARCHAR("information_schema")] [VARCHAR("mysql")] [VARCHAR("sys")] [VARCHAR("performance_schema")]]`) + _, err = execute(t, conn, "create table ks.t1(id int)") + require.NoError(t, err) + _, err = execute(t, conn, "alter vschema on ks.t1 add vindex `binary_md5`(id) using `binary_md5`") + require.NoError(t, err) + _, err = execute(t, conn, "insert into ks.t1(id) values (10),(20),(30)") + require.NoError(t, err) + assertMatches(t, conn, "select id from ks.t1 order by id", `[[INT32(10)] [INT32(20)] [INT32(30)]]`) + }) + } +} + +func TestMysqlMaxCons(t *testing.T) { + dockerImages := []string{vttestserverMysql57image, vttestserverMysql80image} + for _, image := range dockerImages { + t.Run(image, func(t *testing.T) { + vtest := newVttestserver(image, []string{"ks"}, []int{2}, 100000, 33577) + err := vtest.startDockerImage() + require.NoError(t, err) + defer vtest.teardown() + + // wait for the docker to be setup + err = vtest.waitUntilDockerHealthy(10) + require.NoError(t, err) + + ctx := context.Background() + vttestParams := mysql.ConnParams{ + Host: "localhost", + Port: vtest.port, + } + conn, err := mysql.Connect(ctx, &vttestParams) + require.NoError(t, err) + defer conn.Close() + assertMatches(t, conn, "select @@max_connections", `[[UINT64(100000)]]`) + }) + } +} + +func TestLargeNumberOfKeyspaces(t *testing.T) { + dockerImages := []string{vttestserverMysql57image, vttestserverMysql80image} + for _, image := range dockerImages { + t.Run(image, func(t *testing.T) { + var keyspaces []string + var numShards []int + for i := 0; i < 100; i++ { + keyspaces = append(keyspaces, fmt.Sprintf("unsharded_ks%d", i)) + numShards = append(numShards, 1) + } + + vtest := newVttestserver(image, keyspaces, numShards, 100000, 33577) + err := vtest.startDockerImage() + require.NoError(t, err) + defer vtest.teardown() + + // wait for the docker to be setup + err = vtest.waitUntilDockerHealthy(15) + require.NoError(t, err) + + ctx := context.Background() + vttestParams := mysql.ConnParams{ + Host: "localhost", + Port: vtest.port, + } + conn, err := mysql.Connect(ctx, &vttestParams) + require.NoError(t, err) + defer conn.Close() + + // assert that all the keyspaces are correctly setup + for _, keyspace := range keyspaces { + _, err = execute(t, conn, "create table "+keyspace+".t1(id int)") + require.NoError(t, err) + _, err = execute(t, conn, "insert into "+keyspace+".t1(id) values (10),(20),(30)") + require.NoError(t, err) + assertMatches(t, conn, "select * from "+keyspace+".t1", `[[INT32(10)] [INT32(20)] [INT32(30)]]`) + } + }) + } +} + +func execute(t *testing.T, conn *mysql.Conn, query string) (*sqltypes.Result, error) { + t.Helper() + return conn.ExecuteFetch(query, 1000, true) +} + +func checkedExec(t *testing.T, conn *mysql.Conn, query string) *sqltypes.Result { + t.Helper() + qr, err := conn.ExecuteFetch(query, 1000, true) + require.NoError(t, err) + return qr +} + +func assertMatches(t *testing.T, conn *mysql.Conn, query, expected string) { + t.Helper() + qr := checkedExec(t, conn, query) + got := fmt.Sprintf("%v", qr.Rows) + diff := cmp.Diff(expected, got) + if diff != "" { + t.Errorf("Query: %s (-want +got):\n%s", query, diff) + } +}