diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 352653101..6c821fe51 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -11,7 +11,7 @@ on: jobs: run_tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Clone the go-tarantool connector uses: actions/checkout@v4 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d82dc3ef8..758bc89a1 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -23,7 +23,7 @@ jobs: # We could replace it with ubuntu-latest after fixing the bug: # https://github.com/tarantool/setup-tarantool/issues/37 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -100,9 +100,10 @@ jobs: run: make deps - name: Run regression tests - run: | - make test - make testrace + run: make test + + - name: Run race tests + run: make testrace - name: Run fuzzing tests if: ${{ matrix.fuzzing }} @@ -116,6 +117,7 @@ jobs: make coveralls - name: Check workability of benchmark tests + if: matrix.golang == 'stable' run: make bench-deps bench DURATION=1x COUNT=1 testing_mac_os: @@ -270,6 +272,10 @@ jobs: run: | cd "${SRCDIR}" make test + + - name: Run race tests + run: | + cd "${SRCDIR}" make testrace - name: Run fuzzing tests @@ -279,6 +285,7 @@ jobs: make fuzzing TAGS="go_tarantool_decimal_fuzzing" - name: Check workability of benchmark tests + if: matrix.golang == 'stable' run: | cd "${SRCDIR}" make bench-deps bench DURATION=1x COUNT=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc5bdd5b..1d595e230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,23 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Extend box with replication information (#427). - The Instance info has been added to ConnectionInfo for GetInfo response (#429). +- Added helpers to run Tarantool config storage (#431). ### Changed +- Changed helpers API `StartTarantool` and `StopTarantool`, now it uses + pointer on `TarantoolInstance`: + * `StartTarantool()` returns `*TarantoolInstance`; + * `StopTarantool()` and `StopTarantoolWithCleanup()` accepts + `*TarantoolInstance` as arguments. +- Field `Cmd` in `TarantoolInstance` struct declared as deprecated. + Suggested `Wait()`, `Stop()` and `Signal()` methods as safer to use + instead of direct `Cmd.Process` access (#431). + ### Fixed +- Fixed flaky test detection on fail start Tarantool instance (#431). + ## [v2.2.1] - 2024-12-17 The release fixes a schema lost after a reconnect. diff --git a/Makefile b/Makefile index ce3505afc..556fad418 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ test-main: coverage: go clean -testcache go get golang.org/x/tools/cmd/cover - go test -tags "$(TAGS)" ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) + go test -tags "$(TAGS)" $(go list ./... | grep -v test_helpers) -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) go tool cover -func=$(COVERAGE_FILE) .PHONY: coveralls diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index e6f1f50c2..323ce37c3 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -98,7 +98,7 @@ var connOpts = tarantool.Opts{ var defaultCountRetry = 5 var defaultTimeoutRetry = 500 * time.Millisecond -var helpInstances []test_helpers.TarantoolInstance +var helpInstances []*test_helpers.TarantoolInstance func TestConnect_error_duplicate(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() @@ -404,7 +404,7 @@ func TestReconnect(t *testing.T) { defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) - err = test_helpers.RestartTarantool(&helpInstances[0]) + err = test_helpers.RestartTarantool(helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -453,7 +453,7 @@ func TestDisconnect_withReconnect(t *testing.T) { require.Nil(t, err) // Restart the server after success. - err = test_helpers.RestartTarantool(&helpInstances[serverId]) + err = test_helpers.RestartTarantool(helpInstances[serverId]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -501,10 +501,10 @@ func TestDisconnectAll(t *testing.T) { defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) - err = test_helpers.RestartTarantool(&helpInstances[0]) + err = test_helpers.RestartTarantool(helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") - err = test_helpers.RestartTarantool(&helpInstances[1]) + err = test_helpers.RestartTarantool(helpInstances[1]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -1362,10 +1362,10 @@ func TestRequestOnClosed(t *testing.T) { _, err = connPool.Ping(pool.ANY) require.NotNilf(t, err, "err is nil after Ping") - err = test_helpers.RestartTarantool(&helpInstances[0]) + err = test_helpers.RestartTarantool(helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") - err = test_helpers.RestartTarantool(&helpInstances[1]) + err = test_helpers.RestartTarantool(helpInstances[1]) require.Nilf(t, err, "failed to restart tarantool") } diff --git a/queue/queue_test.go b/queue/queue_test.go index d23bd2c9c..e43c47113 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -23,8 +23,6 @@ const ( var servers = []string{"127.0.0.1:3014", "127.0.0.1:3015"} var server = "127.0.0.1:3013" -var instances []test_helpers.TarantoolInstance - var dialer = NetDialer{ Address: server, User: user, @@ -931,7 +929,7 @@ func runTestMain(m *testing.M) int { }) } - instances, err = test_helpers.StartTarantoolInstances(poolInstsOpts) + instances, err := test_helpers.StartTarantoolInstances(poolInstsOpts) if err != nil { log.Printf("Failed to prepare test tarantool pool: %s", err) diff --git a/shutdown_test.go b/shutdown_test.go index b3a09eff0..4df34aef5 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -78,7 +78,7 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar // SIGTERM the server. shutdownStart := time.Now() - require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + require.Nil(t, inst.Signal(syscall.SIGTERM)) // Check that we can't send new requests after shutdown starts. // Retry helps to wait a bit until server starts to shutdown @@ -108,14 +108,11 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar // Wait until server go down. // Server will go down only when it process all requests from our connection // (or on timeout). - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) shutdownFinish := time.Now() shutdownTime := shutdownFinish.Sub(shutdownStart) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - // Check that it wasn't a timeout. require.Lessf(t, shutdownTime, @@ -129,18 +126,16 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar func TestGracefulShutdown(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance var conn *Connection - var err error - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) conn = test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() - testGracefulShutdown(t, conn, &inst) + testGracefulShutdown(t, conn, inst) } func TestCloseGraceful(t *testing.T) { @@ -190,26 +185,23 @@ func TestCloseGraceful(t *testing.T) { func TestGracefulShutdownWithReconnect(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance - var err error - - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) conn := test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() - testGracefulShutdown(t, conn, &inst) + testGracefulShutdown(t, conn, inst) - err = test_helpers.RestartTarantool(&inst) + err = test_helpers.RestartTarantool(inst) require.Nilf(t, err, "Failed to restart tarantool") connected := test_helpers.WaitUntilReconnected(conn, shtdnClntOpts.MaxReconnects, shtdnClntOpts.Reconnect) require.Truef(t, connected, "Reconnect success") - testGracefulShutdown(t, conn, &inst) + testGracefulShutdown(t, conn, inst) } func TestNoGracefulShutdown(t *testing.T) { @@ -219,14 +211,12 @@ func TestNoGracefulShutdown(t *testing.T) { noShtdDialer.RequiredProtocolInfo = ProtocolInfo{} test_helpers.SkipIfWatchersSupported(t) - var inst test_helpers.TarantoolInstance var conn *Connection - var err error testSrvOpts := shtdnSrvOpts testSrvOpts.Dialer = noShtdDialer - inst, err = test_helpers.StartTarantool(testSrvOpts) + inst, err := test_helpers.StartTarantool(testSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -249,21 +239,18 @@ func TestNoGracefulShutdown(t *testing.T) { // SIGTERM the server. shutdownStart := time.Now() - require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + require.Nil(t, inst.Signal(syscall.SIGTERM)) // Check that request was interrupted. _, err = fut.Get() require.NotNilf(t, err, "sleep request error") // Wait until server go down. - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) shutdownFinish := time.Now() shutdownTime := shutdownFinish.Sub(shutdownStart) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - // Check that server finished without waiting for eval to finish. require.Lessf(t, shutdownTime, @@ -274,11 +261,9 @@ func TestNoGracefulShutdown(t *testing.T) { func TestGracefulShutdownRespectsClose(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance var conn *Connection - var err error - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -314,7 +299,7 @@ func TestGracefulShutdownRespectsClose(t *testing.T) { // SIGTERM the server. shutdownStart := time.Now() - require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + require.Nil(t, inst.Signal(syscall.SIGTERM)) // Close the connection. conn.Close() @@ -327,14 +312,11 @@ func TestGracefulShutdownRespectsClose(t *testing.T) { require.NotNilf(t, err, "sleep request error") // Wait until server go down. - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) shutdownFinish := time.Now() shutdownTime := shutdownFinish.Sub(shutdownStart) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - // Check that server finished without waiting for eval to finish. require.Lessf(t, shutdownTime, @@ -354,11 +336,9 @@ func TestGracefulShutdownRespectsClose(t *testing.T) { func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance var conn *Connection - var err error - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -397,16 +377,13 @@ func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) { fut := conn.Do(req) // SIGTERM the server. - require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + require.Nil(t, inst.Signal(syscall.SIGTERM)) // Wait until server go down. // Server is expected to go down on timeout. - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - // Check that request failed by server disconnect, not a client timeout. _, err = fut.Get() require.NotNil(t, err) @@ -425,11 +402,9 @@ func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) { func TestGracefulShutdownCloseConcurrent(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance - var err error var srvShtdnStart, srvShtdnFinish time.Time - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -487,22 +462,19 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) { go func(inst *test_helpers.TarantoolInstance) { srvToStop.Wait() srvShtdnStart = time.Now() - cerr := inst.Cmd.Process.Signal(syscall.SIGTERM) + cerr := inst.Signal(syscall.SIGTERM) if cerr != nil { sret = cerr } srvStop.Done() - }(&inst) + }(inst) srvStop.Wait() require.Nil(t, sret, "No errors on server SIGTERM") - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - srvShtdnFinish = time.Now() srvShtdnTime := srvShtdnFinish.Sub(srvShtdnStart) @@ -515,11 +487,9 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) { func TestGracefulShutdownConcurrent(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance - var err error var srvShtdnStart, srvShtdnFinish time.Time - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -577,12 +547,12 @@ func TestGracefulShutdownConcurrent(t *testing.T) { go func(inst *test_helpers.TarantoolInstance) { srvToStop.Wait() srvShtdnStart = time.Now() - cerr := inst.Cmd.Process.Signal(syscall.SIGTERM) + cerr := inst.Signal(syscall.SIGTERM) if cerr != nil { sret = cerr } srvStop.Done() - }(&inst) + }(inst) srvStop.Wait() require.Nil(t, sret, "No errors on server SIGTERM") @@ -590,12 +560,9 @@ func TestGracefulShutdownConcurrent(t *testing.T) { caseWg.Wait() require.Nil(t, ret, "No errors on concurrent wait") - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - srvShtdnFinish = time.Now() srvShtdnTime := srvShtdnFinish.Sub(srvShtdnStart) diff --git a/tarantool_test.go b/tarantool_test.go index e6adebc6d..2be3e793b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3609,7 +3609,7 @@ func TestConnection_NewWatcher_reconnect(t *testing.T) { <-events test_helpers.StopTarantool(inst) - if err := test_helpers.RestartTarantool(&inst); err != nil { + if err := test_helpers.RestartTarantool(inst); err != nil { t.Fatalf("Unable to restart Tarantool: %s", err) } @@ -3902,7 +3902,7 @@ func TestConnection_named_index_after_reconnect(t *testing.T) { t.Fatalf("An error expected.") } - if err := test_helpers.RestartTarantool(&inst); err != nil { + if err := test_helpers.RestartTarantool(inst); err != nil { t.Fatalf("Unable to restart Tarantool: %s", err) } diff --git a/test_helpers/main.go b/test_helpers/main.go index f452f3e97..4ebe4c622 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -15,6 +15,7 @@ import ( "errors" "fmt" "io" + "io/fs" "log" "os" "os/exec" @@ -33,6 +34,14 @@ type StartOpts struct { // InitScript is a Lua script for tarantool to run on start. InitScript string + // ConfigFile is a path to a configuration file for a Tarantool instance. + // Required in pair with InstanceName. + ConfigFile string + + // InstanceName is a name of an instance to run. + // Required in pair with ConfigFile. + InstanceName string + // Listen is box.cfg listen parameter for tarantool. // Use this address to connect to tarantool after configuration. // https://www.tarantool.io/en/doc/latest/reference/configuration/#cfg-basic-listen @@ -67,9 +76,19 @@ type StartOpts struct { Dialer tarantool.Dialer } +type state struct { + done chan struct{} + ret error + stopped bool +} + // TarantoolInstance is a data for instance graceful shutdown and cleanup. type TarantoolInstance struct { // Cmd is a Tarantool command. Used to kill Tarantool process. + // + // Deprecated: Cmd field will be removed in the next major version. + // Use `Wait()` and `Stop()` methods, instead of calling `Cmd.Process.Wait()` or + // `Cmd.Process.Kill()` directly. Cmd *exec.Cmd // Options for restarting a tarantool instance. @@ -77,6 +96,89 @@ type TarantoolInstance struct { // Dialer to check that connection established. Dialer tarantool.Dialer + + st chan state +} + +// IsExit checks if Tarantool process was terminated. +func (t *TarantoolInstance) IsExit() bool { + st := <-t.st + t.st <- st + + select { + case <-st.done: + default: + return false + } + + return st.ret != nil +} + +func (t *TarantoolInstance) result() error { + st := <-t.st + t.st <- st + + select { + case <-st.done: + default: + return nil + } + + return st.ret +} + +func (t *TarantoolInstance) checkDone() { + ret := t.Cmd.Wait() + + st := <-t.st + + st.ret = ret + close(st.done) + + t.st <- st + + if !st.stopped { + log.Printf("Tarantool %q was unexpectedly terminated: %v", t.Opts.Listen, t.result()) + } +} + +// Wait waits until Tarantool process is terminated. +// Returns error as process result status. +func (t *TarantoolInstance) Wait() error { + st := <-t.st + t.st <- st + + <-st.done + t.Cmd.Process = nil + + st = <-t.st + t.st <- st + + return st.ret +} + +// Stop terminates Tarantool process and waits until it exit. +func (t *TarantoolInstance) Stop() error { + st := <-t.st + st.stopped = true + t.st <- st + + if t.IsExit() { + return nil + } + if t.Cmd != nil && t.Cmd.Process != nil { + if err := t.Cmd.Process.Kill(); err != nil && !t.IsExit() { + return fmt.Errorf("failed to kill tarantool %q (pid %d), got %s", + t.Opts.Listen, t.Cmd.Process.Pid, err) + } + t.Wait() + } + return nil +} + +// Signal sends a signal to the Tarantool instance. +func (t *TarantoolInstance) Signal(sig os.Signal) error { + return t.Cmd.Process.Signal(sig) } func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error { @@ -108,7 +210,7 @@ var ( ) func init() { - tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (?:Enterprise )?(\d+)\.(\d+)\.(\d+).*`) + tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (Enterprise )?(\d+)\.(\d+)\.(\d+).*`) } // atoiUint64 parses string to uint64. @@ -145,15 +247,15 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( return true, fmt.Errorf("failed to parse output %q", out) } - if major, err = atoiUint64(parsed[1]); err != nil { + if major, err = atoiUint64(parsed[2]); err != nil { return true, fmt.Errorf("failed to parse major from output %q: %w", out, err) } - if minor, err = atoiUint64(parsed[2]); err != nil { + if minor, err = atoiUint64(parsed[3]); err != nil { return true, fmt.Errorf("failed to parse minor from output %q: %w", out, err) } - if patch, err = atoiUint64(parsed[3]); err != nil { + if patch, err = atoiUint64(parsed[4]); err != nil { return true, fmt.Errorf("failed to parse patch from output %q: %w", out, err) } @@ -166,6 +268,21 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( } } +// IsTarantoolEE checks if Tarantool is Enterprise edition. +func IsTarantoolEE() (bool, error) { + out, err := exec.Command(getTarantoolExec(), "--version").Output() + if err != nil { + return true, err + } + + parsed := tarantoolVersionRegexp.FindStringSubmatch(string(out)) + if parsed == nil { + return true, fmt.Errorf("failed to parse output %q", out) + } + + return parsed[1] != "", nil +} + // RestartTarantool restarts a tarantool instance for tests // with specifies parameters (refer to StartOpts) // which were specified in inst parameter. @@ -175,42 +292,96 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( // Process must be stopped with StopTarantool. func RestartTarantool(inst *TarantoolInstance) error { startedInst, err := StartTarantool(inst.Opts) + inst.Cmd.Process = startedInst.Cmd.Process + inst.st = startedInst.st + return err } +func removeByMask(dir string, masks ...string) error { + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if d.IsDir() { + return nil + } + + for _, mask := range masks { + if ok, err := filepath.Match(mask, d.Name()); err != nil { + return err + } else if ok { + if err = os.Remove(path); err != nil { + return err + } + } + } + return nil + }) + + if err != nil { + return err + } + return nil +} + +func prepareDir(workDir string) (string, error) { + if workDir == "" { + dir, err := os.MkdirTemp("", "work_dir") + if err != nil { + return "", err + } + return dir, nil + } + // Create work_dir. + err := os.MkdirAll(workDir, 0755) + if err != nil { + return "", err + } + + // Clean up existing work_dir. + err = removeByMask(workDir, "*.snap", "*.xlog") + if err != nil { + return "", err + } + return workDir, nil +} + // StartTarantool starts a tarantool instance for tests // with specifies parameters (refer to StartOpts). // Process must be stopped with StopTarantool. -func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { +func StartTarantool(startOpts StartOpts) (*TarantoolInstance, error) { // Prepare tarantool command. - var inst TarantoolInstance - var dir string - var err error + inst := &TarantoolInstance{ + st: make(chan state, 1), + } + init := state{ + done: make(chan struct{}), + } + inst.st <- init + var err error inst.Dialer = startOpts.Dialer + startOpts.WorkDir, err = prepareDir(startOpts.WorkDir) + if err != nil { + return inst, fmt.Errorf("failed to prepare working dir %q: %w", startOpts.WorkDir, err) + } - if startOpts.WorkDir == "" { - dir, err = os.MkdirTemp("", "work_dir") - if err != nil { - return inst, err - } - startOpts.WorkDir = dir - } else { - // Clean up existing work_dir. - err = os.RemoveAll(startOpts.WorkDir) - if err != nil { - return inst, err - } - - // Create work_dir. - err = os.Mkdir(startOpts.WorkDir, 0755) - if err != nil { - return inst, err + args := []string{} + if startOpts.InitScript != "" { + if !filepath.IsAbs(startOpts.InitScript) { + cwd, err := os.Getwd() + if err != nil { + return inst, fmt.Errorf("failed to get current working directory: %w", err) + } + startOpts.InitScript = filepath.Join(cwd, startOpts.InitScript) } + args = append(args, startOpts.InitScript) } - - inst.Cmd = exec.Command(getTarantoolExec(), startOpts.InitScript) + if startOpts.ConfigFile != "" && startOpts.InstanceName != "" { + args = append(args, "--config", startOpts.ConfigFile) + args = append(args, "--name", startOpts.InstanceName) + } + inst.Cmd = exec.Command(getTarantoolExec(), args...) + inst.Cmd.Dir = startOpts.WorkDir inst.Cmd.Env = append( os.Environ(), @@ -242,6 +413,8 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { // see https://github.com/tarantool/go-tarantool/issues/136 time.Sleep(startOpts.WaitStart) + go inst.checkDone() + opts := tarantool.Opts{ Timeout: 500 * time.Millisecond, SkipSchema: true, @@ -261,24 +434,28 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { } } - return inst, err + if inst.IsExit() && inst.result() != nil { + StopTarantool(inst) + return nil, fmt.Errorf("unexpected terminated Tarantool %q: %w", + inst.Opts.Listen, inst.result()) + } + + if err != nil { + StopTarantool(inst) + return nil, fmt.Errorf("failed to connect Tarantool %q: %w", + inst.Opts.Listen, err) + } + + return inst, nil } // StopTarantool stops a tarantool instance started // with StartTarantool. Waits until any resources // associated with the process is released. If something went wrong, fails. -func StopTarantool(inst TarantoolInstance) { - if inst.Cmd != nil && inst.Cmd.Process != nil { - if err := inst.Cmd.Process.Kill(); err != nil { - log.Fatalf("Failed to kill tarantool (pid %d), got %s", inst.Cmd.Process.Pid, err) - } - - // Wait releases any resources associated with the Process. - if _, err := inst.Cmd.Process.Wait(); err != nil { - log.Fatalf("Failed to wait for Tarantool process to exit, got %s", err) - } - - inst.Cmd.Process = nil +func StopTarantool(inst *TarantoolInstance) { + err := inst.Stop() + if err != nil { + log.Fatal(err) } } @@ -286,7 +463,7 @@ func StopTarantool(inst TarantoolInstance) { // with StartTarantool. Waits until any resources // associated with the process is released. // Cleans work directory after stop. If something went wrong, fails. -func StopTarantoolWithCleanup(inst TarantoolInstance) { +func StopTarantoolWithCleanup(inst *TarantoolInstance) { StopTarantool(inst) if inst.Opts.WorkDir != "" { diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 729a69e44..b67d05f02 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -227,8 +227,8 @@ func SetClusterRO(dialers []tarantool.Dialer, connOpts tarantool.Opts, return nil } -func StartTarantoolInstances(instsOpts []StartOpts) ([]TarantoolInstance, error) { - instances := make([]TarantoolInstance, 0, len(instsOpts)) +func StartTarantoolInstances(instsOpts []StartOpts) ([]*TarantoolInstance, error) { + instances := make([]*TarantoolInstance, 0, len(instsOpts)) for _, opts := range instsOpts { instance, err := StartTarantool(opts) @@ -243,7 +243,7 @@ func StartTarantoolInstances(instsOpts []StartOpts) ([]TarantoolInstance, error) return instances, nil } -func StopTarantoolInstances(instances []TarantoolInstance) { +func StopTarantoolInstances(instances []*TarantoolInstance) { for _, instance := range instances { StopTarantoolWithCleanup(instance) } diff --git a/test_helpers/tcs/prepare.go b/test_helpers/tcs/prepare.go new file mode 100644 index 000000000..c7b87d0f6 --- /dev/null +++ b/test_helpers/tcs/prepare.go @@ -0,0 +1,66 @@ +package tcs + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "text/template" + "time" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +const ( + waitTimeout = 500 * time.Millisecond + connectRetry = 3 + tcsUser = "client" + tcsPassword = "secret" +) + +//go:embed testdata/config.yaml +var tcsConfig []byte + +func writeConfig(name string, port int) error { + cfg, err := os.Create(name) + if err != nil { + return err + } + defer cfg.Close() + + cfg.Chmod(0644) + + t := template.Must(template.New("config").Parse(string(tcsConfig))) + return t.Execute(cfg, map[string]interface{}{ + "host": "localhost", + "port": port, + }) +} + +func makeOpts(port int) (test_helpers.StartOpts, error) { + opts := test_helpers.StartOpts{} + var err error + opts.WorkDir, err = os.MkdirTemp("", "tcs_dir") + if err != nil { + return opts, err + } + + opts.ConfigFile = filepath.Join(opts.WorkDir, "config.yaml") + err = writeConfig(opts.ConfigFile, port) + if err != nil { + return opts, fmt.Errorf("can't save file %q: %w", opts.ConfigFile, err) + } + + opts.Listen = fmt.Sprintf("localhost:%d", port) + opts.WaitStart = waitTimeout + opts.ConnectRetry = connectRetry + opts.RetryTimeout = waitTimeout + opts.InstanceName = "master" + opts.Dialer = tarantool.NetDialer{ + Address: opts.Listen, + User: tcsUser, + Password: tcsPassword, + } + return opts, nil +} diff --git a/test_helpers/tcs/tcs.go b/test_helpers/tcs/tcs.go new file mode 100644 index 000000000..1ba26a19e --- /dev/null +++ b/test_helpers/tcs/tcs.go @@ -0,0 +1,180 @@ +package tcs + +import ( + "context" + "errors" + "fmt" + "net" + "testing" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +// ErrNotSupported identifies result of `Start()` why storage was not started. +var ErrNotSupported = errors.New("required Tarantool EE 3.3+") + +// ErrNoValue used to show that `Get()` was successful, but no values were found. +var ErrNoValue = errors.New("required value not found") + +// TCS is a Tarantool centralized configuration storage connection. +type TCS struct { + inst *test_helpers.TarantoolInstance + conn *tarantool.Connection + tb testing.TB + port int +} + +// dataResponse content of TcS response in data array. +type dataResponse struct { + Path string `msgpack:"path"` + Value string `msgpack:"value"` + ModRevision int64 `msgpack:"mod_revision"` +} + +// findEmptyPort returns some random unused port if @port is passed with zero. +func findEmptyPort(port int) (int, error) { + listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", port)) + if err != nil { + return 0, err + } + defer listener.Close() + + addr := listener.Addr().(*net.TCPAddr) + return addr.Port, nil +} + +// Start starts a Tarantool centralized configuration storage. +// Use `port = 0` to use any unused port. +// Returns a Tcs instance and a cleanup function. +func Start(port int) (TCS, error) { + tcs := TCS{} + if ok, err := test_helpers.IsTcsSupported(); !ok || err != nil { + return tcs, errors.Join(ErrNotSupported, err) + } + var err error + tcs.port, err = findEmptyPort(port) + if err != nil { + if port == 0 { + return tcs, fmt.Errorf("failed to detect an empty port: %w", err) + } else { + return tcs, fmt.Errorf("port %d can't be used: %w", port, err) + } + } + + opts, err := makeOpts(tcs.port) + if err != nil { + return tcs, err + } + + tcs.inst, err = test_helpers.StartTarantool(opts) + if err != nil { + return tcs, fmt.Errorf("failed to start Tarantool config storage: %w", err) + } + + tcs.conn, err = tarantool.Connect(context.Background(), tcs.inst.Dialer, tarantool.Opts{}) + if err != nil { + return tcs, fmt.Errorf("failed to connect to Tarantool config storage: %w", err) + } + + return tcs, nil +} + +// Start starts a Tarantool centralized configuration storage. +// Returns a Tcs instance and a cleanup function. +func StartTesting(tb testing.TB, port int) TCS { + tcs, err := Start(port) + if err != nil { + tb.Fatal(err) + } + return tcs +} + +// Doer returns interface for interacting with Tarantool. +func (t *TCS) Doer() tarantool.Doer { + return t.conn +} + +// Dialer returns a dialer to connect to Tarantool. +func (t *TCS) Dialer() tarantool.Dialer { + return t.inst.Dialer +} + +// Endpoints returns a list of addresses to connect. +func (t *TCS) Endpoints() []string { + return []string{fmt.Sprintf("127.0.0.1:%d", t.port)} +} + +// Credentials returns a user name and password to connect. +func (t *TCS) Credentials() (string, string) { + return tcsUser, tcsPassword +} + +// Stop stops the Tarantool centralized configuration storage. +func (t *TCS) Stop() { + if t.tb != nil { + t.tb.Helper() + } + if t.conn != nil { + t.conn.Close() + } + test_helpers.StopTarantoolWithCleanup(t.inst) +} + +// Put implements "config.storage.put" method. +func (t *TCS) Put(ctx context.Context, path string, value string) error { + if t.tb != nil { + t.tb.Helper() + } + req := tarantool.NewCallRequest("config.storage.put"). + Args([]any{path, value}). + Context(ctx) + if _, err := t.conn.Do(req).GetResponse(); err != nil { + return fmt.Errorf("failed to save data to tarantool: %w", err) + } + return nil +} + +// Delete implements "config.storage.delete" method. +func (t *TCS) Delete(ctx context.Context, path string) error { + if t.tb != nil { + t.tb.Helper() + } + req := tarantool.NewCallRequest("config.storage.delete"). + Args([]any{path}). + Context(ctx) + if _, err := t.conn.Do(req).GetResponse(); err != nil { + return fmt.Errorf("failed to delete data from tarantool: %w", err) + } + return nil +} + +// Get implements "config.storage.get" method. +func (t *TCS) Get(ctx context.Context, path string) (string, error) { + if t.tb != nil { + t.tb.Helper() + } + req := tarantool.NewCallRequest("config.storage.get"). + Args([]any{path}). + Context(ctx) + + resp := []struct { + Data []dataResponse `msgpack:"data"` + }{} + + err := t.conn.Do(req).GetTyped(&resp) + if err != nil { + return "", fmt.Errorf("failed to fetch data from tarantool: %w", err) + } + if len(resp) != 1 { + return "", errors.New("unexpected response from tarantool") + } + if len(resp[0].Data) == 0 { + return "", ErrNoValue + } + if len(resp[0].Data) != 1 { + return "", errors.New("too much data in response from tarantool") + } + + return resp[0].Data[0].Value, nil +} diff --git a/test_helpers/tcs/testdata/config.yaml b/test_helpers/tcs/testdata/config.yaml new file mode 100644 index 000000000..5d42e32aa --- /dev/null +++ b/test_helpers/tcs/testdata/config.yaml @@ -0,0 +1,39 @@ +credentials: + users: + replicator: + password: "topsecret" + roles: [replication] + client: + password: "secret" + privileges: + - permissions: [execute] + universe: true + - permissions: [read, write] + spaces: [config_storage, config_storage_meta] + +iproto: + advertise: + peer: + login: replicator + +replication: + failover: election + +database: + use_mvcc_engine: true + +groups: + group-001: + replicasets: + replicaset-001: + roles: [config.storage] + roles_cfg: + config_storage: + status_check_interval: 3 + instances: + master: + iproto: + listen: + - uri: "{{.host}}:{{.port}}" + params: + transport: plain diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 049dff92d..eff7e1dbe 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -217,6 +217,32 @@ func SkipIfCrudSpliceBroken(t *testing.T) { SkipIfFeatureUnsupported(t, "crud update splice", 2, 0, 0) } +// IsTcsSupported checks if Tarantool supports centralized storage. +// Tarantool supports centralized storage with Enterprise since 3.3.0 version. +func IsTcsSupported() (bool, error) { + + if isEe, err := IsTarantoolEE(); !isEe || err != nil { + return false, err + } + if isLess, err := IsTarantoolVersionLess(3, 3, 0); isLess || err != nil { + return false, err + } + return true, nil +} + +// SkipIfTCSUnsupported skips test if no centralized storage support. +func SkipIfTcsUnsupported(t testing.TB) { + t.Helper() + + ok, err := IsTcsSupported() + if err != nil { + t.Fatalf("Could not check the Tarantool version: %s", err) + } + if !ok { + t.Skip("not found Tarantool EE 3.3+") + } +} + // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. // // Tarantool errors are not comparable by nature: