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

Add wait for reading mycnf to prevent race #14626

Merged
merged 2 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions go/flags/endtoend/vtbackup.txt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ Flags:
--min_retention_count int Always keep at least this many of the most recent backups in this backup storage location, even if some are older than the min_retention_time. This must be at least 1 since a backup must always exist to allow new backups to be made (default 1)
--min_retention_time duration Keep each old backup for at least this long before removing it. Set to 0 to disable pruning of old backups.
--mycnf-file string path to my.cnf, if reading all config params from there
--mycnf-wait-time duration time to wait for my.cnf to be created by mysqlctld externally (default 10s)
GuptaManan100 marked this conversation as resolved.
Show resolved Hide resolved
--mycnf_bin_log_path string mysql binlog path
--mycnf_data_dir string data directory for mysql
--mycnf_error_log_path string mysql error log path
Expand Down
1 change: 1 addition & 0 deletions go/flags/endtoend/vtcombo.txt
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ Flags:
--message_stream_grace_period duration the amount of time to give for a vttablet to resume if it ends a message stream, usually because of a reparent. (default 30s)
--migration_check_interval duration Interval between migration checks (default 1m0s)
--mycnf-file string path to my.cnf, if reading all config params from there
--mycnf-wait-time duration time to wait for my.cnf to be created by mysqlctld externally (default 10s)
--mycnf_bin_log_path string mysql binlog path
--mycnf_data_dir string data directory for mysql
--mycnf_error_log_path string mysql error log path
Expand Down
1 change: 1 addition & 0 deletions go/flags/endtoend/vttablet.txt
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ Flags:
--max_concurrent_online_ddl int Maximum number of online DDL changes that may run concurrently (default 256)
--migration_check_interval duration Interval between migration checks (default 1m0s)
--mycnf-file string path to my.cnf, if reading all config params from there
--mycnf-wait-time duration time to wait for my.cnf to be created by mysqlctld externally (default 10s)
--mycnf_bin_log_path string mysql binlog path
--mycnf_data_dir string data directory for mysql
--mycnf_error_log_path string mysql error log path
Expand Down
2 changes: 1 addition & 1 deletion go/vt/mysqlctl/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func CreateMysqldAndMycnf(tabletUID uint32, mysqlSocket string, mysqlPort int) (
// of the MySQL instance.
func OpenMysqldAndMycnf(tabletUID uint32) (*Mysqld, *Mycnf, error) {
// We pass a port of 0, this will be read and overwritten from the path on disk
mycnf, err := ReadMycnf(NewMycnf(tabletUID, 0))
mycnf, err := ReadMycnf(NewMycnf(tabletUID, 0), 0)
if err != nil {
return nil, nil, fmt.Errorf("couldn't read my.cnf file: %v", err)
}
Expand Down
20 changes: 19 additions & 1 deletion go/vt/mysqlctl/mycnf.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"os"
"path"
"strconv"
"time"
)

// Mycnf is a memory structure that contains a bunch of interesting
Expand Down Expand Up @@ -112,6 +113,10 @@ type Mycnf struct {
Path string // the actual path that represents this mycnf
}

const (
myCnfWaitRetryTime = 100 * time.Millisecond
)

// TabletDir returns the tablet directory.
func (cnf *Mycnf) TabletDir() string {
return path.Dir(cnf.DataDir)
Expand Down Expand Up @@ -153,8 +158,21 @@ func normKey(bkey []byte) string {

// ReadMycnf will read an existing my.cnf from disk, and update the passed in Mycnf object
// with values from the my.cnf on disk.
func ReadMycnf(mycnf *Mycnf) (*Mycnf, error) {
func ReadMycnf(mycnf *Mycnf, waitTime time.Duration) (*Mycnf, error) {
f, err := os.Open(mycnf.Path)
if waitTime != 0 {
timer := time.NewTimer(waitTime)
ticker := time.NewTicker(myCnfWaitRetryTime)
GuptaManan100 marked this conversation as resolved.
Show resolved Hide resolved
defer ticker.Stop()
for err != nil {
select {
case <-timer.C:
return nil, err
case <-ticker.C:
f, err = os.Open(mycnf.Path)
}
}
}
if err != nil {
return nil, err
}
Expand Down
8 changes: 7 additions & 1 deletion go/vt/mysqlctl/mycnf_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package mysqlctl

import (
"time"

"github.com/spf13/pflag"

"vitess.io/vitess/go/vt/log"
Expand Down Expand Up @@ -49,6 +51,9 @@ var (

// the file to use to specify them all
flagMycnfFile string

// flagWaitForMyCnf is the amount of time to wait for the My.cnf file to be created externally.
flagWaitForMyCnf time.Duration
)

// RegisterFlags registers the command line flags for
Expand All @@ -75,6 +80,7 @@ func RegisterFlags() {
fs.StringVar(&flagSecureFilePriv, "mycnf_secure_file_priv", flagSecureFilePriv, "mysql path for loading secure files")

fs.StringVar(&flagMycnfFile, "mycnf-file", flagMycnfFile, "path to my.cnf, if reading all config params from there")
fs.DurationVar(&flagWaitForMyCnf, "mycnf-wait-time", 10*time.Second, "time to wait for my.cnf to be created by mysqlctld externally")
})
}

Expand Down Expand Up @@ -129,5 +135,5 @@ func NewMycnfFromFlags(uid uint32) (mycnf *Mycnf, err error) {
}
mycnf = NewMycnf(uid, 0)
mycnf.Path = flagMycnfFile
return ReadMycnf(mycnf)
return ReadMycnf(mycnf, flagWaitForMyCnf)
}
68 changes: 42 additions & 26 deletions go/vt/mysqlctl/mycnf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import (
"bytes"
"os"
"strings"
"sync"
"testing"
"time"

"github.com/stretchr/testify/require"

"vitess.io/vitess/go/vt/dbconfigs"
"vitess.io/vitess/go/vt/servenv"
Expand All @@ -29,6 +33,9 @@ import (
var MycnfPath = "/tmp/my.cnf"

func TestMycnf(t *testing.T) {
// Remove any my.cnf file if it already exists.
os.Remove(MycnfPath)

uid := uint32(11111)
cnf := NewMycnf(uid, 6802)
myTemplateSource := new(bytes.Buffer)
Expand All @@ -39,36 +46,45 @@ func TestMycnf(t *testing.T) {
f, _ := os.ReadFile("../../../config/mycnf/default.cnf")
myTemplateSource.Write(f)
data, err := cnf.makeMycnf(myTemplateSource.String())
if err != nil {
t.Errorf("err: %v", err)
} else {
t.Logf("data: %v", data)
}
err = os.WriteFile(MycnfPath, []byte(data), 0666)
if err != nil {
t.Errorf("failed creating my.cnf %v", err)
}
_, err = os.ReadFile(MycnfPath)
if err != nil {
t.Errorf("failed reading, err %v", err)
return
}
require.NoError(t, err)
t.Logf("data: %v", data)

// Since there is no my.cnf file, reading it should fail with a no such file error.
mycnf := NewMycnf(uid, 0)
mycnf.Path = MycnfPath
mycnf, err = ReadMycnf(mycnf)
if err != nil {
t.Errorf("failed reading, err %v", err)
} else {
_, err = ReadMycnf(mycnf, 0)
require.ErrorContains(t, err, "no such file or directory")

// Next up we will spawn a go-routine to try and read the cnf file with a timeout.
// We will create the cnf file after some delay and verify that ReadMycnf does wait that long
// and ends up succeeding in reading the my.cnf file.
waitTime := 1 * time.Second
wg := sync.WaitGroup{}
wg.Add(1)

go func() {
defer wg.Done()
startTime := time.Now()
var readErr error
mycnf, readErr = ReadMycnf(mycnf, 1*time.Minute)
require.NoError(t, readErr, "failed reading")
t.Logf("socket file %v", mycnf.SocketFile)
}
totalTimeSpent := time.Since(startTime)
require.GreaterOrEqual(t, totalTimeSpent, waitTime)
}()

time.Sleep(waitTime)
err = os.WriteFile(MycnfPath, []byte(data), 0666)
require.NoError(t, err, "failed creating my.cnf")
_, err = os.ReadFile(MycnfPath)
require.NoError(t, err, "failed reading")

// Wait for ReadMycnf to finish and then verify that the data read is correct.
wg.Wait()
// Tablet UID should be 11111, which determines tablet/data dir.
if got, want := mycnf.DataDir, "/vt_0000011111/"; !strings.Contains(got, want) {
t.Errorf("mycnf.DataDir = %v, want *%v*", got, want)
}
require.Contains(t, mycnf.DataDir, "/vt_0000011111/")
// MySQL server-id should be 22222, different from Tablet UID.
if got, want := mycnf.ServerID, uint32(22222); got != want {
t.Errorf("mycnf.ServerID = %v, want %v", got, want)
}
require.EqualValues(t, uint32(22222), mycnf.ServerID)
}

// Run this test if any changes are made to hook handling / make_mycnf hook
Expand Down Expand Up @@ -112,7 +128,7 @@ func NoTestMycnfHook(t *testing.T) {
}
mycnf := NewMycnf(uid, 0)
mycnf.Path = cnf.Path
mycnf, err = ReadMycnf(mycnf)
mycnf, err = ReadMycnf(mycnf, 0)
if err != nil {
t.Errorf("failed reading, err %v", err)
} else {
Expand Down
Loading