Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve efficiency and accuracy of mysqld.GetVersionString #15096

Merged
merged 13 commits into from
Feb 1, 2024
35 changes: 21 additions & 14 deletions go/vt/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import (
"errors"
"fmt"
"os"
"os/exec"
"path"
Expand All @@ -30,9 +29,12 @@
// DefaultVtDataRoot is the default value for VTROOT environment variable
DefaultVtDataRoot = "/vt"
// DefaultVtRoot is only required for hooks
DefaultVtRoot = "/usr/local/vitess"
DefaultVtRoot = "/usr/local/vitess"
mysqldSbinPath = "/usr/sbin/mysqld"
)

var errMysqldNotFound = errors.New("VT_MYSQL_ROOT is not set and no mysqld could be found in your PATH")

// VtRoot returns $VTROOT or tries to guess its value if it's not set.
// This is the root for the 'vt' distribution, which contains bin/vttablet
// for instance.
Expand Down Expand Up @@ -64,25 +66,30 @@
}

// VtMysqlRoot returns the root for the mysql distribution,
// which contains bin/mysql CLI for instance.
// If it is not set, look for mysqld in the path.
// which contains the bin/mysql CLI for instance.
// If $VT_MYSQL_ROOT is not set, look for mysqld in the $PATH.
func VtMysqlRoot() (string, error) {
// if the environment variable is set, use that
// If the environment variable is set, use that.
if root := os.Getenv("VT_MYSQL_ROOT"); root != "" {
return root, nil
}

// otherwise let's look for mysqld in the PATH.
// ensure that /usr/sbin is included, as it might not be by default
// This is the default location for mysqld from packages.
newPath := fmt.Sprintf("/usr/sbin:%s", os.Getenv("PATH"))
os.Setenv("PATH", newPath)
path, err := exec.LookPath("mysqld")
getRoot := func(path string) string {
return filepath.Dir(filepath.Dir(path)) // Strip mysqld and [s]bin parts
}
binpath, err := exec.LookPath("mysqld")
mattlord marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", errors.New("VT_MYSQL_ROOT is not set and no mysqld could be found in your PATH")
// First see if /usr/sbin/mysqld exists as it might not be in
// the PATH by default and this is often the default location
// used by mysqld OS system packages (apt, dnf, etc).
fi, err := os.Stat(mysqldSbinPath)
if err == nil /* file exists */ && fi.Mode().IsRegular() /* not a DIR or other special file */ &&
fi.Mode()&0111 != 0 /* executable by anyone */ {
return getRoot(mysqldSbinPath), nil
}
return "", errMysqldNotFound

Check warning on line 90 in go/vt/env/env.go

View check run for this annotation

Codecov / codecov/patch

go/vt/env/env.go#L90

Added line #L90 was not covered by tests
}
path = filepath.Dir(filepath.Dir(path)) // strip mysqld, and the sbin
return path, nil
return getRoot(binpath), nil
}

// VtMysqlBaseDir returns the Mysql base directory, which
Expand Down
82 changes: 82 additions & 0 deletions go/vt/env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ package env

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

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

func TestVtDataRoot(t *testing.T) {
Expand All @@ -43,3 +46,82 @@ func TestVtDataRoot(t *testing.T) {
t.Errorf("The value of VtDataRoot should be %v, not %v.", passed, root)
}
}

func TestVtMysqlRoot(t *testing.T) {
envVar := "VT_MYSQL_ROOT"
originalMySQLRoot := os.Getenv(envVar)
defer os.Setenv(envVar, originalMySQLRoot)
originalPATH := os.Getenv("PATH")
defer os.Setenv("PATH", originalPATH)

// The test directory is used to create our fake mysqld binary.
testDir := t.TempDir() // This is automatically cleaned up
createExecutable := func(path string) error {
fullPath := testDir + path
err := os.MkdirAll(filepath.Dir(fullPath), 0755)
require.NoError(t, err)
return os.WriteFile(fullPath, []byte("test"), 0755)
}

type testcase struct {
name string
preFunc func() error
vtMysqlRootEnvVal string
pathEnvVal string
expect string // The return value we expect from VtMysqlRoot()
expectErr string
}
testcases := []testcase{
{
name: "VT_MYSQL_ROOT set",
vtMysqlRootEnvVal: "/home/mysql/binaries",
},
{
name: "VT_MYSQL_ROOT empty; PATH set without /usr/sbin",
pathEnvVal: testDir + filepath.Dir(mysqldSbinPath) +
":/usr/bin:/sbin:/bin:/usr/local/bin:/usr/local/sbin:/home/mysql/binaries",
preFunc: func() error {
return createExecutable(mysqldSbinPath)
},
expect: testDir + "/usr",
},
}

// If /usr/sbin/mysqld exists, confirm that we find it even
// when /usr/sbin is not in the PATH.
_, err := os.Stat(mysqldSbinPath)
if err == nil {
t.Logf("Found %s, confirming auto detection behavior", mysqldSbinPath)
testcases = append(testcases, testcase{
name: "VT_MYSQL_ROOT empty; PATH empty; mysqld in /usr/sbin",
expect: "/usr",
})
} else {
testcases = append(testcases, testcase{ // Error expected
name: "VT_MYSQL_ROOT empty; PATH empty; mysqld not in /usr/sbin",
expectErr: errMysqldNotFound.Error(),
})
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if tc.preFunc != nil {
err := tc.preFunc()
require.NoError(t, err)
}
os.Setenv(envVar, tc.vtMysqlRootEnvVal)
os.Setenv("PATH", tc.pathEnvVal)
path, err := VtMysqlRoot()
if tc.expectErr != "" {
require.EqualError(t, err, tc.expectErr)
} else {
require.NoError(t, err)
}
if tc.vtMysqlRootEnvVal != "" {
// This should always be returned.
tc.expect = tc.vtMysqlRootEnvVal
}
require.Equal(t, tc.expect, path)
})
}
}
39 changes: 27 additions & 12 deletions go/vt/mysqlctl/mysqld.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,32 @@

