diff --git a/go/mysql/flavor_mysql.go b/go/mysql/flavor_mysql.go index 04d8e3be055..f40681a546f 100644 --- a/go/mysql/flavor_mysql.go +++ b/go/mysql/flavor_mysql.go @@ -121,6 +121,15 @@ func (mysqlFlavor) status(c *Conn) (SlaveStatus, error) { if err != nil { return SlaveStatus{}, vterrors.Wrapf(err, "SlaveStatus can't parse MySQL 5.6 GTID (Executed_Gtid_Set: %#v)", resultMap["Executed_Gtid_Set"]) } + relayLogGTIDSet, err := parseMysql56GTIDSet(resultMap["Retrieved_Gtid_Set"]) + if err != nil { + return SlaveStatus{}, vterrors.Wrapf(err, "SlaveStatus can't parse MySQL 5.6 GTID (Retrieved_Gtid_Set: %#v)", resultMap["Retrieved_Gtid_Set"]) + } + // We take the union of the executed and retrieved gtidset, because the retrieved gtidset only represents GTIDs since + // the relay log has been reset. To get the full Position, we need to take a union of executed GTIDSets, since these would + // have been in the relay log's GTIDSet in the past, prior to a reset. + status.RelayLogPosition.GTIDSet = status.Position.GTIDSet.Union(relayLogGTIDSet) + return status, nil } diff --git a/go/mysql/slave_status.go b/go/mysql/slave_status.go index 3b0dcacfdd3..0d603b7ebd4 100644 --- a/go/mysql/slave_status.go +++ b/go/mysql/slave_status.go @@ -23,7 +23,12 @@ import ( // SlaveStatus holds replication information from SHOW SLAVE STATUS. type SlaveStatus struct { - Position Position + Position Position + // RelayLogPosition is the Position that the replica would be at if it + // were to finish executing everything that's currently in its relay log. + // However, some MySQL flavors don't expose this information, + // in which case RelayLogPosition.IsZero() will be true. + RelayLogPosition Position SlaveIORunning bool SlaveSQLRunning bool SecondsBehindMaster uint @@ -42,6 +47,7 @@ func (s *SlaveStatus) SlaveRunning() bool { func SlaveStatusToProto(s SlaveStatus) *replicationdatapb.Status { return &replicationdatapb.Status{ Position: EncodePosition(s.Position), + RelayLogPosition: EncodePosition(s.RelayLogPosition), SlaveIoRunning: s.SlaveIORunning, SlaveSqlRunning: s.SlaveSQLRunning, SecondsBehindMaster: uint32(s.SecondsBehindMaster), @@ -57,8 +63,13 @@ func ProtoToSlaveStatus(s *replicationdatapb.Status) SlaveStatus { if err != nil { panic(vterrors.Wrapf(err, "cannot decode Position")) } + relayPos, err := DecodePosition(s.RelayLogPosition) + if err != nil { + panic(vterrors.Wrapf(err, "cannot decode RelayLogPosition")) + } return SlaveStatus{ Position: pos, + RelayLogPosition: relayPos, SlaveIORunning: s.SlaveIoRunning, SlaveSQLRunning: s.SlaveSqlRunning, SecondsBehindMaster: uint(s.SecondsBehindMaster), diff --git a/go/vt/proto/replicationdata/replicationdata.pb.go b/go/vt/proto/replicationdata/replicationdata.pb.go index 3924d47bb4f..3a83b644563 100644 --- a/go/vt/proto/replicationdata/replicationdata.pb.go +++ b/go/vt/proto/replicationdata/replicationdata.pb.go @@ -24,7 +24,9 @@ const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package // Status is the replication status for MySQL (returned by 'show slave status' // and parsed into a Position and fields). type Status struct { - Position string `protobuf:"bytes,1,opt,name=position,proto3" json:"position,omitempty"` + Position string `protobuf:"bytes,1,opt,name=position,proto3" json:"position,omitempty"` + // RelayLogPosition will be empty for flavors that do not support returning the full GTIDSet from the relay log, such as MariaDB. + RelayLogPosition string `protobuf:"bytes,8,opt,name=relay_log_position,json=relayLogPosition,proto3" json:"relay_log_position,omitempty"` SlaveIoRunning bool `protobuf:"varint,2,opt,name=slave_io_running,json=slaveIoRunning,proto3" json:"slave_io_running,omitempty"` SlaveSqlRunning bool `protobuf:"varint,3,opt,name=slave_sql_running,json=slaveSqlRunning,proto3" json:"slave_sql_running,omitempty"` SecondsBehindMaster uint32 `protobuf:"varint,4,opt,name=seconds_behind_master,json=secondsBehindMaster,proto3" json:"seconds_behind_master,omitempty"` @@ -68,6 +70,13 @@ func (m *Status) GetPosition() string { return "" } +func (m *Status) GetRelayLogPosition() string { + if m != nil { + return m.RelayLogPosition + } + return "" +} + func (m *Status) GetSlaveIoRunning() bool { if m != nil { return m.SlaveIoRunning @@ -117,22 +126,23 @@ func init() { func init() { proto.RegisterFile("replicationdata.proto", fileDescriptor_ee8ee22b8c4b9d06) } var fileDescriptor_ee8ee22b8c4b9d06 = []byte{ - // 264 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x90, 0xc1, 0x4a, 0x03, 0x31, - 0x10, 0x86, 0xd9, 0x6a, 0xd7, 0x1a, 0xd1, 0x6a, 0xb4, 0x10, 0xbc, 0xb8, 0x78, 0x5a, 0x44, 0x36, - 0xa2, 0x6f, 0x50, 0x2f, 0x7a, 0x10, 0x24, 0xbd, 0x79, 0x09, 0xe9, 0x6e, 0x68, 0x03, 0x6b, 0x66, - 0x9b, 0x99, 0x2e, 0xf8, 0x3a, 0x3e, 0xa9, 0x34, 0x69, 0x8b, 0xf4, 0x96, 0x7c, 0xdf, 0x77, 0x18, - 0x7e, 0x36, 0x09, 0xb6, 0x6b, 0x5d, 0x6d, 0xc8, 0x81, 0x6f, 0x0c, 0x99, 0xaa, 0x0b, 0x40, 0xc0, - 0xc7, 0x07, 0xf8, 0xfe, 0x77, 0xc0, 0xf2, 0x19, 0x19, 0x5a, 0x23, 0xbf, 0x65, 0xa3, 0x0e, 0xd0, - 0x6d, 0x94, 0xc8, 0x8a, 0xac, 0x3c, 0x55, 0xfb, 0x3f, 0x2f, 0xd9, 0x25, 0xb6, 0xa6, 0xb7, 0xda, - 0x81, 0x0e, 0x6b, 0xef, 0x9d, 0x5f, 0x88, 0x41, 0x91, 0x95, 0x23, 0x75, 0x11, 0xf9, 0x3b, 0xa8, - 0x44, 0xf9, 0x03, 0xbb, 0x4a, 0x25, 0xae, 0xda, 0x7d, 0x7a, 0x14, 0xd3, 0x71, 0x14, 0xb3, 0x55, - 0xbb, 0x6b, 0x9f, 0xd9, 0x04, 0x6d, 0x0d, 0xbe, 0x41, 0x3d, 0xb7, 0x4b, 0xe7, 0x1b, 0xfd, 0x6d, - 0x90, 0x6c, 0x10, 0xc7, 0x45, 0x56, 0x9e, 0xab, 0xeb, 0xad, 0x9c, 0x46, 0xf7, 0x11, 0x15, 0xbf, - 0x63, 0x67, 0x29, 0xd2, 0x4b, 0x40, 0x12, 0xc3, 0x78, 0x28, 0x4b, 0xe8, 0x0d, 0x90, 0xfe, 0x05, - 0x1d, 0x04, 0x12, 0x79, 0x91, 0x95, 0xc3, 0x5d, 0xf0, 0x09, 0x81, 0xf8, 0x13, 0xbb, 0xd9, 0x06, - 0x35, 0x78, 0x6f, 0x6b, 0xd2, 0xc1, 0x52, 0xf8, 0x11, 0x27, 0xb1, 0xe4, 0xc9, 0xbd, 0x26, 0xa5, - 0x36, 0x66, 0x5a, 0x7d, 0x3d, 0xf6, 0x8e, 0x2c, 0x62, 0xe5, 0x40, 0xa6, 0x97, 0x5c, 0x80, 0xec, - 0x49, 0xc6, 0x55, 0xe5, 0xc1, 0xa8, 0xf3, 0x3c, 0xe2, 0x97, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, - 0xd3, 0x3d, 0xca, 0x01, 0x85, 0x01, 0x00, 0x00, + // 286 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0xd1, 0x41, 0x4b, 0xfb, 0x30, + 0x18, 0xc7, 0x71, 0xba, 0xff, 0x7f, 0xb5, 0x46, 0x74, 0x33, 0x3a, 0x08, 0x5e, 0x2c, 0x9e, 0x8a, + 0x8c, 0x56, 0xf4, 0x1d, 0xcc, 0x8b, 0x82, 0xc2, 0xe8, 0x6e, 0x5e, 0x42, 0xd6, 0x86, 0x2e, 0x10, + 0xf3, 0x74, 0xc9, 0xb3, 0xc2, 0xde, 0xa5, 0x2f, 0x49, 0x96, 0xac, 0x45, 0x76, 0x6b, 0x7f, 0xdf, + 0xcf, 0x21, 0xf0, 0x90, 0x99, 0x95, 0xad, 0x56, 0x95, 0x40, 0x05, 0xa6, 0x16, 0x28, 0xf2, 0xd6, + 0x02, 0x02, 0x9d, 0x9c, 0xcc, 0x0f, 0x3f, 0x23, 0x12, 0xaf, 0x50, 0xe0, 0xce, 0xd1, 0x3b, 0x92, + 0xb4, 0xe0, 0xd4, 0x21, 0xb1, 0x28, 0x8d, 0xb2, 0xf3, 0x72, 0xf8, 0xa7, 0x73, 0x42, 0xad, 0xd4, + 0x62, 0xcf, 0x35, 0x34, 0x7c, 0x50, 0x89, 0x57, 0x53, 0x5f, 0x3e, 0xa0, 0x59, 0xf6, 0x3a, 0x23, + 0x53, 0xa7, 0x45, 0x27, 0xb9, 0x02, 0x6e, 0x77, 0xc6, 0x28, 0xd3, 0xb0, 0x51, 0x1a, 0x65, 0x49, + 0x79, 0xe5, 0xf7, 0x77, 0x28, 0xc3, 0x4a, 0x1f, 0xc9, 0x75, 0x90, 0x6e, 0xab, 0x07, 0xfa, 0xcf, + 0xd3, 0x89, 0x0f, 0xab, 0xad, 0xee, 0xed, 0x33, 0x99, 0x39, 0x59, 0x81, 0xa9, 0x1d, 0x5f, 0xcb, + 0x8d, 0x32, 0x35, 0xff, 0x16, 0x0e, 0xa5, 0x65, 0xff, 0xd3, 0x28, 0xbb, 0x2c, 0x6f, 0x8e, 0x71, + 0xe1, 0xdb, 0xa7, 0x4f, 0xf4, 0x9e, 0x5c, 0x04, 0xc4, 0x37, 0xe0, 0x90, 0x8d, 0xfd, 0x83, 0x49, + 0x98, 0xde, 0xc0, 0xe1, 0x1f, 0xd0, 0x82, 0x45, 0x16, 0xa7, 0x51, 0x36, 0xee, 0xc1, 0x12, 0x2c, + 0xd2, 0x27, 0x72, 0x7b, 0x04, 0x15, 0x18, 0x23, 0x2b, 0xe4, 0x56, 0xa2, 0xdd, 0xb3, 0x33, 0x2f, + 0x69, 0x68, 0xaf, 0x21, 0x95, 0x87, 0xb2, 0xc8, 0xbf, 0xe6, 0x9d, 0x42, 0xe9, 0x5c, 0xae, 0xa0, + 0x08, 0x5f, 0x45, 0x03, 0x45, 0x87, 0x85, 0xbf, 0x41, 0x71, 0x72, 0x82, 0x75, 0xec, 0xe7, 0x97, + 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf2, 0x5e, 0x2a, 0x18, 0xb3, 0x01, 0x00, 0x00, } diff --git a/go/vt/wrangler/reparent.go b/go/vt/wrangler/reparent.go index c15a2b6b755..6dd5b3244bc 100644 --- a/go/vt/wrangler/reparent.go +++ b/go/vt/wrangler/reparent.go @@ -23,6 +23,7 @@ This file handles the reparenting operations. import ( "context" "fmt" + "sort" "sync" "time" @@ -757,47 +758,75 @@ func (wr *Wrangler) findCurrentMaster(tabletMap map[string]*topo.TabletInfo) *to return currentMaster } -// maxReplPosSearch is a struct helping to search for a tablet with the largest replication -// position querying status from all tablets in parallel. -type maxReplPosSearch struct { +// tabletReplInfoAggregator is a struct that can be used to get all tablets replication positions tied to their aliases concurrently. +// It allows for a sorting of the retrieved replication infos, based on position, such that the earliest members +// of the sorted list have the largest replication position. +type tabletReplInfoAggregator struct { wrangler *Wrangler ctx context.Context waitReplicasTimeout time.Duration waitGroup sync.WaitGroup - maxPosLock sync.Mutex - maxPos mysql.Position - maxPosTablet *topodatapb.Tablet + statusLock sync.Mutex + tabletReplInfos []*replicationInfo } -func (maxPosSearch *maxReplPosSearch) processTablet(tablet *topodatapb.Tablet) { - defer maxPosSearch.waitGroup.Done() - maxPosSearch.wrangler.logger.Infof("getting replication position from %v", topoproto.TabletAliasString(tablet.Alias)) +type replicationInfo struct { + alias *topodatapb.TabletAlias + position mysql.Position +} + +func (t *tabletReplInfoAggregator) processTablet(tablet *topodatapb.Tablet) { + defer t.waitGroup.Done() + t.wrangler.logger.Infof("getting replication position from %v", topoproto.TabletAliasString(tablet.Alias)) - slaveStatusCtx, cancelSlaveStatus := context.WithTimeout(maxPosSearch.ctx, maxPosSearch.waitReplicasTimeout) + slaveStatusCtx, cancelSlaveStatus := context.WithTimeout(t.ctx, t.waitReplicasTimeout) defer cancelSlaveStatus() - status, err := maxPosSearch.wrangler.tmc.SlaveStatus(slaveStatusCtx, tablet) + status, err := t.wrangler.tmc.SlaveStatus(slaveStatusCtx, tablet) if err != nil { - maxPosSearch.wrangler.logger.Warningf("failed to get replication status from %v, ignoring tablet: %v", topoproto.TabletAliasString(tablet.Alias), err) + t.wrangler.logger.Warningf("failed to get replication status from %v, ignoring tablet: %v", topoproto.TabletAliasString(tablet.Alias), err) return } - replPos, err := mysql.DecodePosition(status.Position) + replPos, err := decodePosition(status) if err != nil { - maxPosSearch.wrangler.logger.Warningf("cannot decode slave %v position %v: %v", topoproto.TabletAliasString(tablet.Alias), status.Position, err) + t.wrangler.logger.Warningf("cannot decode slave %v position %v: %v", topoproto.TabletAliasString(tablet.Alias), status.Position, err) return } - maxPosSearch.maxPosLock.Lock() - if maxPosSearch.maxPosTablet == nil || !maxPosSearch.maxPos.AtLeast(replPos) { - maxPosSearch.maxPos = replPos - maxPosSearch.maxPosTablet = tablet + t.statusLock.Lock() + t.tabletReplInfos = append(t.tabletReplInfos, &replicationInfo{ + alias: tablet.Alias, + position: replPos, + }) + t.statusLock.Unlock() +} + +// unpackTabletAliases will turn a []*replicationInfo from the tabletReplInfos field into an []*TabletAlias. +func (t *tabletReplInfoAggregator) unpackTabletAliases() []*topodatapb.TabletAlias { + t.statusLock.Lock() + tabletAliases := make([]*topodatapb.TabletAlias, 0, len(t.tabletReplInfos)) + + for i := range t.tabletReplInfos { + tabletAliases = append(tabletAliases, t.tabletReplInfos[i].alias) } - maxPosSearch.maxPosLock.Unlock() + t.statusLock.Unlock() + + return tabletAliases +} + +func (t *tabletReplInfoAggregator) Len() int { + return len(t.tabletReplInfos) +} +func (t *tabletReplInfoAggregator) Swap(i, j int) { + t.tabletReplInfos[i], t.tabletReplInfos[j] = t.tabletReplInfos[j], t.tabletReplInfos[i] +} +func (t *tabletReplInfoAggregator) Less(i, j int) bool { + return !t.tabletReplInfos[i].position.AtLeast(t.tabletReplInfos[j].position) } // chooseNewMaster finds a tablet that is going to become master after reparent. The criteria -// for the new master-elect are (preferably) to be in the same cell as the current master, and -// to be different from avoidMasterTabletAlias. The tablet with the largest replication +// for the new master-elect are to be in the same cell as the current master, and +// to be different from avoidMasterTabletAlias, if supplied. The tablet with the largest replication // position is chosen to minimize the time of catching up with the master. Note that the search // for largest replication position will race with transactions being executed on the master at // the same time, so when all tablets are roughly at the same position then the choice of the @@ -808,37 +837,71 @@ func (wr *Wrangler) chooseNewMaster( tabletMap map[string]*topo.TabletInfo, avoidMasterTabletAlias *topodatapb.TabletAlias, waitReplicasTimeout time.Duration) (*topodatapb.TabletAlias, error) { - - if avoidMasterTabletAlias == nil { - return nil, fmt.Errorf("tablet to avoid for reparent is not provided, cannot choose new master") + candidates, err := wr.FindMasterCandidates(ctx, tabletMap, waitReplicasTimeout) + if err != nil { + return nil, err } + var masterCell string if shardInfo.MasterAlias != nil { masterCell = shardInfo.MasterAlias.Cell } - maxPosSearch := maxReplPosSearch{ + // Filter out avoidMasterTabletAlias, if it exists in the candidates list, and also filter out + // any candidates that aren't in the master tablets cell. + filteredCandidates := make([]*topodatapb.TabletAlias, 0, len(candidates)) + for i := range candidates { + if candidates[i] != avoidMasterTabletAlias && candidates[i].Cell == masterCell { + filteredCandidates = append(filteredCandidates, candidates[i]) + } + } + + if len(filteredCandidates) == 0 { + return nil, nil + } + return filteredCandidates[0], nil +} + +// FindMasterCandidates will look at all of the tablets in the supplied tabletMap, and return a list of tabletAliases that +// are all caught up. This means that all of the returned TabletAliases will have the same replication position. +func (wr *Wrangler) FindMasterCandidates( + ctx context.Context, + tabletMap map[string]*topo.TabletInfo, + waitReplicasTimeout time.Duration) ([]*topodatapb.TabletAlias, error) { + aggregator := &tabletReplInfoAggregator{ wrangler: wr, ctx: ctx, waitReplicasTimeout: waitReplicasTimeout, waitGroup: sync.WaitGroup{}, - maxPosLock: sync.Mutex{}, + statusLock: sync.Mutex{}, + tabletReplInfos: make([]*replicationInfo, 0, len(tabletMap)), } for _, tabletInfo := range tabletMap { - if (masterCell != "" && tabletInfo.Alias.Cell != masterCell) || - topoproto.TabletAliasEqual(tabletInfo.Alias, avoidMasterTabletAlias) || - tabletInfo.Tablet.Type != topodatapb.TabletType_REPLICA { + if tabletInfo.Tablet.Type != topodatapb.TabletType_REPLICA { continue } - maxPosSearch.waitGroup.Add(1) - go maxPosSearch.processTablet(tabletInfo.Tablet) + aggregator.waitGroup.Add(1) + go aggregator.processTablet(tabletInfo.Tablet) } - maxPosSearch.waitGroup.Wait() + aggregator.waitGroup.Wait() - if maxPosSearch.maxPosTablet == nil { - return nil, nil + sort.Sort(aggregator) + + if len(aggregator.tabletReplInfos) < 2 { + // Either 1 result, or 0. Regardless, we can return the list at this point as we don't need to + // filter out tablets that aren't fully caught up. + return aggregator.unpackTabletAliases(), nil } - return maxPosSearch.maxPosTablet.Alias, nil + + for i := 1; i < len(aggregator.tabletReplInfos); i++ { + if aggregator.Less(i, i-1) { + // We found the first one that isn't fully caught up. Remove this and all further list items. + aggregator.tabletReplInfos = aggregator.tabletReplInfos[0:i] + break + } + } + + return aggregator.unpackTabletAliases(), nil } // EmergencyReparentShard will make the provided tablet the master for @@ -947,26 +1010,9 @@ func (wr *Wrangler) emergencyReparentShardLocked(ctx context.Context, ev *events return fmt.Errorf("lost topology lock, aborting: %v", err) } - // Verify masterElect is alive and has the most advanced position - masterElectStatus, ok := statusMap[masterElectTabletAliasStr] - if !ok { - return fmt.Errorf("couldn't get master elect %v replication position", topoproto.TabletAliasString(masterElectTabletAlias)) - } - masterElectPos, err := mysql.DecodePosition(masterElectStatus.Position) - if err != nil { - return fmt.Errorf("cannot decode master elect position %v: %v", masterElectStatus.Position, err) - } - for alias, status := range statusMap { - if alias == masterElectTabletAliasStr { - continue - } - pos, err := mysql.DecodePosition(status.Position) - if err != nil { - return fmt.Errorf("cannot decode replica %v position %v: %v", alias, status.Position, err) - } - if !masterElectPos.AtLeast(pos) { - return fmt.Errorf("tablet %v is more advanced than master elect tablet %v: %v > %v", alias, masterElectTabletAliasStr, status.Position, masterElectStatus.Position) - } + // Bail out if the master tablet candidate is not the farthest ahead. + if err := wr.CheckTabletIsFarthestAhead(masterElectTabletAliasStr, statusMap); err != nil { + return err } // Promote the masterElect @@ -1043,6 +1089,52 @@ func (wr *Wrangler) emergencyReparentShardLocked(ctx context.Context, ev *events return nil } +// CheckTabletIsFarthestAhead will take a tablet alias string, along with a statusMap of tablet alias strings to tablet Status objects, +// and return an error if the tablet is not the farthest ahead, or otherwise return nil if it is the farthest ahead. +func (wr *Wrangler) CheckTabletIsFarthestAhead(tabletAliasStr string, statusMap map[string]*replicationdatapb.Status) error { + tabletStatus, ok := statusMap[tabletAliasStr] + if !ok { + return fmt.Errorf("couldn't get tablet %v replication position", tabletAliasStr) + } + candidatePos, err := decodePosition(tabletStatus) + if err != nil { + return fmt.Errorf("cannot decode tablet position %v: %v", tabletStatus.Position, err) + } + for alias, status := range statusMap { + if alias == tabletAliasStr { + continue + } + pos, err := decodePosition(status) + if err != nil { + return fmt.Errorf("cannot decode and merge replica %v executed and retrieved positions %v: %v", alias, status.Position, err) + } + if !candidatePos.AtLeast(pos) { + return fmt.Errorf("tablet %v is more advanced than master elect tablet %v: %v > %v", alias, tabletAliasStr, status.Position, tabletStatus.Position) + } + } + + return nil +} + +// decodePosition is a helper that will decode the RelayLogPosition, if it's non-empty, and otherwise fall back +// to the executed position. +func decodePosition(status *replicationdatapb.Status) (mysql.Position, error) { + relayPos, err := mysql.DecodePosition(status.RelayLogPosition) + if err != nil { + log.Infof("Decode relay log position failed, err: %v", err) + return mysql.Position{}, err + } + if !relayPos.IsZero() { + return relayPos, nil + } + executedPos, err := mysql.DecodePosition(status.Position) + if err != nil { + log.Infof("Decode position failed, err: %v", err) + return mysql.Position{}, err + } + return executedPos, nil +} + // TabletExternallyReparented changes the type of new master for this shard to MASTER // and updates it's tablet record in the topo. Updating the shard record is handled // by the new master tablet diff --git a/proto/replicationdata.proto b/proto/replicationdata.proto index 4b0b65e8c9a..0b7e0733db4 100644 --- a/proto/replicationdata.proto +++ b/proto/replicationdata.proto @@ -25,6 +25,8 @@ package replicationdata; // and parsed into a Position and fields). message Status { string position = 1; + // RelayLogPosition will be empty for flavors that do not support returning the full GTIDSet from the relay log, such as MariaDB. + string relay_log_position = 8; bool slave_io_running = 2; bool slave_sql_running = 3; uint32 seconds_behind_master = 4;