func TestStreamerParseEventsBeginWithoutCommit(t *testing.T) { input := []replication.BinlogEvent{ rotateEvent{}, formatEvent{}, queryEvent{query: replication.Query{ Database: "vt_test_keyspace", SQL: "insert into vt_a(eid, id) values (1, 1) /* _stream vt_a (eid id ) (1 1 ); */"}}, queryEvent{query: replication.Query{ Database: "vt_test_keyspace", SQL: "BEGIN"}}, xidEvent{}, } events := make(chan replication.BinlogEvent) want := []pb.BinlogTransaction{ pb.BinlogTransaction{ Statements: []*pb.BinlogTransaction_Statement{ {Category: pb.BinlogTransaction_Statement_BL_SET, Sql: "SET TIMESTAMP=1407805592"}, {Category: pb.BinlogTransaction_Statement_BL_DML, Sql: "insert into vt_a(eid, id) values (1, 1) /* _stream vt_a (eid id ) (1 1 ); */"}, }, Timestamp: 1407805592, TransactionId: replication.EncodeGTID(replication.MariadbGTID{ Domain: 0, Server: 62344, Sequence: 0x0d, }), }, pb.BinlogTransaction{ Statements: []*pb.BinlogTransaction_Statement{}, Timestamp: 1407805592, TransactionId: replication.EncodeGTID(replication.MariadbGTID{ Domain: 0, Server: 62344, Sequence: 0x0d, }), }, } var got []pb.BinlogTransaction sendTransaction := func(trans *pb.BinlogTransaction) error { got = append(got, *trans) return nil } bls := NewStreamer("vt_test_keyspace", nil, nil, replication.Position{}, sendTransaction) go sendTestEvents(events, input) svm := &sync2.ServiceManager{} svm.Go(func(ctx *sync2.ServiceContext) error { _, err := bls.parseEvents(ctx, events) return err }) if err := svm.Join(); err != ErrServerEOF { t.Errorf("unexpected error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("binlogConnStreamer.parseEvents(): got %v, want %v", got, want) } }
func TestStreamerParseEventsMariadbBeginGTID(t *testing.T) { input := []replication.BinlogEvent{ mariadbRotateEvent, mariadbFormatEvent, mariadbBeginGTIDEvent, mariadbInsertEvent, mariadbXidEvent, } events := make(chan replication.BinlogEvent) want := []pb.BinlogTransaction{ pb.BinlogTransaction{ Statements: []*pb.BinlogTransaction_Statement{ {Category: pb.BinlogTransaction_Statement_BL_SET, Charset: charset, Sql: "SET TIMESTAMP=1409892744"}, {Category: pb.BinlogTransaction_Statement_BL_DML, Charset: charset, Sql: "insert into vt_insert_test(msg) values ('test 0') /* _stream vt_insert_test (id ) (null ); */"}, }, Timestamp: 1409892744, TransactionId: replication.EncodeGTID(replication.MariadbGTID{ Domain: 0, Server: 62344, Sequence: 10, }), }, } var got []pb.BinlogTransaction sendTransaction := func(trans *pb.BinlogTransaction) error { got = append(got, *trans) return nil } bls := NewStreamer("vt_test_keyspace", nil, nil, replication.Position{}, sendTransaction) go sendTestEvents(events, input) svm := &sync2.ServiceManager{} svm.Go(func(ctx *sync2.ServiceContext) error { _, err := bls.parseEvents(ctx, events) return err }) if err := svm.Join(); err != ErrServerEOF { t.Errorf("unexpected error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("binlogConnStreamer.parseEvents(): got %v, want %v", got, want) } }
func TestStreamerParseEventsMariadbStandaloneGTID(t *testing.T) { input := []replication.BinlogEvent{ mariadbRotateEvent, mariadbFormatEvent, mariadbStandaloneGTIDEvent, mariadbCreateEvent, } events := make(chan replication.BinlogEvent) want := []pb.BinlogTransaction{ pb.BinlogTransaction{ Statements: []*pb.BinlogTransaction_Statement{ {Category: pb.BinlogTransaction_Statement_BL_SET, Charset: &pb.Charset{Client: 8, Conn: 8, Server: 33}, Sql: "SET TIMESTAMP=1409892744"}, {Category: pb.BinlogTransaction_Statement_BL_DDL, Charset: &pb.Charset{Client: 8, Conn: 8, Server: 33}, Sql: "create table if not exists vt_insert_test (\nid bigint auto_increment,\nmsg varchar(64),\nprimary key (id)\n) Engine=InnoDB"}, }, Timestamp: 1409892744, TransactionId: replication.EncodeGTID(replication.MariadbGTID{ Domain: 0, Server: 62344, Sequence: 9, }), }, } var got []pb.BinlogTransaction sendTransaction := func(trans *pb.BinlogTransaction) error { got = append(got, *trans) return nil } bls := NewStreamer("vt_test_keyspace", nil, nil, replication.Position{}, sendTransaction) go sendTestEvents(events, input) svm := &sync2.ServiceManager{} svm.Go(func(ctx *sync2.ServiceContext) error { _, err := bls.parseEvents(ctx, events) return err }) if err := svm.Join(); err != ErrServerEOF { t.Errorf("unexpected error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("binlogConnStreamer.parseEvents(): got %v, want %v", got, want) } }
// parseEvents processes the raw binlog dump stream from the server, one event // at a time, and groups them into transactions. It is called from within the // service function launched by Stream(). // // If the sendTransaction func returns io.EOF, parseEvents returns ErrClientEOF. // If the events channel is closed, parseEvents returns ErrServerEOF. func (bls *Streamer) parseEvents(ctx *sync2.ServiceContext, events <-chan replication.BinlogEvent) (replication.Position, error) { var statements []*binlogdatapb.BinlogTransaction_Statement var format replication.BinlogFormat var gtid replication.GTID var pos = bls.startPos var autocommit = true var err error // A begin can be triggered either by a BEGIN query, or by a GTID_EVENT. begin := func() { if statements != nil { // If this happened, it would be a legitimate error. log.Errorf("BEGIN in binlog stream while still in another transaction; dropping %d statements: %v", len(statements), statements) binlogStreamerErrors.Add("ParseEvents", 1) } statements = make([]*binlogdatapb.BinlogTransaction_Statement, 0, 10) autocommit = false } // A commit can be triggered either by a COMMIT query, or by an XID_EVENT. // Statements that aren't wrapped in BEGIN/COMMIT are committed immediately. commit := func(timestamp uint32) error { trans := &binlogdatapb.BinlogTransaction{ Statements: statements, Timestamp: int64(timestamp), TransactionId: replication.EncodeGTID(gtid), } if err = bls.sendTransaction(trans); err != nil { if err == io.EOF { return ErrClientEOF } return fmt.Errorf("send reply error: %v", err) } statements = nil autocommit = true return nil } // Parse events. for ctx.IsRunning() { var ev replication.BinlogEvent var ok bool select { case ev, ok = <-events: if !ok { // events channel has been closed, which means the connection died. log.Infof("reached end of binlog event stream") return pos, ErrServerEOF } case <-ctx.ShuttingDown: log.Infof("stopping early due to binlog Streamer service shutdown") return pos, nil } // Validate the buffer before reading fields from it. if !ev.IsValid() { return pos, fmt.Errorf("can't parse binlog event, invalid data: %#v", ev) } // We need to keep checking for FORMAT_DESCRIPTION_EVENT even after we've // seen one, because another one might come along (e.g. on log rotate due to // binlog settings change) that changes the format. if ev.IsFormatDescription() { format, err = ev.Format() if err != nil { return pos, fmt.Errorf("can't parse FORMAT_DESCRIPTION_EVENT: %v, event data: %#v", err, ev) } continue } // We can't parse anything until we get a FORMAT_DESCRIPTION_EVENT that // tells us the size of the event header. if format.IsZero() { // The only thing that should come before the FORMAT_DESCRIPTION_EVENT // is a fake ROTATE_EVENT, which the master sends to tell us the name // of the current log file. if ev.IsRotate() { continue } return pos, fmt.Errorf("got a real event before FORMAT_DESCRIPTION_EVENT: %#v", ev) } // Strip the checksum, if any. We don't actually verify the checksum, so discard it. ev, _, err = ev.StripChecksum(format) if err != nil { return pos, fmt.Errorf("can't strip checksum from binlog event: %v, event data: %#v", err, ev) } // Update the GTID if the event has one. The actual event type could be // something special like GTID_EVENT (MariaDB, MySQL 5.6), or it could be // an arbitrary event with a GTID in the header (Google MySQL). if ev.HasGTID(format) { gtid, err = ev.GTID(format) if err != nil { return pos, fmt.Errorf("can't get GTID from binlog event: %v, event data: %#v", err, ev) } pos = replication.AppendGTID(pos, gtid) } switch { case ev.IsGTID(): // GTID_EVENT if ev.IsBeginGTID(format) { begin() } case ev.IsXID(): // XID_EVENT (equivalent to COMMIT) if err = commit(ev.Timestamp()); err != nil { return pos, err } case ev.IsIntVar(): // INTVAR_EVENT name, value, err := ev.IntVar(format) if err != nil { return pos, fmt.Errorf("can't parse INTVAR_EVENT: %v, event data: %#v", err, ev) } statements = append(statements, &binlogdatapb.BinlogTransaction_Statement{ Category: binlogdatapb.BinlogTransaction_Statement_BL_SET, Sql: fmt.Sprintf("SET %s=%d", name, value), }) case ev.IsRand(): // RAND_EVENT seed1, seed2, err := ev.Rand(format) if err != nil { return pos, fmt.Errorf("can't parse RAND_EVENT: %v, event data: %#v", err, ev) } statements = append(statements, &binlogdatapb.BinlogTransaction_Statement{ Category: binlogdatapb.BinlogTransaction_Statement_BL_SET, Sql: fmt.Sprintf("SET @@RAND_SEED1=%d, @@RAND_SEED2=%d", seed1, seed2), }) case ev.IsQuery(): // QUERY_EVENT // Extract the query string and group into transactions. q, err := ev.Query(format) if err != nil { return pos, fmt.Errorf("can't get query from binlog event: %v, event data: %#v", err, ev) } switch cat := getStatementCategory(q.SQL); cat { case binlogdatapb.BinlogTransaction_Statement_BL_BEGIN: begin() case binlogdatapb.BinlogTransaction_Statement_BL_ROLLBACK: // Rollbacks are possible under some circumstances. Since the stream // client keeps track of its replication position by updating the set // of GTIDs it's seen, we must commit an empty transaction so the client // can update its position. statements = nil fallthrough case binlogdatapb.BinlogTransaction_Statement_BL_COMMIT: if err = commit(ev.Timestamp()); err != nil { return pos, err } default: // BL_DDL, BL_DML, BL_SET, BL_UNRECOGNIZED if q.Database != "" && q.Database != bls.dbname { // Skip cross-db statements. continue } setTimestamp := &binlogdatapb.BinlogTransaction_Statement{ Category: binlogdatapb.BinlogTransaction_Statement_BL_SET, Sql: fmt.Sprintf("SET TIMESTAMP=%d", ev.Timestamp()), } statement := &binlogdatapb.BinlogTransaction_Statement{ Category: cat, Sql: q.SQL, } // If the statement has a charset and it's different than our client's // default charset, send it along with the statement. // If our client hasn't told us its charset, always send it. if bls.clientCharset == nil || (q.Charset != nil && *q.Charset != *bls.clientCharset) { setTimestamp.Charset = q.Charset statement.Charset = q.Charset } statements = append(statements, setTimestamp, statement) if autocommit { if err = commit(ev.Timestamp()); err != nil { return pos, err } } } } } return pos, nil }