Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

### Changed
- `status`: added `--instance-timeout` flag to bound how long collecting a single
instance's status may take.

### Fixed

- `status`: replication errors were ignored.
- `status`: status requests could stuck into deadlock without timeout.

## [2.14.0] - 2026-08-06

This release introduces cluster backup and restore: `tt backup` plans a
Expand Down
13 changes: 12 additions & 1 deletion cli/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"time"

"github.com/spf13/cobra"
"github.com/tarantool/tt/cli/cmd/internal"
Expand All @@ -18,6 +19,10 @@ type statusOpts struct {
details bool
// Deprecated: use --format instead.
pretty bool
// instanceTimeout bounds how long collecting a single instance's status may
// take, so that one stuck instance can't hang the whole command. Zero disables
// the timeout.
instanceTimeout time.Duration
}

var opts statusOpts
Expand Down Expand Up @@ -61,6 +66,9 @@ Columns:
statusCmd.Flags().BoolVarP(&opts.pretty, "pretty", "p", false,
"output a pretty-formatted table (deprecated, use --format instead)")
statusCmd.Flags().MarkDeprecated("pretty", "use --format instead")
statusCmd.Flags().DurationVar(&opts.instanceTimeout, "instance-timeout",
status.DefaultInstanceTimeout,
"timeout for collecting a single instance's status; 0 disables the timeout")

return statusCmd
}
Expand Down Expand Up @@ -106,5 +114,8 @@ func internalStatusModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
printer = status.NewTablePrinter(status.WithDetails(opts.details))
}

return status.Status(runningCtx, printer)
if err := status.Status(runningCtx, printer, opts.instanceTimeout); err != nil {
return fmt.Errorf("failed to get status: %w", err)
}
return nil
}
86 changes: 66 additions & 20 deletions cli/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"runtime"
"sync"
"time"

"github.com/tarantool/go-tarantool"
Expand All @@ -18,6 +19,59 @@ const (
maxSocketPathMac = 106
)

// connectMutex serializes connections that depend on the process-wide working
// directory. prepareUnixAddress may temporarily change it to shorten a socket path.
var connectMutex sync.Mutex

// unixSocketPathLimit returns the maximum socket path length for the current OS.
func unixSocketPathLimit() int {
if runtime.GOOS == "darwin" {
return maxSocketPathMac
}
return maxSocketPathLinux
}

// prepareUnixAddress prepares a Unix socket address for use with Tarantool.
func prepareUnixAddress(address string) (string, func(), error) {
maxSocketPath := unixSocketPathLimit()

pathNeedsShortening := len(address)+1 > maxSocketPath
if filepath.IsAbs(address) && !pathNeedsShortening {
return address, nil, nil
}

shortAddress := "./" + filepath.Base(address)
if pathNeedsShortening && len(shortAddress)+1 > maxSocketPath {
return "", nil, fmt.Errorf("socket name is longer than %d symbols: %s",
maxSocketPath-3, filepath.Base(address))
}

// Relative paths also depend on the process-wide working directory.
connectMutex.Lock() // unlock in cleanup.

if !pathNeedsShortening {
return address, connectMutex.Unlock, nil
}

workDir, err := os.Getwd()
if err != nil {
connectMutex.Unlock()
return "", nil, fmt.Errorf("failed to get working directory: %w", err)
}

if err := os.Chdir(filepath.Dir(address)); err != nil {
connectMutex.Unlock()
return "", nil, fmt.Errorf("failed to change directory to socket directory: %w", err)
}

cleanup := func() {
_ = os.Chdir(workDir)
connectMutex.Unlock()
}

return shortAddress, cleanup, nil
}

