github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/binlogreplication/binlog_replica_applier.go (about) 1 // Copyright 2023 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package binlogreplication 16 17 import ( 18 "fmt" 19 "io" 20 "strconv" 21 "strings" 22 "sync/atomic" 23 "time" 24 25 gms "github.com/dolthub/go-mysql-server" 26 "github.com/dolthub/go-mysql-server/sql" 27 "github.com/dolthub/go-mysql-server/sql/binlogreplication" 28 "github.com/dolthub/go-mysql-server/sql/mysql_db" 29 "github.com/dolthub/go-mysql-server/sql/planbuilder" 30 "github.com/dolthub/go-mysql-server/sql/rowexec" 31 "github.com/dolthub/go-mysql-server/sql/types" 32 "github.com/dolthub/vitess/go/mysql" 33 "github.com/dolthub/vitess/go/sqltypes" 34 vquery "github.com/dolthub/vitess/go/vt/proto/query" 35 "github.com/sirupsen/logrus" 36 37 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 38 "github.com/dolthub/dolt/go/libraries/doltcore/env" 39 "github.com/dolthub/dolt/go/libraries/doltcore/sqle" 40 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" 41 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/writer" 42 "github.com/dolthub/dolt/go/libraries/doltcore/sqlserver" 43 ) 44 45 // positionStore is a singleton instance for loading/saving binlog position state to disk for durable storage. 46 var positionStore = &binlogPositionStore{} 47 48 const ( 49 ERNetReadError = 1158 50 ERFatalReplicaError = 13117 51 ) 52 53 // binlogReplicaApplier represents the process that applies updates from a binlog connection. 54 // 55 // This type is NOT used concurrently – there is currently only one single applier process running to process binlog 56 // events, so the state in this type is NOT protected with a mutex. 57 type binlogReplicaApplier struct { 58 format *mysql.BinlogFormat 59 tableMapsById map[uint64]*mysql.TableMap 60 stopReplicationChan chan struct{} 61 currentGtid mysql.GTID 62 replicationSourceUuid string 63 currentPosition *mysql.Position // successfully executed GTIDs 64 filters *filterConfiguration 65 running atomic.Bool 66 } 67 68 func newBinlogReplicaApplier(filters *filterConfiguration) *binlogReplicaApplier { 69 return &binlogReplicaApplier{ 70 tableMapsById: make(map[uint64]*mysql.TableMap), 71 stopReplicationChan: make(chan struct{}), 72 filters: filters, 73 } 74 } 75 76 // Row Flags – https://mariadb.com/kb/en/rows_event_v1v2-rows_compressed_event_v1/ 77 78 // rowFlag_endOfStatement indicates that a row event with this flag set is the last event in a statement. 79 const rowFlag_endOfStatement = 0x0001 80 const rowFlag_noForeignKeyChecks = 0x0002 81 const rowFlag_noUniqueKeyChecks = 0x0004 82 const rowFlag_noCheckConstraints = 0x0010 83 84 // rowFlag_rowsAreComplete indicates that rows in this event are complete, and contain values for all columns of the table. 85 const rowFlag_rowsAreComplete = 0x0008 86 87 // Go spawns a new goroutine to run the applier's binlog event handler. 88 func (a *binlogReplicaApplier) Go(ctx *sql.Context) { 89 go func() { 90 a.running.Store(true) 91 err := a.replicaBinlogEventHandler(ctx) 92 a.running.Store(false) 93 if err != nil { 94 ctx.GetLogger().Errorf("unexpected error of type %T: '%v'", err, err.Error()) 95 DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, err.Error()) 96 } 97 }() 98 } 99 100 // IsRunning returns true if this binlog applier is running and has not been stopped, otherwise returns false. 101 func (a *binlogReplicaApplier) IsRunning() bool { 102 return a.running.Load() 103 } 104 105 // connectAndStartReplicationEventStream connects to the configured MySQL replication source, including pausing 106 // and retrying if errors are encountered. 107 func (a *binlogReplicaApplier) connectAndStartReplicationEventStream(ctx *sql.Context) (*mysql.Conn, error) { 108 var maxConnectionAttempts uint64 109 var connectRetryDelay uint32 110 DoltBinlogReplicaController.updateStatus(func(status *binlogreplication.ReplicaStatus) { 111 status.ReplicaIoRunning = binlogreplication.ReplicaIoConnecting 112 status.ReplicaSqlRunning = binlogreplication.ReplicaSqlRunning 113 maxConnectionAttempts = status.SourceRetryCount 114 connectRetryDelay = status.ConnectRetry 115 }) 116 117 var conn *mysql.Conn 118 var err error 119 for connectionAttempts := uint64(0); ; connectionAttempts++ { 120 replicaSourceInfo, err := loadReplicationConfiguration(ctx) 121 122 if replicaSourceInfo == nil { 123 err = ErrServerNotConfiguredAsReplica 124 DoltBinlogReplicaController.setIoError(ERFatalReplicaError, err.Error()) 125 return nil, err 126 } else if replicaSourceInfo.Uuid != "" { 127 a.replicationSourceUuid = replicaSourceInfo.Uuid 128 } 129 130 if replicaSourceInfo.Host == "" { 131 DoltBinlogReplicaController.setIoError(ERFatalReplicaError, ErrEmptyHostname.Error()) 132 return nil, ErrEmptyHostname 133 } else if replicaSourceInfo.User == "" { 134 DoltBinlogReplicaController.setIoError(ERFatalReplicaError, ErrEmptyUsername.Error()) 135 return nil, ErrEmptyUsername 136 } 137 138 connParams := mysql.ConnParams{ 139 Host: replicaSourceInfo.Host, 140 Port: int(replicaSourceInfo.Port), 141 Uname: replicaSourceInfo.User, 142 Pass: replicaSourceInfo.Password, 143 ConnectTimeoutMs: 4_000, 144 } 145 146 conn, err = mysql.Connect(ctx, &connParams) 147 if err != nil { 148 if connectionAttempts >= maxConnectionAttempts { 149 ctx.GetLogger().Errorf("Exceeded max connection attempts (%d) to source server", maxConnectionAttempts) 150 return nil, err 151 } 152 // If there was an error connecting (and we haven't used up all our retry attempts), listen for a 153 // STOP REPLICA signal or for the retry delay timer to fire. We need to use select here so that we don't 154 // block on our retry backoff and ignore the STOP REPLICA signal for a long time. 155 select { 156 case <-a.stopReplicationChan: 157 ctx.GetLogger().Debugf("Received stop replication signal while trying to connect") 158 return nil, ErrReplicationStopped 159 case <-time.After(time.Duration(connectRetryDelay) * time.Second): 160 // Nothing to do here if our timer completes; just fall through 161 } 162 } else { 163 break 164 } 165 } 166 167 // Request binlog events to start 168 // TODO: This should also have retry logic 169 err = a.startReplicationEventStream(ctx, conn) 170 if err != nil { 171 return nil, err 172 } 173 174 DoltBinlogReplicaController.updateStatus(func(status *binlogreplication.ReplicaStatus) { 175 status.ReplicaIoRunning = binlogreplication.ReplicaIoRunning 176 }) 177 178 return conn, nil 179 } 180 181 // startReplicationEventStream sends a request over |conn|, the connection to the MySQL source server, to begin 182 // sending binlog events. 183 func (a *binlogReplicaApplier) startReplicationEventStream(ctx *sql.Context, conn *mysql.Conn) error { 184 serverId, err := loadReplicaServerId() 185 if err != nil { 186 return err 187 } 188 189 position, err := positionStore.Load(ctx) 190 if err != nil { 191 return err 192 } 193 194 if position == nil { 195 // If the positionStore doesn't have a record of executed GTIDs, check to see if the gtid_purged system 196 // variable is set. If it holds a GTIDSet, then we use that as our starting position. As part of loading 197 // a mysqldump onto a replica, gtid_purged will be set to indicate where to start replication. 198 _, value, ok := sql.SystemVariables.GetGlobal("gtid_purged") 199 gtidPurged, isString := value.(string) 200 if ok && value != nil && isString { 201 // Starting in MySQL 8.0, when setting the GTID_PURGED sys variable, if the new value starts with '+', then 202 // the specified GTID Set value is added to the current GTID Set value to get a new GTID Set that contains 203 // all the previous GTIDs, plus the new ones from the current assignment. Dolt doesn't support this 204 // special behavior for appending to GTID Sets yet, so in this case the GTID_PURGED sys var will end up 205 // with a "+" prefix. For now, just ignore the "+" prefix if we see it. 206 // https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_gtid_purged 207 if strings.HasPrefix(gtidPurged, "+") { 208 ctx.GetLogger().Warnf("Ignoring unsupported '+' prefix on @@GTID_PURGED value") 209 gtidPurged = gtidPurged[1:] 210 } 211 212 purged, err := mysql.ParsePosition(mysqlFlavor, gtidPurged) 213 if err != nil { 214 return err 215 } 216 position = &purged 217 } 218 } 219 220 if position == nil { 221 // If we still don't have any record of executed GTIDs, we create a GTIDSet with just one transaction ID 222 // for the 0000 server ID. There doesn't seem to be a cleaner way of saying "start at the very beginning". 223 // 224 // Also... "starting position" is a bit of a misnomer – it's actually the processed GTIDs, which 225 // indicate the NEXT GTID where replication should start, but it's not as direct as specifying 226 // a starting position, like the Vitess function signature seems to suggest. 227 gtid := mysql.Mysql56GTID{ 228 Sequence: 1, 229 } 230 position = &mysql.Position{GTIDSet: gtid.GTIDSet()} 231 } 232 233 a.currentPosition = position 234 235 // Clear out the format description in case we're reconnecting, so that we don't use the old format description 236 // to interpret any event messages before we receive the new format description from the new stream. 237 a.format = nil 238 239 // If the source server has binlog checksums enabled (@@global.binlog_checksum), then the replica MUST 240 // set @master_binlog_checksum to handshake with the server to acknowledge that it knows that checksums 241 // are in use. Without this step, the server will just send back error messages saying that the replica 242 // does not support the binlog checksum algorithm in use on the primary. 243 // For more details, see: https://dev.mysql.com/worklog/task/?id=2540 244 _, err = conn.ExecuteFetch("set @master_binlog_checksum=@@global.binlog_checksum;", 0, false) 245 if err != nil { 246 return err 247 } 248 249 return conn.SendBinlogDumpCommand(serverId, *position) 250 } 251 252 // replicaBinlogEventHandler runs a loop, processing binlog events until the applier's stop replication channel 253 // receives a signal to stop. 254 func (a *binlogReplicaApplier) replicaBinlogEventHandler(ctx *sql.Context) error { 255 server := sqlserver.GetRunningServer() 256 if server == nil { 257 return fmt.Errorf("unable to access a running SQL server") 258 } 259 engine := server.Engine 260 261 var conn *mysql.Conn 262 var eventProducer *binlogEventProducer 263 264 // Process binlog events 265 for { 266 if conn == nil { 267 ctx.GetLogger().Debug("no binlog connection to source, attempting to establish one") 268 if eventProducer != nil { 269 eventProducer.Stop() 270 } 271 272 var err error 273 if conn, err = a.connectAndStartReplicationEventStream(ctx); err == ErrReplicationStopped { 274 return nil 275 } else if err != nil { 276 return err 277 } 278 eventProducer = newBinlogEventProducer(conn) 279 eventProducer.Go(ctx) 280 } 281 282 select { 283 case event := <-eventProducer.EventChan(): 284 err := a.processBinlogEvent(ctx, engine, event) 285 if err != nil { 286 ctx.GetLogger().Errorf("unexpected error of type %T: '%v'", err, err.Error()) 287 DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, err.Error()) 288 } 289 290 case err := <-eventProducer.ErrorChan(): 291 if sqlError, isSqlError := err.(*mysql.SQLError); isSqlError { 292 badConnection := sqlError.Message == io.EOF.Error() || 293 strings.HasPrefix(sqlError.Message, io.ErrUnexpectedEOF.Error()) 294 if badConnection { 295 DoltBinlogReplicaController.updateStatus(func(status *binlogreplication.ReplicaStatus) { 296 status.LastIoError = sqlError.Message 297 status.LastIoErrNumber = ERNetReadError 298 currentTime := time.Now() 299 status.LastIoErrorTimestamp = ¤tTime 300 }) 301 eventProducer.Stop() 302 eventProducer = nil 303 conn.Close() 304 conn = nil 305 } 306 } else { 307 // otherwise, log the error if it's something we don't expect and continue 308 ctx.GetLogger().Errorf("unexpected error of type %T: '%v'", err, err.Error()) 309 DoltBinlogReplicaController.setIoError(mysql.ERUnknownError, err.Error()) 310 } 311 312 case <-a.stopReplicationChan: 313 ctx.GetLogger().Trace("received stop replication signal") 314 eventProducer.Stop() 315 return nil 316 } 317 } 318 } 319 320 // processBinlogEvent processes a single binlog event message and returns an error if there were any problems 321 // processing it. 322 func (a *binlogReplicaApplier) processBinlogEvent(ctx *sql.Context, engine *gms.Engine, event mysql.BinlogEvent) error { 323 var err error 324 createCommit := false 325 commitToAllDatabases := false 326 327 // We don't support checksum validation, so we MUST strip off any checksum bytes if present, otherwise it gets 328 // interpreted as part of the payload and corrupts the data. Future checksum sizes, are not guaranteed to be the 329 // same size, so we can't strip the checksum until we've seen a valid Format binlog event that definitively 330 // tells us if checksums are enabled and what algorithm they use. We can NOT strip the checksum off of 331 // FormatDescription events, because FormatDescription always includes a CRC32 checksum, and Vitess depends on 332 // those bytes always being present when we parse the event into a FormatDescription type. 333 if a.format != nil && event.IsFormatDescription() == false { 334 var err error 335 event, _, err = event.StripChecksum(*a.format) 336 if err != nil { 337 msg := fmt.Sprintf("unable to strip checksum from binlog event: '%v'", err.Error()) 338 ctx.GetLogger().Error(msg) 339 DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, msg) 340 } 341 } 342 343 switch { 344 case event.IsRand(): 345 // A RAND_EVENT contains two seed values that set the rand_seed1 and rand_seed2 system variables that are 346 // used to compute the random number. For more details, see: https://mariadb.com/kb/en/rand_event/ 347 // Note: it is written only before a QUERY_EVENT and is NOT used with row-based logging. 348 ctx.GetLogger().Debug("Received binlog event: Rand") 349 350 case event.IsXID(): 351 // An XID event is generated for a COMMIT of a transaction that modifies one or more tables of an 352 // XA-capable storage engine. For more details, see: https://mariadb.com/kb/en/xid_event/ 353 ctx.GetLogger().Debug("Received binlog event: XID") 354 createCommit = true 355 commitToAllDatabases = true 356 357 case event.IsQuery(): 358 // A Query event represents a statement executed on the source server that should be executed on the 359 // replica. Used for all statements with statement-based replication, DDL statements with row-based replication 360 // as well as COMMITs for non-transactional engines such as MyISAM. 361 // For more details, see: https://mariadb.com/kb/en/query_event/ 362 query, err := event.Query(*a.format) 363 if err != nil { 364 return err 365 } 366 ctx.GetLogger().WithFields(logrus.Fields{ 367 "database": query.Database, 368 "charset": query.Charset, 369 "query": query.SQL, 370 "options": fmt.Sprintf("0x%x", query.Options), 371 "sql_mode": fmt.Sprintf("0x%x", query.SqlMode), 372 }).Debug("Received binlog event: Query") 373 374 // When executing SQL statements sent from the primary, we can't be sure what database was modified unless we 375 // look closely at the statement. For example, we could be connected to db01, but executed 376 // "create table db02.t (...);" – i.e., looking at query.Database is NOT enough to always determine the correct 377 // database that was modified, so instead, we commit to all databases when we see a Query binlog event to 378 // avoid issues with correctness, at the cost of being slightly less efficient 379 commitToAllDatabases = true 380 381 if query.Options&mysql.QFlagOptionAutoIsNull > 0 { 382 ctx.GetLogger().Tracef("Setting sql_auto_is_null ON") 383 ctx.SetSessionVariable(ctx, "sql_auto_is_null", 1) 384 } else { 385 ctx.GetLogger().Tracef("Setting sql_auto_is_null OFF") 386 ctx.SetSessionVariable(ctx, "sql_auto_is_null", 0) 387 } 388 389 if query.Options&mysql.QFlagOptionNotAutocommit > 0 { 390 ctx.GetLogger().Tracef("Setting autocommit=0") 391 ctx.SetSessionVariable(ctx, "autocommit", 0) 392 } else { 393 ctx.GetLogger().Tracef("Setting autocommit=1") 394 ctx.SetSessionVariable(ctx, "autocommit", 1) 395 } 396 397 if query.Options&mysql.QFlagOptionNoForeignKeyChecks > 0 { 398 ctx.GetLogger().Tracef("Setting foreign_key_checks=0") 399 ctx.SetSessionVariable(ctx, "foreign_key_checks", 0) 400 } else { 401 ctx.GetLogger().Tracef("Setting foreign_key_checks=1") 402 ctx.SetSessionVariable(ctx, "foreign_key_checks", 1) 403 } 404 405 // NOTE: unique_checks is not currently honored by Dolt 406 if query.Options&mysql.QFlagOptionRelaxedUniqueChecks > 0 { 407 ctx.GetLogger().Tracef("Setting unique_checks=0") 408 ctx.SetSessionVariable(ctx, "unique_checks", 0) 409 } else { 410 ctx.GetLogger().Tracef("Setting unique_checks=1") 411 ctx.SetSessionVariable(ctx, "unique_checks", 1) 412 } 413 414 ctx.SetCurrentDatabase(query.Database) 415 executeQueryWithEngine(ctx, engine, query.SQL) 416 createCommit = strings.ToLower(query.SQL) != "begin" 417 418 case event.IsRotate(): 419 // When a binary log file exceeds the configured size limit, a ROTATE_EVENT is written at the end of the file, 420 // pointing to the next file in the sequence. ROTATE_EVENT is generated locally and written to the binary log 421 // on the source server and it's also written when a FLUSH LOGS statement occurs on the source server. 422 // For more details, see: https://mariadb.com/kb/en/rotate_event/ 423 ctx.GetLogger().Debug("Received binlog event: Rotate") 424 425 case event.IsFormatDescription(): 426 // This is a descriptor event that is written to the beginning of a binary log file, at position 4 (after 427 // the 4 magic number bytes). For more details, see: https://mariadb.com/kb/en/format_description_event/ 428 format, err := event.Format() 429 if err != nil { 430 return err 431 } 432 a.format = &format 433 ctx.GetLogger().WithFields(logrus.Fields{ 434 "format": a.format, 435 "formatVersion": a.format.FormatVersion, 436 "serverVersion": a.format.ServerVersion, 437 "checksum": a.format.ChecksumAlgorithm, 438 }).Debug("Received binlog event: FormatDescription") 439 440 case event.IsPreviousGTIDs(): 441 // Logged in every binlog to record the current replication state. Consists of the last GTID seen for each 442 // replication domain. For more details, see: https://mariadb.com/kb/en/gtid_list_event/ 443 position, err := event.PreviousGTIDs(*a.format) 444 if err != nil { 445 return err 446 } 447 ctx.GetLogger().WithFields(logrus.Fields{ 448 "previousGtids": position.GTIDSet.String(), 449 }).Debug("Received binlog event: PreviousGTIDs") 450 451 case event.IsGTID(): 452 // For global transaction ID, used to start a new transaction event group, instead of the old BEGIN query event, 453 // and also to mark stand-alone (ddl). For more details, see: https://mariadb.com/kb/en/gtid_event/ 454 gtid, isBegin, err := event.GTID(*a.format) 455 if err != nil { 456 return err 457 } 458 if isBegin { 459 ctx.GetLogger().Errorf("unsupported binlog protocol message: GTID event with 'isBegin' set to true") 460 } 461 ctx.GetLogger().WithFields(logrus.Fields{ 462 "gtid": gtid, 463 "isBegin": isBegin, 464 }).Debug("Received binlog event: GTID") 465 a.currentGtid = gtid 466 // if the source's UUID hasn't been set yet, set it and persist it 467 if a.replicationSourceUuid == "" { 468 uuid := fmt.Sprintf("%v", gtid.SourceServer()) 469 err = persistSourceUuid(ctx, uuid) 470 if err != nil { 471 return err 472 } 473 a.replicationSourceUuid = uuid 474 } 475 476 case event.IsTableMap(): 477 // Used for row-based binary logging beginning (binlog_format=ROW or MIXED). This event precedes each row 478 // operation event and maps a table definition to a number, where the table definition consists of database 479 // and table names. For more details, see: https://mariadb.com/kb/en/table_map_event/ 480 // Note: TableMap events are sent before each row event, so there is no need to persist them between restarts. 481 tableId := event.TableID(*a.format) 482 tableMap, err := event.TableMap(*a.format) 483 if err != nil { 484 return err 485 } 486 ctx.GetLogger().WithFields(logrus.Fields{ 487 "id": tableId, 488 "tableName": tableMap.Name, 489 "database": tableMap.Database, 490 "flags": convertToHexString(tableMap.Flags), 491 "metadata": tableMap.Metadata, 492 "types": tableMap.Types, 493 }).Debug("Received binlog event: TableMap") 494 495 if tableId == 0xFFFFFF { 496 // Table ID 0xFFFFFF is a special value that indicates table maps can be freed. 497 ctx.GetLogger().Infof("binlog protocol message: table ID '0xFFFFFF'; clearing table maps") 498 a.tableMapsById = make(map[uint64]*mysql.TableMap) 499 } else { 500 flags := tableMap.Flags 501 if flags&rowFlag_endOfStatement == rowFlag_endOfStatement { 502 // nothing to be done for end of statement; just clear the flag 503 flags = flags &^ rowFlag_endOfStatement 504 } 505 if flags&rowFlag_noForeignKeyChecks == rowFlag_noForeignKeyChecks { 506 flags = flags &^ rowFlag_noForeignKeyChecks 507 } 508 if flags != 0 { 509 msg := fmt.Sprintf("unsupported binlog protocol message: TableMap event with unsupported flags '%x'", flags) 510 ctx.GetLogger().Errorf(msg) 511 DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, msg) 512 } 513 a.tableMapsById[tableId] = tableMap 514 } 515 516 case event.IsDeleteRows(), event.IsWriteRows(), event.IsUpdateRows(): 517 // A ROWS_EVENT is written for row based replication if data is inserted, deleted or updated. 518 // For more details, see: https://mariadb.com/kb/en/rows_event_v1v2-rows_compressed_event_v1/ 519 err = a.processRowEvent(ctx, event, engine) 520 if err != nil { 521 return err 522 } 523 524 default: 525 // We can't access the bytes directly because these non-interface types in Vitess are not exposed. 526 // Having a Bytes() or Type() method on the Vitess interface would let us clean this up. 527 byteString := fmt.Sprintf("%v", event) 528 if strings.HasPrefix(byteString, "{[0 0 0 0 27 ") { 529 // Type 27 is a Heartbeat event. This event does not appear in the binary log. It's only sent over the 530 // network by a primary to a replica to let it know that the primary is still alive, and is only sent 531 // when the primary has no binlog events to send to replica servers. 532 // For more details, see: https://mariadb.com/kb/en/heartbeat_log_event/ 533 ctx.GetLogger().Debug("Received binlog event: Heartbeat") 534 } else { 535 return fmt.Errorf("received unknown event: %v", event) 536 } 537 } 538 539 if createCommit { 540 var databasesToCommit []string 541 if commitToAllDatabases { 542 databasesToCommit = getAllUserDatabaseNames(ctx, engine) 543 for _, database := range databasesToCommit { 544 executeQueryWithEngine(ctx, engine, "use `"+database+"`;") 545 executeQueryWithEngine(ctx, engine, "commit;") 546 } 547 } 548 549 // Record the last GTID processed after the commit 550 a.currentPosition.GTIDSet = a.currentPosition.GTIDSet.AddGTID(a.currentGtid) 551 err := sql.SystemVariables.AssignValues(map[string]interface{}{"gtid_executed": a.currentPosition.GTIDSet.String()}) 552 if err != nil { 553 ctx.GetLogger().Errorf("unable to set @@GLOBAL.gtid_executed: %s", err.Error()) 554 } 555 err = positionStore.Save(ctx, a.currentPosition) 556 if err != nil { 557 return fmt.Errorf("unable to store GTID executed metadata to disk: %s", err.Error()) 558 } 559 560 // For now, create a Dolt commit from every data update. Eventually, we'll want to make this configurable. 561 ctx.GetLogger().Trace("Creating Dolt commit(s)") 562 for _, database := range databasesToCommit { 563 executeQueryWithEngine(ctx, engine, "use `"+database+"`;") 564 executeQueryWithEngine(ctx, engine, 565 fmt.Sprintf("call dolt_commit('-Am', 'Dolt binlog replica commit: GTID %s');", a.currentGtid)) 566 } 567 } 568 569 return nil 570 } 571 572 // processRowEvent processes a WriteRows, DeleteRows, or UpdateRows binlog event and returns an error if any problems 573 // were encountered. 574 func (a *binlogReplicaApplier) processRowEvent(ctx *sql.Context, event mysql.BinlogEvent, engine *gms.Engine) error { 575 var eventType string 576 switch { 577 case event.IsDeleteRows(): 578 eventType = "DeleteRows" 579 case event.IsWriteRows(): 580 eventType = "WriteRows" 581 case event.IsUpdateRows(): 582 eventType = "UpdateRows" 583 default: 584 return fmt.Errorf("unsupported event type: %v", event) 585 } 586 ctx.GetLogger().Debugf("Received binlog event: %s", eventType) 587 588 tableId := event.TableID(*a.format) 589 tableMap, ok := a.tableMapsById[tableId] 590 if !ok { 591 return fmt.Errorf("unable to find replication metadata for table ID: %d", tableId) 592 } 593 594 if a.filters.isTableFilteredOut(ctx, tableMap) { 595 return nil 596 } 597 598 rows, err := event.Rows(*a.format, tableMap) 599 if err != nil { 600 return err 601 } 602 603 ctx.GetLogger().WithFields(logrus.Fields{ 604 "flags": fmt.Sprintf("%x", rows.Flags), 605 }).Debugf("Processing rows from %s event", eventType) 606 607 flags := rows.Flags 608 foreignKeyChecksDisabled := false 609 if flags&rowFlag_endOfStatement > 0 { 610 // nothing to be done for end of statement; just clear the flag and move on 611 flags = flags &^ rowFlag_endOfStatement 612 } 613 if flags&rowFlag_noForeignKeyChecks > 0 { 614 foreignKeyChecksDisabled = true 615 flags = flags &^ rowFlag_noForeignKeyChecks 616 } 617 if flags != 0 { 618 msg := fmt.Sprintf("unsupported binlog protocol message: row event with unsupported flags '%x'", flags) 619 ctx.GetLogger().Errorf(msg) 620 DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, msg) 621 } 622 schema, err := getTableSchema(ctx, engine, tableMap.Name, tableMap.Database) 623 if err != nil { 624 return err 625 } 626 627 switch { 628 case event.IsDeleteRows(): 629 ctx.GetLogger().Debugf(" - Deleted Rows (table: %s)", tableMap.Name) 630 case event.IsUpdateRows(): 631 ctx.GetLogger().Debugf(" - Updated Rows (table: %s)", tableMap.Name) 632 case event.IsWriteRows(): 633 ctx.GetLogger().Debugf(" - Inserted Rows (table: %s)", tableMap.Name) 634 } 635 636 writeSession, tableWriter, err := getTableWriter(ctx, engine, tableMap.Name, tableMap.Database, foreignKeyChecksDisabled) 637 if err != nil { 638 return err 639 } 640 641 for _, row := range rows.Rows { 642 var identityRow, dataRow sql.Row 643 if len(row.Identify) > 0 { 644 identityRow, err = parseRow(ctx, tableMap, schema, rows.IdentifyColumns, row.NullIdentifyColumns, row.Identify) 645 if err != nil { 646 return err 647 } 648 ctx.GetLogger().Debugf(" - Identity: %v ", sql.FormatRow(identityRow)) 649 } 650 651 if len(row.Data) > 0 { 652 dataRow, err = parseRow(ctx, tableMap, schema, rows.DataColumns, row.NullColumns, row.Data) 653 if err != nil { 654 return err 655 } 656 ctx.GetLogger().Debugf(" - Data: %v ", sql.FormatRow(dataRow)) 657 } 658 659 switch { 660 case event.IsDeleteRows(): 661 err = tableWriter.Delete(ctx, identityRow) 662 case event.IsWriteRows(): 663 err = tableWriter.Insert(ctx, dataRow) 664 case event.IsUpdateRows(): 665 err = tableWriter.Update(ctx, identityRow, dataRow) 666 } 667 if err != nil { 668 return err 669 } 670 671 } 672 673 err = closeWriteSession(ctx, engine, tableMap.Database, writeSession) 674 if err != nil { 675 return err 676 } 677 678 return nil 679 } 680 681 // 682 // Helper functions 683 // 684 685 // closeWriteSession flushes and closes the specified |writeSession| and returns an error if anything failed. 686 func closeWriteSession(ctx *sql.Context, engine *gms.Engine, databaseName string, writeSession writer.WriteSession) error { 687 newWorkingSet, err := writeSession.Flush(ctx) 688 if err != nil { 689 return err 690 } 691 692 database, err := engine.Analyzer.Catalog.Database(ctx, databaseName) 693 if err != nil { 694 return err 695 } 696 if privDatabase, ok := database.(mysql_db.PrivilegedDatabase); ok { 697 database = privDatabase.Unwrap() 698 } 699 sqlDatabase, ok := database.(sqle.Database) 700 if !ok { 701 return fmt.Errorf("unexpected database type: %T", database) 702 } 703 704 hash, err := newWorkingSet.HashOf() 705 if err != nil { 706 return err 707 } 708 709 return sqlDatabase.DbData().Ddb.UpdateWorkingSet(ctx, newWorkingSet.Ref(), newWorkingSet, hash, newWorkingSet.Meta(), nil) 710 } 711 712 // getTableSchema returns a sql.Schema for the specified table in the specified database. 713 func getTableSchema(ctx *sql.Context, engine *gms.Engine, tableName, databaseName string) (sql.Schema, error) { 714 database, err := engine.Analyzer.Catalog.Database(ctx, databaseName) 715 if err != nil { 716 return nil, err 717 } 718 table, ok, err := database.GetTableInsensitive(ctx, tableName) 719 if err != nil { 720 return nil, err 721 } 722 if !ok { 723 return nil, fmt.Errorf("unable to find table %q", tableName) 724 } 725 726 return table.Schema(), nil 727 } 728 729 // getTableWriter returns a WriteSession and a TableWriter for writing to the specified |table| in the specified |database|. 730 func getTableWriter(ctx *sql.Context, engine *gms.Engine, tableName, databaseName string, foreignKeyChecksDisabled bool) (writer.WriteSession, writer.TableWriter, error) { 731 database, err := engine.Analyzer.Catalog.Database(ctx, databaseName) 732 if err != nil { 733 return nil, nil, err 734 } 735 if privDatabase, ok := database.(mysql_db.PrivilegedDatabase); ok { 736 database = privDatabase.Unwrap() 737 } 738 sqlDatabase, ok := database.(sqle.Database) 739 if !ok { 740 return nil, nil, fmt.Errorf("unexpected database type: %T", database) 741 } 742 743 binFormat := sqlDatabase.DbData().Ddb.Format() 744 745 ws, err := env.WorkingSet(ctx, sqlDatabase.GetDoltDB(), sqlDatabase.DbData().Rsr) 746 if err != nil { 747 return nil, nil, err 748 } 749 750 tracker, err := dsess.NewAutoIncrementTracker(ctx, sqlDatabase.Name(), ws) 751 if err != nil { 752 return nil, nil, err 753 } 754 755 options := sqlDatabase.EditOptions() 756 options.ForeignKeyChecksDisabled = foreignKeyChecksDisabled 757 writeSession := writer.NewWriteSession(binFormat, ws, tracker, options) 758 759 ds := dsess.DSessFromSess(ctx.Session) 760 setter := ds.SetWorkingRoot 761 tableWriter, err := writeSession.GetTableWriter(ctx, doltdb.TableName{Name: tableName}, databaseName, setter) 762 if err != nil { 763 return nil, nil, err 764 } 765 766 return writeSession, tableWriter, nil 767 } 768 769 // parseRow parses the binary row data from a MySQL binlog event and converts it into a go-mysql-server Row using the 770 // |schema| information provided. |columnsPresentBitmap| indicates which column values are present in |data| and 771 // |nullValuesBitmap| indicates which columns have null values and are NOT present in |data|. 772 func parseRow(ctx *sql.Context, tableMap *mysql.TableMap, schema sql.Schema, columnsPresentBitmap, nullValuesBitmap mysql.Bitmap, data []byte) (sql.Row, error) { 773 var parsedRow sql.Row 774 pos := 0 775 776 for i, typ := range tableMap.Types { 777 column := schema[i] 778 779 if columnsPresentBitmap.Bit(i) == false { 780 parsedRow = append(parsedRow, nil) 781 continue 782 } 783 784 var value sqltypes.Value 785 var err error 786 if nullValuesBitmap.Bit(i) { 787 value, err = sqltypes.NewValue(vquery.Type_NULL_TYPE, nil) 788 if err != nil { 789 return nil, err 790 } 791 } else { 792 var length int 793 value, length, err = mysql.CellValue(data, pos, typ, tableMap.Metadata[i], getSignedType(column)) 794 if err != nil { 795 return nil, err 796 } 797 pos += length 798 } 799 800 convertedValue, err := convertSqlTypesValue(ctx, value, column) 801 if err != nil { 802 return nil, err 803 } 804 parsedRow = append(parsedRow, convertedValue) 805 } 806 807 return parsedRow, nil 808 } 809 810 // getSignedType returns a Vitess query.Type that can be used with the Vitess mysql.CellValue function to correctly 811 // parse the value of a signed or unsigned integer value. The mysql.TableMap structure provides information about the 812 // type, but it doesn't indicate if an integer type is signed or unsigned, so we have to look at the column type in the 813 // replica's schema and then choose any signed/unsigned query.Type to pass into mysql.CellValue to instruct it whether 814 // to treat a value as signed or unsigned – the actual type does not matter, only the signed/unsigned property. 815 func getSignedType(column *sql.Column) vquery.Type { 816 switch column.Type.Type() { 817 case vquery.Type_UINT8, vquery.Type_UINT16, vquery.Type_UINT24, vquery.Type_UINT32, vquery.Type_UINT64: 818 // For any unsigned integer value, we just need to return any unsigned numeric type to signal to Vitess to treat 819 // the value as unsigned. The actual type returned doesn't matter – only the signed/unsigned property is used. 820 return vquery.Type_UINT64 821 default: 822 return vquery.Type_INT64 823 } 824 } 825 826 // convertSqlTypesValues converts a sqltypes.Value instance (from vitess) into a sql.Type value (for go-mysql-server). 827 func convertSqlTypesValue(ctx *sql.Context, value sqltypes.Value, column *sql.Column) (interface{}, error) { 828 if value.IsNull() { 829 return nil, nil 830 } 831 832 var convertedValue interface{} 833 var err error 834 switch { 835 case types.IsEnum(column.Type), types.IsSet(column.Type): 836 atoi, err := strconv.Atoi(value.ToString()) 837 if err != nil { 838 return nil, err 839 } 840 convertedValue, _, err = column.Type.Convert(atoi) 841 case types.IsDecimal(column.Type): 842 // Decimal values need to have any leading/trailing whitespace trimmed off 843 // TODO: Consider moving this into DecimalType_.Convert; if DecimalType_.Convert handled trimming 844 // leading/trailing whitespace, this special case for Decimal types wouldn't be needed. 845 convertedValue, _, err = column.Type.Convert(strings.TrimSpace(value.ToString())) 846 case types.IsJSON(column.Type): 847 convertedValue, err = convertVitessJsonExpressionString(ctx, value) 848 default: 849 convertedValue, _, err = column.Type.Convert(value.ToString()) 850 } 851 if err != nil { 852 return nil, fmt.Errorf("unable to convert value %q, for column of type %T: %v", value.ToString(), column.Type, err.Error()) 853 } 854 855 return convertedValue, nil 856 } 857 858 // convertVitessJsonExpressionString extracts a JSON value from the specified |value| instance, which Vitess has 859 // encoded as a SQL expression string. Vitess parses the binary JSON representation from an incoming binlog event, 860 // and converts it into an expression string containing JSON_OBJECT and JSON_ARRAY function calls. Because we don't 861 // have access to the raw JSON string or JSON bytes, we have to do extra work to translate from Vitess' SQL 862 // expression syntax into a raw JSON string value that we can pass to the storage layer. If Vitess kept around the 863 // raw string representation and returned it from value.ToString, this logic would not be necessary. 864 func convertVitessJsonExpressionString(ctx *sql.Context, value sqltypes.Value) (interface{}, error) { 865 if value.Type() != vquery.Type_EXPRESSION { 866 return nil, fmt.Errorf("invalid sqltypes.Value specified; expected a Value instance with an Expression type") 867 } 868 869 strValue := value.String() 870 if strings.HasPrefix(strValue, "EXPRESSION(") { 871 strValue = strValue[len("EXPRESSION(") : len(strValue)-1] 872 } 873 874 server := sqlserver.GetRunningServer() 875 if server == nil { 876 return nil, fmt.Errorf("unable to access running SQL server") 877 } 878 879 binder := planbuilder.New(ctx, server.Engine.Analyzer.Catalog, server.Engine.Parser) 880 node, _, _, err := binder.Parse("SELECT "+strValue, false) 881 if err != nil { 882 return nil, err 883 } 884 885 analyze, err := server.Engine.Analyzer.Analyze(ctx, node, nil) 886 if err != nil { 887 return nil, err 888 } 889 890 rowIter, err := rowexec.DefaultBuilder.Build(ctx, analyze, nil) 891 if err != nil { 892 return nil, err 893 } 894 row, err := rowIter.Next(ctx) 895 if err != nil { 896 return nil, err 897 } 898 899 return row[0], nil 900 } 901 902 func getAllUserDatabaseNames(ctx *sql.Context, engine *gms.Engine) []string { 903 allDatabases := engine.Analyzer.Catalog.AllDatabases(ctx) 904 userDatabaseNames := make([]string, 0, len(allDatabases)) 905 for _, database := range allDatabases { 906 switch database.Name() { 907 case "information_schema", "mysql": 908 default: 909 userDatabaseNames = append(userDatabaseNames, database.Name()) 910 } 911 } 912 return userDatabaseNames 913 } 914 915 // loadReplicaServerId loads the @@GLOBAL.server_id system variable needed to register the replica with the source, 916 // and returns an error specific to replication configuration if the variable is not set to a valid value. 917 func loadReplicaServerId() (uint32, error) { 918 _, value, ok := sql.SystemVariables.GetGlobal("server_id") 919 if !ok { 920 return 0, fmt.Errorf("no server_id global system variable set") 921 } 922 923 serverId, ok := value.(uint32) 924 if !ok || serverId == 0 { 925 return 0, fmt.Errorf("invalid server ID configured for @@GLOBAL.server_id (%v); "+ 926 "must be an integer greater than zero and less than 4,294,967,296", serverId) 927 } 928 929 return serverId, nil 930 } 931 932 func executeQueryWithEngine(ctx *sql.Context, engine *gms.Engine, query string) { 933 // Create a sub-context when running queries against the engine, so that we get an accurate query start time. 934 queryCtx := sql.NewContext(ctx, sql.WithSession(ctx.Session)) 935 936 if queryCtx.GetCurrentDatabase() == "" { 937 ctx.GetLogger().WithFields(logrus.Fields{ 938 "query": query, 939 }).Warn("No current database selected") 940 } 941 942 _, iter, err := engine.Query(queryCtx, query) 943 if err != nil { 944 // Log any errors, except for commits with "nothing to commit" 945 if err.Error() != "nothing to commit" { 946 queryCtx.GetLogger().WithFields(logrus.Fields{ 947 "error": err.Error(), 948 "query": query, 949 }).Errorf("Error executing query") 950 msg := fmt.Sprintf("Error executing query: %v", err.Error()) 951 DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, msg) 952 } 953 return 954 } 955 for { 956 _, err := iter.Next(queryCtx) 957 if err != nil { 958 if err != io.EOF { 959 queryCtx.GetLogger().Errorf("ERROR reading query results: %v ", err.Error()) 960 } 961 return 962 } 963 } 964 } 965 966 // 967 // Generic util functions... 968 // 969 970 // convertToHexString returns a lower-case hex string representation of the specified uint16 value |v|. 971 func convertToHexString(v uint16) string { 972 return fmt.Sprintf("%x", v) 973 } 974 975 // keys returns a slice containing the keys in the specified map |m|. 976 func keys[K comparable, V any](m map[K]V) []K { 977 keys := make([]K, 0, len(m)) 978 for k := range m { 979 keys = append(keys, k) 980 } 981 return keys 982 }