Skip to content

Commit fb9e39f

Browse files
Incremental backup: fix race condition in reading 'mysqlbinlog' output (#14330)
Signed-off-by: Shlomi Noach <2607934+shlomi-noach@users.noreply.github.com>
1 parent 5f0df9a commit fb9e39f

File tree

3 files changed

+88
-63
lines changed

3 files changed

+88
-63
lines changed

go/vt/mysqlctl/builtinbackupengine.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ func (be *BuiltinBackupEngine) executeIncrementalBackup(ctx context.Context, par
348348
if resp.FirstTimestampBinlog == "" || resp.LastTimestampBinlog == "" {
349349
return false, vterrors.Errorf(vtrpc.Code_ABORTED, "empty binlog name in response. Request=%v, Response=%v", req, resp)
350350
}
351+
log.Infof("ReadBinlogFilesTimestampsResponse: %+v", resp)
351352
incrDetails := &IncrementalBackupDetails{
352353
FirstTimestamp: FormatRFC3339(protoutil.TimeFromProto(resp.FirstTimestamp).UTC()),
353354
FirstTimestampBinlog: filepath.Base(resp.FirstTimestampBinlog),

go/vt/mysqlctl/mysqld.go

Lines changed: 85 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,85 +1294,86 @@ func (mysqld *Mysqld) ApplyBinlogFile(ctx context.Context, req *mysqlctlpb.Apply
12941294
}
12951295

12961296
// parseBinlogEntryTimestamp attempts to extract a timestamp from a binlog entry.
1297-
func parseBinlogEntryTimestamp(logEntry string) (found bool, t time.Time, err error) {
1297+
func parseBinlogEntryTimestamp(logEntry string) (t time.Time, err error) {
12981298
if len(logEntry) == 0 {
1299-
return false, t, nil
1299+
return t, nil
13001300
}
13011301
if logEntry[0] != '#' {
1302-
return false, t, nil
1302+
return t, nil
13031303
}
13041304
if submatch := binlogEntryCommittedTimestampRegex.FindStringSubmatch(logEntry); submatch != nil {
13051305
// MySQL 8.0
13061306
binlogEntryCommittedTimestamp := submatch[1]
13071307
unixMicros, err := strconv.ParseInt(binlogEntryCommittedTimestamp, 10, 64)
13081308
if err != nil {
1309-
return false, t, err
1309+
return t, err
13101310
}
1311-
return true, time.UnixMicro(unixMicros), nil
1311+
return time.UnixMicro(unixMicros), nil
13121312
}
13131313
if submatch := binlogEntryTimestampGTIDRegexp.FindStringSubmatch(logEntry); submatch != nil {
13141314
// MySQL 5.7
13151315
t, err = ParseBinlogTimestamp(submatch[1])
13161316
if err != nil {
1317-
return false, t, err
1317+
return t, err
13181318
}
1319-
return true, t, nil
1319+
return t, nil
13201320
}
1321-
return false, t, nil
1321+
return t, nil
13221322
}
13231323

13241324
// scanBinlogTimestamp invokes a `mysqlbinlog` binary to look for a timestamp in the given binary. The function
1325-
// either looks for the first such timestamp or the last.
1326-
func (mysqld *Mysqld) scanBinlogTimestamp(mysqlbinlogDir string, mysqlbinlogEnv []string, mysqlbinlogName string, binlogFile string, stopAtFirst bool) (matchedTime time.Time, matchFound bool, err error) {
1325+
// looks for the first and last timestamps.
1326+
func (mysqld *Mysqld) scanBinlogTimestamp(
1327+
mysqlbinlogDir string,
1328+
mysqlbinlogEnv []string,
1329+
mysqlbinlogName string,
1330+
binlogFile string,
1331+
stopAtFirst bool, // unused at this moment, to be used as an optimization hint
1332+
) (
1333+
firstMatchedTime time.Time,
1334+
lastMatchedTime time.Time,
1335+
err error,
1336+
) {
13271337
args := []string{binlogFile}
13281338
mysqlbinlogCmd := exec.Command(mysqlbinlogName, args...)
13291339
mysqlbinlogCmd.Dir = mysqlbinlogDir
13301340
mysqlbinlogCmd.Env = mysqlbinlogEnv
13311341
log.Infof("ApplyBinlogFile: running mysqlbinlog command: %#v", mysqlbinlogCmd)
13321342
pipe, err := mysqlbinlogCmd.StdoutPipe() // to be piped into mysql
13331343
if err != nil {
1334-
return matchedTime, false, err
1335-
}
1336-
scanComplete := make(chan error)
1337-
intentionalKill := false
1338-
scan := func() {
1339-
defer close(scanComplete)
1340-
defer func() {
1341-
intentionalKill = true
1342-
mysqlbinlogCmd.Process.Kill() // ensures the binlog file is released
1343-
}()
1344+
return firstMatchedTime, lastMatchedTime, err
1345+
}
1346+
scan := func() error {
13441347
// Read line by line and process it
13451348
scanner := bufio.NewScanner(pipe)
13461349
for scanner.Scan() {
13471350
logEntry := scanner.Text()
13481351

1349-
found, t, err := parseBinlogEntryTimestamp(logEntry)
1352+
t, err := parseBinlogEntryTimestamp(logEntry)
13501353
if err != nil {
1351-
scanComplete <- err
1352-
return
1354+
return err
13531355
}
1354-
if found {
1355-
matchedTime = t
1356-
matchFound = true
1356+
if t.IsZero() {
1357+
continue
13571358
}
1358-
if found && stopAtFirst {
1359-
// Found the first timestamp and it's all we need. We won't scan any further and so we should also
1360-
// kill mysqlbinlog (otherwise it keeps waiting until we've read the entire pipe).
1361-
return
1359+
if firstMatchedTime.IsZero() {
1360+
firstMatchedTime = t
13621361
}
1362+
lastMatchedTime = t
13631363
}
1364+
return nil
13641365
}
1365-
if err := mysqlbinlogCmd.Start(); err != nil {
1366-
return matchedTime, false, err
1366+
if err := mysqlbinlogCmd.Start(); err != nil { // Start() is nonblockig
1367+
return firstMatchedTime, lastMatchedTime, err
13671368
}
1368-
go scan()
1369-
if err := mysqlbinlogCmd.Wait(); err != nil && !intentionalKill {
1370-
return matchedTime, false, vterrors.Wrapf(err, "waiting on mysqlbinlog command in ReadBinlogFilesTimestamps")
1369+
defer mysqlbinlogCmd.Process.Kill()
1370+
if err := scan(); err != nil { // We must first exhaust reading the command's output, before calling cmd.Wait()
1371+
return firstMatchedTime, lastMatchedTime, vterrors.Wrapf(err, "scanning mysqlbinlog output in ReadBinlogFilesTimestamps")
13711372
}
1372-
if err := <-scanComplete; err != nil {
1373-
return matchedTime, false, vterrors.Wrapf(err, "scanning mysqlbinlog output in ReadBinlogFilesTimestamps ")
1373+
if err := mysqlbinlogCmd.Wait(); err != nil {
1374+
return firstMatchedTime, lastMatchedTime, vterrors.Wrapf(err, "waiting on mysqlbinlog command in ReadBinlogFilesTimestamps")
13741375
}
1375-
return matchedTime, matchFound, nil
1376+
return firstMatchedTime, lastMatchedTime, nil
13761377
}
13771378

13781379
// ReadBinlogFilesTimestamps reads all given binlog files via `mysqlbinlog` command and returns the first and last found transaction timestamps
@@ -1402,31 +1403,60 @@ func (mysqld *Mysqld) ReadBinlogFilesTimestamps(ctx context.Context, req *mysqlc
14021403
return nil, err
14031404
}
14041405

1406+
lastMatchedTimeMap := map[string]time.Time{} // a simple cache to avoid rescanning same files. Key=binlog file name
1407+
14051408
resp := &mysqlctlpb.ReadBinlogFilesTimestampsResponse{}
14061409
// Find first timestamp
1407-
for _, binlogFile := range req.BinlogFileNames {
1408-
t, found, err := mysqld.scanBinlogTimestamp(dir, env, mysqlbinlogName, binlogFile, true)
1409-
if err != nil {
1410-
return nil, err
1411-
}
1412-
if found {
1413-
resp.FirstTimestamp = protoutil.TimeToProto(t)
1410+
err = func() error {
1411+
for _, binlogFile := range req.BinlogFileNames {
1412+
firstMatchedTime, lastMatchedTime, err := mysqld.scanBinlogTimestamp(dir, env, mysqlbinlogName, binlogFile, true)
1413+
if err != nil {
1414+
return vterrors.Wrapf(err, "while scanning for first binlog timestamp in %v", binlogFile)
1415+
}
1416+
if !lastMatchedTime.IsZero() {
1417+
// cache result
1418+
lastMatchedTimeMap[binlogFile] = lastMatchedTime
1419+
}
1420+
if firstMatchedTime.IsZero() {
1421+
// Timestamp not found in this file.
1422+
continue
1423+
}
1424+
resp.FirstTimestamp = protoutil.TimeToProto(firstMatchedTime)
14141425
resp.FirstTimestampBinlog = binlogFile
1415-
break
1426+
return nil // early break
14161427
}
1428+
return nil
1429+
}()
1430+
if err != nil {
1431+
return resp, err
14171432
}
14181433
// Find last timestamp
1419-
for i := len(req.BinlogFileNames) - 1; i >= 0; i-- {
1420-
binlogFile := req.BinlogFileNames[i]
1421-
t, found, err := mysqld.scanBinlogTimestamp(dir, env, mysqlbinlogName, binlogFile, false)
1422-
if err != nil {
1423-
return nil, err
1424-
}
1425-
if found {
1426-
resp.LastTimestamp = protoutil.TimeToProto(t)
1434+
err = func() error {
1435+
for i := len(req.BinlogFileNames) - 1; i >= 0; i-- {
1436+
binlogFile := req.BinlogFileNames[i]
1437+
1438+
// See if we have a cached value for this file. This is certainly be the situation if there's a single binary log file in req.BinlogFileNames,
1439+
// which means the first file and last file are the same, and so we have already parsed the file while searching for the first timestamp.
1440+
lastMatchedTime, ok := lastMatchedTimeMap[binlogFile]
1441+
if !ok {
1442+
var err error
1443+
_, lastMatchedTime, err = mysqld.scanBinlogTimestamp(dir, env, mysqlbinlogName, binlogFile, false)
1444+
if err != nil {
1445+
return vterrors.Wrapf(err, "while scanning for last binlog timestamp in %v", binlogFile)
1446+
}
1447+
}
1448+
if lastMatchedTime.IsZero() {
1449+
// Timestamp not found in this file.
1450+
continue
1451+
}
1452+
resp.LastTimestamp = protoutil.TimeToProto(lastMatchedTime)
14271453
resp.LastTimestampBinlog = binlogFile
1428-
break
1454+
return nil // early break
14291455
}
1456+
return nil
1457+
}()
1458+
if err != nil {
1459+
return resp, err
14301460
}
14311461
return resp, nil
14321462
}

go/vt/mysqlctl/mysqld_test.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ func TestParseBinlogEntryTimestamp(t *testing.T) {
139139
tcases := []struct {
140140
name string
141141
entry string
142-
found bool
143142
tm time.Time
144143
}{
145144
{
@@ -157,24 +156,19 @@ func TestParseBinlogEntryTimestamp(t *testing.T) {
157156
{
158157
name: "mysql80",
159158
entry: "#230605 16:06:34 server id 22233 end_log_pos 1037 CRC32 0xa4707c5b GTID last_committed=4 sequence_number=5 rbr_only=no original_committed_timestamp=1685970394031366 immediate_commit_timestamp=1685970394032458 transaction_length=186",
160-
found: true,
161159
tm: time.UnixMicro(1685970394031366),
162160
},
163161
{
164162
name: "mysql57",
165163
entry: "#230608 13:14:31 server id 484362839 end_log_pos 259 CRC32 0xc07510d0 GTID last_committed=0 sequence_number=1 rbr_only=yes",
166-
found: true,
167164
tm: time.Date(2023, time.June, 8, 13, 14, 31, 0, time.UTC),
168165
},
169166
}
170167
for _, tcase := range tcases {
171168
t.Run(tcase.name, func(t *testing.T) {
172-
found, tm, err := parseBinlogEntryTimestamp(tcase.entry)
169+
tm, err := parseBinlogEntryTimestamp(tcase.entry)
173170
assert.NoError(t, err)
174-
assert.Equal(t, tcase.found, found)
175-
if tcase.found {
176-
assert.Equal(t, tcase.tm, tm)
177-
}
171+
assert.Equal(t, tcase.tm, tm)
178172
})
179173
}
180174
}

0 commit comments

Comments
 (0)