// RequestOpts describes the parameters of a request to be executed.
type RequestOpts struct {
// PushCallback is the cb that will be called when a "push" message is received.
Expand All @@ -43,29 +97,20 @@ type Connector interface {

// Connect connects to the tarantool instance according to options.
func Connect(opts ConnectOpts) (Connector, error) {
// It became common that address is longer than 108 symbols(sun_path limit).
// To reduce length of address we use relative path
// with chdir into a directory of socket.
// e.g foo/bar/123.sock -> ./123.sock
workDir, err := os.Getwd()
if err != nil {
return nil, err
}

maxSocketPath := maxSocketPathLinux
if runtime.GOOS == "darwin" {
maxSocketPath = maxSocketPathMac
}
if opts.Network == "unix" {
address, cleanup, err := prepareUnixAddress(opts.Address)
if err != nil {
return nil, fmt.Errorf("failed to prepare unix socket address: %w", err)
}

if _, err := os.Stat(opts.Address); err == nil {
os.Chdir(filepath.Dir(opts.Address))
opts.Address = "./" + filepath.Base(opts.Address)
if len(opts.Address)+1 > maxSocketPath {
return nil, fmt.Errorf("socket name is longer than %d symbols: %s",
maxSocketPath-3, filepath.Base(opts.Address))
if cleanup != nil {
defer cleanup()
}
defer os.Chdir(workDir)

// Use the short address if it was prepared.
opts.Address = address
}

// Connect to specified address.
greetingConn, err := net.Dial(opts.Network, opts.Address)
if err != nil {
Expand All @@ -85,6 +130,7 @@ func Connect(opts ConnectOpts) (Connector, error) {
protocol = BinaryProtocol
transport = "ssl"
} else {
greetingConn.Close()
return nil, fmt.Errorf("failed to get protocol: %s", err)
}
} else if ssl {
Expand Down
81 changes: 81 additions & 0 deletions cli/connector/connector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package connector

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestPrepareUnixAddressShortAbsolutePath(t *testing.T) {
address := filepath.Join(t.TempDir(), "instance.sock")
if len(address)+1 > unixSocketPathLimit() {
t.Skip("temporary directory path is too long for this test")
}

prepared, cleanup, err := prepareUnixAddress(address)

require.NoError(t, err)
require.Equal(t, address, prepared)
require.Nil(t, cleanup)
}

func TestPrepareUnixAddressRelativePath(t *testing.T) {
const address = "run/instance.sock"

prepared, cleanup, err := prepareUnixAddress(address)
require.NoError(t, err)
require.NotNil(t, cleanup)
defer cleanup()

require.Equal(t, address, prepared)
}

func TestPrepareUnixAddressLongPath(t *testing.T) {
const socketName = "instance.sock"

originalWorkDir, err := os.Getwd()
require.NoError(t, err)

socketDir := t.TempDir()
for len(filepath.Join(socketDir, socketName))+1 <= unixSocketPathLimit() {
socketDir = filepath.Join(socketDir, strings.Repeat("d", 32))
}
require.NoError(t, os.MkdirAll(socketDir, 0o755))

prepared, cleanup, err := prepareUnixAddress(filepath.Join(socketDir, socketName))
require.NoError(t, err)
require.NotNil(t, cleanup)

cleanedUp := false
defer func() {
if !cleanedUp {
cleanup()
}
}()

currentWorkDir, err := os.Getwd()
require.NoError(t, err)
require.Equal(t, filepath.Clean(socketDir), currentWorkDir)
require.Equal(t, "./"+socketName, prepared)

cleanup()
cleanedUp = true

currentWorkDir, err = os.Getwd()
require.NoError(t, err)
require.Equal(t, originalWorkDir, currentWorkDir)
}

func TestPrepareUnixAddressLongSocketName(t *testing.T) {
socketName := strings.Repeat("s", unixSocketPathLimit())
address := filepath.Join(t.TempDir(), socketName)

prepared, cleanup, err := prepareUnixAddress(address)

require.ErrorContains(t, err, "socket name is longer")
require.Empty(t, prepared)
require.Nil(t, cleanup)
}
43 changes: 31 additions & 12 deletions cli/running/running.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,21 +335,21 @@ func findInstanceScriptInAppDir(appDir, instName, clusterCfgPath, defaultScript
return script, nil
}

// loadInstanceConfig load instance configuration from cluster config.
func loadInstanceConfig(configPath, instName string,
// loadClusterConfig reads and parses a cluster config.
func loadClusterConfig(configPath string,
integrityCtx integrity.IntegrityCtx,
) (libcluster.InstanceConfig, error) {
var instCfg libcluster.InstanceConfig
) (libcluster.ClusterConfig, error) {
var clusterCfg libcluster.ClusterConfig
if configPath == "" {
return instCfg, nil
return clusterCfg, nil
}

var dataCollectors libcluster.DataCollectorFactory
checkFunc, err := integrity.GetCheckFunction(integrityCtx)
if err == integrity.ErrNotConfigured {
dataCollectors = libcluster.NewDataCollectorFactory()
} else if err != nil {
return instCfg,
return clusterCfg,
fmt.Errorf("failed to create collectors with integrity check: %w", err)
} else {
dataCollectors = libcluster.NewIntegrityDataCollectorFactory(checkFunc,
Expand All @@ -359,12 +359,24 @@ func loadInstanceConfig(configPath, instName string,
}
collectors := libcluster.NewCollectorFactory(dataCollectors)

clusterCfg, err := cluster.GetClusterConfig(collectors, configPath)
clusterCfg, err = cluster.GetClusterConfig(collectors, configPath)
if err != nil {
return instCfg, err
return clusterCfg, fmt.Errorf("failed to get cluster config: %w", err)
}
return clusterCfg, nil
}

// loadInstanceConfig derives an instance configuration from an
// cluster config.
func loadInstanceConfig(clusterCfg libcluster.ClusterConfig, configPath,
instName string,
) (libcluster.InstanceConfig, error) {
if configPath == "" {
return libcluster.InstanceConfig{}, nil
}
if instCfg, err = cluster.GetInstanceConfig(clusterCfg, instName); err != nil {
return instCfg, err
instCfg, err := cluster.GetInstanceConfig(clusterCfg, instName)
if err != nil {
return instCfg, fmt.Errorf("failed to get instance config: %w", err)
}
return instCfg, nil
}
Expand Down Expand Up @@ -418,6 +430,13 @@ func collectInstancesFromAppDir(appDir, selectedInstName string,
if err != nil {
return nil, err
}

clusterCfg, err := loadClusterConfig(appDirFiles.clusterCfgPath, integrityCtx)
if err != nil && (loadConfig == ConfigLoadAll || loadConfig == ConfigLoadCluster) {
return nil, fmt.Errorf("error loading cluster configuration from config %q: %w",
appDirFiles.clusterCfgPath, err)
}

log.Debug("Processing application instances file")
instances := []InstanceCtx{}
for inst := range instParams {
Expand All @@ -433,8 +452,8 @@ func collectInstancesFromAppDir(appDir, selectedInstName string,
}
log.Debugf("Instance %q", instance.InstName)

instance.Configuration, err = loadInstanceConfig(instance.ClusterConfigPath,
instance.InstName, integrityCtx)
instance.Configuration, err = loadInstanceConfig(clusterCfg,
instance.ClusterConfigPath, instance.InstName)
if err != nil && (loadConfig == ConfigLoadAll || loadConfig == ConfigLoadCluster) {
return instances, fmt.Errorf("error loading instance %q configuration from "+
"config %q: %w", instance.InstName, instance.ClusterConfigPath, err)
Expand Down
Loading
Loading