"github.com/spf13/pflag"

"vitess.io/vitess/go/mysql/sqlerror"
"vitess.io/vitess/go/protoutil"

"vitess.io/vitess/config"
"vitess.io/vitess/go/mysql"
"vitess.io/vitess/go/mysql/sqlerror"
"vitess.io/vitess/go/protoutil"
"vitess.io/vitess/go/sqltypes"
"vitess.io/vitess/go/vt/dbconfigs"
"vitess.io/vitess/go/vt/dbconnpool"
vtenv "vitess.io/vitess/go/vt/env"
"vitess.io/vitess/go/vt/hook"
"vitess.io/vitess/go/vt/log"
"vitess.io/vitess/go/vt/mysqlctl/mysqlctlclient"
"vitess.io/vitess/go/vt/servenv"
"vitess.io/vitess/go/vt/vterrors"

vtenv "vitess.io/vitess/go/vt/env"
mysqlctlpb "vitess.io/vitess/go/vt/proto/mysqlctl"
"vitess.io/vitess/go/vt/proto/vtrpc"
vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
)

var (
// The string we expect before the MySQL version number
// in strings containing MySQL version information.
const versionStringPrefix = "Ver "

// How many bytes from MySQL error log to sample for error messages
const maxLogFileSampleSize = 4096

var (
// DisableActiveReparents is a flag to disable active
// reparents for safety reasons. It is used in three places:
// 1. in this file to skip registering the commands.
Expand All @@ -87,15 +92,18 @@

replicationConnectRetry = 10 * time.Second

versionRegex = regexp.MustCompile(`Ver ([0-9]+)\.([0-9]+)\.([0-9]+)`)
versionRegex = regexp.MustCompile(fmt.Sprintf(`%s([0-9]+)\.([0-9]+)\.([0-9]+)`, versionStringPrefix))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure there's a good reason you changed this, but it doesn't feel necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so that we're using the same const in the regex and where we build the string to match it when querying mysqld.

// versionSQLQuery will return a version string directly from
// a MySQL server that is compatible with what we expect from
// mysqld --version and matches the versionRegex. Example
// result: Ver 8.0.35 MySQL Community Server - GPL
versionSQLQuery = fmt.Sprintf("select concat('%s', @@global.version, ' ', @@global.version_comment) as version",
versionStringPrefix)

binlogEntryCommittedTimestampRegex = regexp.MustCompile("original_committed_timestamp=([0-9]+)")
binlogEntryTimestampGTIDRegexp = regexp.MustCompile(`^#(.+) server id.*\bGTID\b`)
)

// How many bytes from MySQL error log to sample for error messages
const maxLogFileSampleSize = 4096

// Mysqld is the object that represents a mysqld daemon running on this server.
type Mysqld struct {
dbcfgs *dbconfigs.DBConfigs
Expand Down Expand Up @@ -1186,7 +1194,13 @@

// GetVersionString is part of the MysqlExecutor interface.
func (mysqld *Mysqld) GetVersionString(ctx context.Context) (string, error) {
// Execute as remote action on mysqlctld to ensure we get the actual running MySQL version.
// Try to query the mysqld instance directly.
qr, err := mysqld.FetchSuperQuery(ctx, versionSQLQuery)
if err == nil && len(qr.Rows) == 1 {
return qr.Rows[0][0].ToString(), nil
}

Check warning on line 1201 in go/vt/mysqlctl/mysqld.go

View check run for this annotation

Codecov / codecov/patch

go/vt/mysqlctl/mysqld.go#L1197-L1201

Added lines #L1197 - L1201 were not covered by tests
// Execute as remote action on mysqlctld to use the actual running MySQL
// version.
if socketFile != "" {
client, err := mysqlctlclient.New("unix", socketFile)
if err != nil {
Expand All @@ -1195,6 +1209,7 @@
defer client.Close()
return client.VersionString(ctx)
}
// Fall back to the sys exec method using mysqld --version.
return GetVersionString()
}

Expand Down Expand Up @@ -1429,7 +1444,7 @@
// ReadBinlogFilesTimestamps reads all given binlog files via `mysqlbinlog` command and returns the first and last found transaction timestamps
func (mysqld *Mysqld) ReadBinlogFilesTimestamps(ctx context.Context, req *mysqlctlpb.ReadBinlogFilesTimestampsRequest) (*mysqlctlpb.ReadBinlogFilesTimestampsResponse, error) {
if len(req.BinlogFileNames) == 0 {
return nil, vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "empty binlog list in ReadBinlogFilesTimestampsRequest")
return nil, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "empty binlog list in ReadBinlogFilesTimestampsRequest")

Check warning on line 1447 in go/vt/mysqlctl/mysqld.go

View check run for this annotation

Codecov / codecov/patch

go/vt/mysqlctl/mysqld.go#L1447

Added line #L1447 was not covered by tests
}
if socketFile != "" {
log.Infof("executing Mysqld.ReadBinlogFilesTimestamps() remotely via mysqlctld server: %v", socketFile)
Expand Down
Loading