Initial commit

This commit is contained in:
Donny
2019-04-22 20:46:32 +08:00
commit 49ab8aadd1
25441 changed files with 4055000 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"backup.go",
"binlogstreamer.go",
"binlogsyncer.go",
"const.go",
"doc.go",
"event.go",
"generic_event.go",
"json_binary.go",
"parser.go",
"row_event.go",
"time.go",
],
importmap = "go-common/vendor/github.com/siddontang/go-mysql/replication",
importpath = "github.com/siddontang/go-mysql/replication",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/juju/errors:go_default_library",
"//vendor/github.com/satori/go.uuid:go_default_library",
"//vendor/github.com/shopspring/decimal:go_default_library",
"//vendor/github.com/siddontang/go-mysql/client:go_default_library",
"//vendor/github.com/siddontang/go-mysql/mysql:go_default_library",
"//vendor/github.com/siddontang/go/hack:go_default_library",
"//vendor/github.com/sirupsen/logrus:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,96 @@
package replication
import (
"context"
"io"
"os"
"path"
"time"
"github.com/juju/errors"
. "github.com/siddontang/go-mysql/mysql"
)
// Like mysqlbinlog remote raw backup
// Backup remote binlog from position (filename, offset) and write in backupDir
func (b *BinlogSyncer) StartBackup(backupDir string, p Position, timeout time.Duration) error {
if timeout == 0 {
// a very long timeout here
timeout = 30 * 3600 * 24 * time.Second
}
// Force use raw mode
b.parser.SetRawMode(true)
os.MkdirAll(backupDir, 0755)
s, err := b.StartSync(p)
if err != nil {
return errors.Trace(err)
}
var filename string
var offset uint32
var f *os.File
defer func() {
if f != nil {
f.Close()
}
}()
for {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
e, err := s.GetEvent(ctx)
cancel()
if err == context.DeadlineExceeded {
return nil
}
if err != nil {
return errors.Trace(err)
}
offset = e.Header.LogPos
if e.Header.EventType == ROTATE_EVENT {
rotateEvent := e.Event.(*RotateEvent)
filename = string(rotateEvent.NextLogName)
if e.Header.Timestamp == 0 || offset == 0 {
// fake rotate event
continue
}
} else if e.Header.EventType == FORMAT_DESCRIPTION_EVENT {
// FormateDescriptionEvent is the first event in binlog, we will close old one and create a new
if f != nil {
f.Close()
}
if len(filename) == 0 {
return errors.Errorf("empty binlog filename for FormateDescriptionEvent")
}
f, err = os.OpenFile(path.Join(backupDir, filename), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return errors.Trace(err)
}
// write binlog header fe'bin'
if _, err = f.Write(BinLogFileHeader); err != nil {
return errors.Trace(err)
}
}
if n, err := f.Write(e.RawData); err != nil {
return errors.Trace(err)
} else if n != len(e.RawData) {
return errors.Trace(io.ErrShortWrite)
}
}
return nil
}

View File

@@ -0,0 +1,61 @@
package replication
import (
"context"
"github.com/juju/errors"
log "github.com/sirupsen/logrus"
)
var (
ErrNeedSyncAgain = errors.New("Last sync error or closed, try sync and get event again")
ErrSyncClosed = errors.New("Sync was closed")
)
// BinlogStreamer gets the streaming event.
type BinlogStreamer struct {
ch chan *BinlogEvent
ech chan error
err error
}
// GetEvent gets the binlog event one by one, it will block until Syncer receives any events from MySQL
// or meets a sync error. You can pass a context (like Cancel or Timeout) to break the block.
func (s *BinlogStreamer) GetEvent(ctx context.Context) (*BinlogEvent, error) {
if s.err != nil {
return nil, ErrNeedSyncAgain
}
select {
case c := <-s.ch:
return c, nil
case s.err = <-s.ech:
return nil, s.err
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (s *BinlogStreamer) close() {
s.closeWithError(ErrSyncClosed)
}
func (s *BinlogStreamer) closeWithError(err error) {
if err == nil {
err = ErrSyncClosed
}
log.Errorf("close sync with err: %v", err)
select {
case s.ech <- err:
default:
}
}
func newBinlogStreamer() *BinlogStreamer {
s := new(BinlogStreamer)
s.ch = make(chan *BinlogEvent, 10240)
s.ech = make(chan error, 4)
return s
}

View File

@@ -0,0 +1,742 @@
package replication
import (
"context"
"crypto/tls"
"encoding/binary"
"fmt"
"net"
"os"
"sync"
"time"
"github.com/juju/errors"
"github.com/satori/go.uuid"
"github.com/siddontang/go-mysql/client"
. "github.com/siddontang/go-mysql/mysql"
log "github.com/sirupsen/logrus"
)
var (
errSyncRunning = errors.New("Sync is running, must Close first")
)
// BinlogSyncerConfig is the configuration for BinlogSyncer.
type BinlogSyncerConfig struct {
// ServerID is the unique ID in cluster.
ServerID uint32
// Flavor is "mysql" or "mariadb", if not set, use "mysql" default.
Flavor string
// Host is for MySQL server host.
Host string
// Port is for MySQL server port.
Port uint16
// User is for MySQL user.
User string
// Password is for MySQL password.
Password string
// Localhost is local hostname if register salve.
// If not set, use os.Hostname() instead.
Localhost string
// Charset is for MySQL client character set
Charset string
// SemiSyncEnabled enables semi-sync or not.
SemiSyncEnabled bool
// RawModeEnabled is for not parsing binlog event.
RawModeEnabled bool
// If not nil, use the provided tls.Config to connect to the database using TLS/SSL.
TLSConfig *tls.Config
// Use replication.Time structure for timestamp and datetime.
// We will use Local location for timestamp and UTC location for datatime.
ParseTime bool
// Use decimal.Decimal structure for decimals.
UseDecimal bool
// RecvBufferSize sets the size in bytes of the operating system's receive buffer associated with the connection.
RecvBufferSize int
// master heartbeat period
HeartbeatPeriod time.Duration
// read timeout
ReadTimeout time.Duration
}
// BinlogSyncer syncs binlog event from server.
type BinlogSyncer struct {
m sync.RWMutex
cfg BinlogSyncerConfig
c *client.Conn
wg sync.WaitGroup
parser *BinlogParser
nextPos Position
useGTID bool
gset GTIDSet
running bool
ctx context.Context
cancel context.CancelFunc
lastConnectionID uint32
}
// NewBinlogSyncer creates the BinlogSyncer with cfg.
func NewBinlogSyncer(cfg BinlogSyncerConfig) *BinlogSyncer {
// Clear the Password to avoid outputing it in log.
pass := cfg.Password
cfg.Password = ""
log.Infof("create BinlogSyncer with config %v", cfg)
cfg.Password = pass
b := new(BinlogSyncer)
b.cfg = cfg
b.parser = NewBinlogParser()
b.parser.SetRawMode(b.cfg.RawModeEnabled)
b.parser.SetParseTime(b.cfg.ParseTime)
b.parser.SetUseDecimal(b.cfg.UseDecimal)
b.useGTID = false
b.running = false
b.ctx, b.cancel = context.WithCancel(context.Background())
return b
}
// Close closes the BinlogSyncer.
func (b *BinlogSyncer) Close() {
b.m.Lock()
defer b.m.Unlock()
b.close()
}
func (b *BinlogSyncer) close() {
if b.isClosed() {
return
}
log.Info("syncer is closing...")
b.running = false
b.cancel()
if b.c != nil {
b.c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
}
b.wg.Wait()
if b.c != nil {
b.c.Close()
}
log.Info("syncer is closed")
}
func (b *BinlogSyncer) isClosed() bool {
select {
case <-b.ctx.Done():
return true
default:
return false
}
}
func (b *BinlogSyncer) registerSlave() error {
if b.c != nil {
b.c.Close()
}
log.Infof("register slave for master server %s:%d", b.cfg.Host, b.cfg.Port)
var err error
b.c, err = client.Connect(fmt.Sprintf("%s:%d", b.cfg.Host, b.cfg.Port), b.cfg.User, b.cfg.Password, "", func(c *client.Conn) {
c.TLSConfig = b.cfg.TLSConfig
})
if err != nil {
return errors.Trace(err)
}
if len(b.cfg.Charset) != 0 {
b.c.SetCharset(b.cfg.Charset)
}
//set read timeout
if b.cfg.ReadTimeout > 0 {
b.c.SetReadDeadline(time.Now().Add(b.cfg.ReadTimeout))
}
if b.cfg.RecvBufferSize > 0 {
if tcp, ok := b.c.Conn.Conn.(*net.TCPConn); ok {
tcp.SetReadBuffer(b.cfg.RecvBufferSize)
}
}
// kill last connection id
if b.lastConnectionID > 0 {
cmd := fmt.Sprintf("KILL %d", b.lastConnectionID)
if _, err := b.c.Execute(cmd); err != nil {
log.Errorf("kill connection %d error %v", b.lastConnectionID, err)
// Unknown thread id
if code := ErrorCode(err.Error()); code != ER_NO_SUCH_THREAD {
return errors.Trace(err)
}
}
log.Infof("kill last connection id %d", b.lastConnectionID)
}
// save last last connection id for kill
b.lastConnectionID = b.c.GetConnectionID()
//for mysql 5.6+, binlog has a crc32 checksum
//before mysql 5.6, this will not work, don't matter.:-)
if r, err := b.c.Execute("SHOW GLOBAL VARIABLES LIKE 'BINLOG_CHECKSUM'"); err != nil {
return errors.Trace(err)
} else {
s, _ := r.GetString(0, 1)
if s != "" {
// maybe CRC32 or NONE
// mysqlbinlog.cc use NONE, see its below comments:
// Make a notice to the server that this client
// is checksum-aware. It does not need the first fake Rotate
// necessary checksummed.
// That preference is specified below.
if _, err = b.c.Execute(`SET @master_binlog_checksum='NONE'`); err != nil {
return errors.Trace(err)
}
// if _, err = b.c.Execute(`SET @master_binlog_checksum=@@global.binlog_checksum`); err != nil {
// return errors.Trace(err)
// }
}
}
if b.cfg.Flavor == MariaDBFlavor {
// Refer https://github.com/alibaba/canal/wiki/BinlogChange(MariaDB5&10)
// Tell the server that we understand GTIDs by setting our slave capability
// to MARIA_SLAVE_CAPABILITY_GTID = 4 (MariaDB >= 10.0.1).
if _, err := b.c.Execute("SET @mariadb_slave_capability=4"); err != nil {
return errors.Errorf("failed to set @mariadb_slave_capability=4: %v", err)
}
}
if b.cfg.HeartbeatPeriod > 0 {
_, err = b.c.Execute(fmt.Sprintf("SET @master_heartbeat_period=%d;", b.cfg.HeartbeatPeriod))
if err != nil {
log.Error("failed to set @master_heartbeat_period=%d", b.cfg.HeartbeatPeriod, err)
return errors.Trace(err)
}
}
if err = b.writeRegisterSlaveCommand(); err != nil {
return errors.Trace(err)
}
if _, err = b.c.ReadOKPacket(); err != nil {
return errors.Trace(err)
}
return nil
}
func (b *BinlogSyncer) enableSemiSync() error {
if !b.cfg.SemiSyncEnabled {
return nil
}
if r, err := b.c.Execute("SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled';"); err != nil {
return errors.Trace(err)
} else {
s, _ := r.GetString(0, 1)
if s != "ON" {
log.Errorf("master does not support semi synchronous replication, use no semi-sync")
b.cfg.SemiSyncEnabled = false
return nil
}
}
_, err := b.c.Execute(`SET @rpl_semi_sync_slave = 1;`)
if err != nil {
return errors.Trace(err)
}
return nil
}
func (b *BinlogSyncer) prepare() error {
if b.isClosed() {
return errors.Trace(ErrSyncClosed)
}
if err := b.registerSlave(); err != nil {
return errors.Trace(err)
}
if err := b.enableSemiSync(); err != nil {
return errors.Trace(err)
}
return nil
}
func (b *BinlogSyncer) startDumpStream() *BinlogStreamer {
b.running = true
s := newBinlogStreamer()
b.wg.Add(1)
go b.onStream(s)
return s
}
// GetNextPosition returns the next position of the syncer
func (b *BinlogSyncer) GetNextPosition() Position {
return b.nextPos
}
// StartSync starts syncing from the `pos` position.
func (b *BinlogSyncer) StartSync(pos Position) (*BinlogStreamer, error) {
log.Infof("begin to sync binlog from position %s", pos)
b.m.Lock()
defer b.m.Unlock()
if b.running {
return nil, errors.Trace(errSyncRunning)
}
if err := b.prepareSyncPos(pos); err != nil {
return nil, errors.Trace(err)
}
return b.startDumpStream(), nil
}
// StartSyncGTID starts syncing from the `gset` GTIDSet.
func (b *BinlogSyncer) StartSyncGTID(gset GTIDSet) (*BinlogStreamer, error) {
log.Infof("begin to sync binlog from GTID %s", gset)
b.useGTID = true
b.gset = gset
b.m.Lock()
defer b.m.Unlock()
if b.running {
return nil, errors.Trace(errSyncRunning)
}
if err := b.prepare(); err != nil {
return nil, errors.Trace(err)
}
var err error
if b.cfg.Flavor != MariaDBFlavor {
// default use MySQL
err = b.writeBinlogDumpMysqlGTIDCommand(gset)
} else {
err = b.writeBinlogDumpMariadbGTIDCommand(gset)
}
if err != nil {
return nil, err
}
return b.startDumpStream(), nil
}
func (b *BinlogSyncer) writeBinlogDumpCommand(p Position) error {
b.c.ResetSequence()
data := make([]byte, 4+1+4+2+4+len(p.Name))
pos := 4
data[pos] = COM_BINLOG_DUMP
pos++
binary.LittleEndian.PutUint32(data[pos:], p.Pos)
pos += 4
binary.LittleEndian.PutUint16(data[pos:], BINLOG_DUMP_NEVER_STOP)
pos += 2
binary.LittleEndian.PutUint32(data[pos:], b.cfg.ServerID)
pos += 4
copy(data[pos:], p.Name)
return b.c.WritePacket(data)
}
func (b *BinlogSyncer) writeBinlogDumpMysqlGTIDCommand(gset GTIDSet) error {
p := Position{"", 4}
gtidData := gset.Encode()
b.c.ResetSequence()
data := make([]byte, 4+1+2+4+4+len(p.Name)+8+4+len(gtidData))
pos := 4
data[pos] = COM_BINLOG_DUMP_GTID
pos++
binary.LittleEndian.PutUint16(data[pos:], 0)
pos += 2
binary.LittleEndian.PutUint32(data[pos:], b.cfg.ServerID)
pos += 4
binary.LittleEndian.PutUint32(data[pos:], uint32(len(p.Name)))
pos += 4
n := copy(data[pos:], p.Name)
pos += n
binary.LittleEndian.PutUint64(data[pos:], uint64(p.Pos))
pos += 8
binary.LittleEndian.PutUint32(data[pos:], uint32(len(gtidData)))
pos += 4
n = copy(data[pos:], gtidData)
pos += n
data = data[0:pos]
return b.c.WritePacket(data)
}
func (b *BinlogSyncer) writeBinlogDumpMariadbGTIDCommand(gset GTIDSet) error {
// Copy from vitess
startPos := gset.String()
// Set the slave_connect_state variable before issuing COM_BINLOG_DUMP to
// provide the start position in GTID form.
query := fmt.Sprintf("SET @slave_connect_state='%s'", startPos)
if _, err := b.c.Execute(query); err != nil {
return errors.Errorf("failed to set @slave_connect_state='%s': %v", startPos, err)
}
// Real slaves set this upon connecting if their gtid_strict_mode option was
// enabled. We always use gtid_strict_mode because we need it to make our
// internal GTID comparisons safe.
if _, err := b.c.Execute("SET @slave_gtid_strict_mode=1"); err != nil {
return errors.Errorf("failed to set @slave_gtid_strict_mode=1: %v", err)
}
// Since we use @slave_connect_state, the file and position here are ignored.
return b.writeBinlogDumpCommand(Position{"", 0})
}
// localHostname returns the hostname that register slave would register as.
func (b *BinlogSyncer) localHostname() string {
if len(b.cfg.Localhost) == 0 {
h, _ := os.Hostname()
return h
}
return b.cfg.Localhost
}
func (b *BinlogSyncer) writeRegisterSlaveCommand() error {
b.c.ResetSequence()
hostname := b.localHostname()
// This should be the name of slave host not the host we are connecting to.
data := make([]byte, 4+1+4+1+len(hostname)+1+len(b.cfg.User)+1+len(b.cfg.Password)+2+4+4)
pos := 4
data[pos] = COM_REGISTER_SLAVE
pos++
binary.LittleEndian.PutUint32(data[pos:], b.cfg.ServerID)
pos += 4
// This should be the name of slave hostname not the host we are connecting to.
data[pos] = uint8(len(hostname))
pos++
n := copy(data[pos:], hostname)
pos += n
data[pos] = uint8(len(b.cfg.User))
pos++
n = copy(data[pos:], b.cfg.User)
pos += n
data[pos] = uint8(len(b.cfg.Password))
pos++
n = copy(data[pos:], b.cfg.Password)
pos += n
binary.LittleEndian.PutUint16(data[pos:], b.cfg.Port)
pos += 2
//replication rank, not used
binary.LittleEndian.PutUint32(data[pos:], 0)
pos += 4
// master ID, 0 is OK
binary.LittleEndian.PutUint32(data[pos:], 0)
return b.c.WritePacket(data)
}
func (b *BinlogSyncer) replySemiSyncACK(p Position) error {
b.c.ResetSequence()
data := make([]byte, 4+1+8+len(p.Name))
pos := 4
// semi sync indicator
data[pos] = SemiSyncIndicator
pos++
binary.LittleEndian.PutUint64(data[pos:], uint64(p.Pos))
pos += 8
copy(data[pos:], p.Name)
err := b.c.WritePacket(data)
if err != nil {
return errors.Trace(err)
}
return nil
}
func (b *BinlogSyncer) retrySync() error {
b.m.Lock()
defer b.m.Unlock()
b.parser.Reset()
if b.useGTID {
log.Infof("begin to re-sync from %s", b.gset.String())
if err := b.prepareSyncGTID(b.gset); err != nil {
return errors.Trace(err)
}
} else {
log.Infof("begin to re-sync from %s", b.nextPos)
if err := b.prepareSyncPos(b.nextPos); err != nil {
return errors.Trace(err)
}
}
return nil
}
func (b *BinlogSyncer) prepareSyncPos(pos Position) error {
// always start from position 4
if pos.Pos < 4 {
pos.Pos = 4
}
if err := b.prepare(); err != nil {
return errors.Trace(err)
}
if err := b.writeBinlogDumpCommand(pos); err != nil {
return errors.Trace(err)
}
return nil
}
func (b *BinlogSyncer) prepareSyncGTID(gset GTIDSet) error {
var err error
if err = b.prepare(); err != nil {
return errors.Trace(err)
}
if b.cfg.Flavor != MariaDBFlavor {
// default use MySQL
err = b.writeBinlogDumpMysqlGTIDCommand(gset)
} else {
err = b.writeBinlogDumpMariadbGTIDCommand(gset)
}
if err != nil {
return err
}
return nil
}
func (b *BinlogSyncer) onStream(s *BinlogStreamer) {
defer func() {
if e := recover(); e != nil {
s.closeWithError(fmt.Errorf("Err: %v\n Stack: %s", e, Pstack()))
}
b.wg.Done()
}()
for {
data, err := b.c.ReadPacket()
if err != nil {
log.Error(err)
// we meet connection error, should re-connect again with
// last nextPos or nextGTID we got.
if len(b.nextPos.Name) == 0 && b.gset == nil {
// we can't get the correct position, close.
s.closeWithError(err)
return
}
// TODO: add a max retry count.
for {
select {
case <-b.ctx.Done():
s.close()
return
case <-time.After(time.Second):
if err = b.retrySync(); err != nil {
log.Errorf("retry sync err: %v, wait 1s and retry again", err)
continue
}
}
break
}
// we connect the server and begin to re-sync again.
continue
}
//set read timeout
if b.cfg.ReadTimeout > 0 {
b.c.SetReadDeadline(time.Now().Add(b.cfg.ReadTimeout))
}
switch data[0] {
case OK_HEADER:
if err = b.parseEvent(s, data); err != nil {
s.closeWithError(err)
return
}
case ERR_HEADER:
err = b.c.HandleErrorPacket(data)
s.closeWithError(err)
return
case EOF_HEADER:
// Refer http://dev.mysql.com/doc/internals/en/packet-EOF_Packet.html
// In the MySQL client/server protocol, EOF and OK packets serve the same purpose.
// Some users told me that they received EOF packet here, but I don't know why.
// So we only log a message and retry ReadPacket.
log.Info("receive EOF packet, retry ReadPacket")
continue
default:
log.Errorf("invalid stream header %c", data[0])
continue
}
}
}
func (b *BinlogSyncer) parseEvent(s *BinlogStreamer, data []byte) error {
//skip OK byte, 0x00
data = data[1:]
needACK := false
if b.cfg.SemiSyncEnabled && (data[0] == SemiSyncIndicator) {
needACK = (data[1] == 0x01)
//skip semi sync header
data = data[2:]
}
e, err := b.parser.Parse(data)
if err != nil {
return errors.Trace(err)
}
if e.Header.LogPos > 0 {
// Some events like FormatDescriptionEvent return 0, ignore.
b.nextPos.Pos = e.Header.LogPos
}
switch event := e.Event.(type) {
case *RotateEvent:
b.nextPos.Name = string(event.NextLogName)
b.nextPos.Pos = uint32(event.Position)
log.Infof("rotate to %s", b.nextPos)
case *GTIDEvent:
if !b.useGTID {
break
}
u, _ := uuid.FromBytes(event.SID)
err := b.gset.Update(fmt.Sprintf("%s:%d", u.String(), event.GNO))
if err != nil {
return errors.Trace(err)
}
case *MariadbGTIDEvent:
if !b.useGTID {
break
}
GTID := event.GTID
err := b.gset.Update(fmt.Sprintf("%d-%d-%d", GTID.DomainID, GTID.ServerID, GTID.SequenceNumber))
if err != nil {
return errors.Trace(err)
}
case *XIDEvent:
event.GSet = b.getGtidSet()
case *QueryEvent:
event.GSet = b.getGtidSet()
}
needStop := false
select {
case s.ch <- e:
case <-b.ctx.Done():
needStop = true
}
if needACK {
err := b.replySemiSyncACK(b.nextPos)
if err != nil {
return errors.Trace(err)
}
}
if needStop {
return errors.New("sync is been closing...")
}
return nil
}
func (b *BinlogSyncer) getGtidSet() GTIDSet {
var gtidSet GTIDSet
if !b.useGTID {
return nil
}
if b.cfg.Flavor != MariaDBFlavor {
gtidSet, _ = ParseGTIDSet(MySQLFlavor, b.gset.String())
} else {
gtidSet, _ = ParseGTIDSet(MariaDBFlavor, b.gset.String())
}
return gtidSet
}
// LastConnectionID returns last connectionID.
func (b *BinlogSyncer) LastConnectionID() uint32 {
return b.lastConnectionID
}

View File

@@ -0,0 +1,185 @@
package replication
const (
//we only support MySQL 5.0.0+ binlog format, maybe???
MinBinlogVersion = 4
)
var (
//binlog header [ fe `bin` ]
BinLogFileHeader []byte = []byte{0xfe, 0x62, 0x69, 0x6e}
SemiSyncIndicator byte = 0xef
)
const (
LOG_EVENT_BINLOG_IN_USE_F uint16 = 0x0001
LOG_EVENT_FORCED_ROTATE_F uint16 = 0x0002
LOG_EVENT_THREAD_SPECIFIC_F uint16 = 0x0004
LOG_EVENT_SUPPRESS_USE_F uint16 = 0x0008
LOG_EVENT_UPDATE_TABLE_MAP_VERSION_F uint16 = 0x0010
LOG_EVENT_ARTIFICIAL_F uint16 = 0x0020
LOG_EVENT_RELAY_LOG_F uint16 = 0x0040
LOG_EVENT_IGNORABLE_F uint16 = 0x0080
LOG_EVENT_NO_FILTER_F uint16 = 0x0100
LOG_EVENT_MTS_ISOLATE_F uint16 = 0x0200
)
const (
BINLOG_DUMP_NEVER_STOP uint16 = 0x00
BINLOG_DUMP_NON_BLOCK uint16 = 0x01
BINLOG_THROUGH_POSITION uint16 = 0x02
BINLOG_THROUGH_GTID uint16 = 0x04
)
const (
BINLOG_ROW_IMAGE_FULL = "FULL"
BINLOG_ROW_IAMGE_MINIMAL = "MINIMAL"
BINLOG_ROW_IMAGE_NOBLOB = "NOBLOB"
)
type EventType byte
const (
UNKNOWN_EVENT EventType = iota
START_EVENT_V3
QUERY_EVENT
STOP_EVENT
ROTATE_EVENT
INTVAR_EVENT
LOAD_EVENT
SLAVE_EVENT
CREATE_FILE_EVENT
APPEND_BLOCK_EVENT
EXEC_LOAD_EVENT
DELETE_FILE_EVENT
NEW_LOAD_EVENT
RAND_EVENT
USER_VAR_EVENT
FORMAT_DESCRIPTION_EVENT
XID_EVENT
BEGIN_LOAD_QUERY_EVENT
EXECUTE_LOAD_QUERY_EVENT
TABLE_MAP_EVENT
WRITE_ROWS_EVENTv0
UPDATE_ROWS_EVENTv0
DELETE_ROWS_EVENTv0
WRITE_ROWS_EVENTv1
UPDATE_ROWS_EVENTv1
DELETE_ROWS_EVENTv1
INCIDENT_EVENT
HEARTBEAT_EVENT
IGNORABLE_EVENT
ROWS_QUERY_EVENT
WRITE_ROWS_EVENTv2
UPDATE_ROWS_EVENTv2
DELETE_ROWS_EVENTv2
GTID_EVENT
ANONYMOUS_GTID_EVENT
PREVIOUS_GTIDS_EVENT
)
const (
// MariaDB event starts from 160
MARIADB_ANNOTATE_ROWS_EVENT EventType = 160 + iota
MARIADB_BINLOG_CHECKPOINT_EVENT
MARIADB_GTID_EVENT
MARIADB_GTID_LIST_EVENT
)
func (e EventType) String() string {
switch e {
case UNKNOWN_EVENT:
return "UnknownEvent"
case START_EVENT_V3:
return "StartEventV3"
case QUERY_EVENT:
return "QueryEvent"
case STOP_EVENT:
return "StopEvent"
case ROTATE_EVENT:
return "RotateEvent"
case INTVAR_EVENT:
return "IntVarEvent"
case LOAD_EVENT:
return "LoadEvent"
case SLAVE_EVENT:
return "SlaveEvent"
case CREATE_FILE_EVENT:
return "CreateFileEvent"
case APPEND_BLOCK_EVENT:
return "AppendBlockEvent"
case EXEC_LOAD_EVENT:
return "ExecLoadEvent"
case DELETE_FILE_EVENT:
return "DeleteFileEvent"
case NEW_LOAD_EVENT:
return "NewLoadEvent"
case RAND_EVENT:
return "RandEvent"
case USER_VAR_EVENT:
return "UserVarEvent"
case FORMAT_DESCRIPTION_EVENT:
return "FormatDescriptionEvent"
case XID_EVENT:
return "XIDEvent"
case BEGIN_LOAD_QUERY_EVENT:
return "BeginLoadQueryEvent"
case EXECUTE_LOAD_QUERY_EVENT:
return "ExectueLoadQueryEvent"
case TABLE_MAP_EVENT:
return "TableMapEvent"
case WRITE_ROWS_EVENTv0:
return "WriteRowsEventV0"
case UPDATE_ROWS_EVENTv0:
return "UpdateRowsEventV0"
case DELETE_ROWS_EVENTv0:
return "DeleteRowsEventV0"
case WRITE_ROWS_EVENTv1:
return "WriteRowsEventV1"
case UPDATE_ROWS_EVENTv1:
return "UpdateRowsEventV1"
case DELETE_ROWS_EVENTv1:
return "DeleteRowsEventV1"
case INCIDENT_EVENT:
return "IncidentEvent"
case HEARTBEAT_EVENT:
return "HeartbeatEvent"
case IGNORABLE_EVENT:
return "IgnorableEvent"
case ROWS_QUERY_EVENT:
return "RowsQueryEvent"
case WRITE_ROWS_EVENTv2:
return "WriteRowsEventV2"
case UPDATE_ROWS_EVENTv2:
return "UpdateRowsEventV2"
case DELETE_ROWS_EVENTv2:
return "DeleteRowsEventV2"
case GTID_EVENT:
return "GTIDEvent"
case ANONYMOUS_GTID_EVENT:
return "AnonymousGTIDEvent"
case PREVIOUS_GTIDS_EVENT:
return "PreviousGTIDsEvent"
case MARIADB_ANNOTATE_ROWS_EVENT:
return "MariadbAnnotateRowsEvent"
case MARIADB_BINLOG_CHECKPOINT_EVENT:
return "MariadbBinLogCheckPointEvent"
case MARIADB_GTID_EVENT:
return "MariadbGTIDEvent"
case MARIADB_GTID_LIST_EVENT:
return "MariadbGTIDListEvent"
default:
return "UnknownEvent"
}
}
const (
BINLOG_CHECKSUM_ALG_OFF byte = 0 // Events are without checksum though its generator
// is checksum-capable New Master (NM).
BINLOG_CHECKSUM_ALG_CRC32 byte = 1 // CRC32 of zlib algorithm.
// BINLOG_CHECKSUM_ALG_ENUM_END, // the cut line: valid alg range is [1, 0x7f].
BINLOG_CHECKSUM_ALG_UNDEF byte = 255 // special value to tag undetermined yet checksum
// or events from checksum-unaware servers
)

View File

@@ -0,0 +1,8 @@
/*
Replication package is to handle MySQL replication protocol.
Todo:
+ Get table information when handing rows event.
*/
package replication

View File

@@ -0,0 +1,487 @@
package replication
import (
"encoding/binary"
//"encoding/hex"
"fmt"
"io"
"strconv"
"strings"
"time"
"unicode"
"github.com/juju/errors"
"github.com/satori/go.uuid"
. "github.com/siddontang/go-mysql/mysql"
)
const (
EventHeaderSize = 19
SidLength = 16
LogicalTimestampTypeCode = 2
PartLogicalTimestampLength = 8
)
type BinlogEvent struct {
// raw binlog data, including crc32 checksum if exists
RawData []byte
Header *EventHeader
Event Event
}
func (e *BinlogEvent) Dump(w io.Writer) {
e.Header.Dump(w)
e.Event.Dump(w)
}
type Event interface {
//Dump Event, format like python-mysql-replication
Dump(w io.Writer)
Decode(data []byte) error
}
type EventError struct {
Header *EventHeader
//Error message
Err string
//Event data
Data []byte
}
func (e *EventError) Error() string {
return e.Err
}
type EventHeader struct {
Timestamp uint32
EventType EventType
ServerID uint32
EventSize uint32
LogPos uint32
Flags uint16
}
func (h *EventHeader) Decode(data []byte) error {
if len(data) < EventHeaderSize {
return errors.Errorf("header size too short %d, must 19", len(data))
}
pos := 0
h.Timestamp = binary.LittleEndian.Uint32(data[pos:])
pos += 4
h.EventType = EventType(data[pos])
pos++
h.ServerID = binary.LittleEndian.Uint32(data[pos:])
pos += 4
h.EventSize = binary.LittleEndian.Uint32(data[pos:])
pos += 4
h.LogPos = binary.LittleEndian.Uint32(data[pos:])
pos += 4
h.Flags = binary.LittleEndian.Uint16(data[pos:])
pos += 2
if h.EventSize < uint32(EventHeaderSize) {
return errors.Errorf("invalid event size %d, must >= 19", h.EventSize)
}
return nil
}
func (h *EventHeader) Dump(w io.Writer) {
fmt.Fprintf(w, "=== %s ===\n", EventType(h.EventType))
fmt.Fprintf(w, "Date: %s\n", time.Unix(int64(h.Timestamp), 0).Format(TimeFormat))
fmt.Fprintf(w, "Log position: %d\n", h.LogPos)
fmt.Fprintf(w, "Event size: %d\n", h.EventSize)
}
var (
checksumVersionSplitMysql []int = []int{5, 6, 1}
checksumVersionProductMysql int = (checksumVersionSplitMysql[0]*256+checksumVersionSplitMysql[1])*256 + checksumVersionSplitMysql[2]
checksumVersionSplitMariaDB []int = []int{5, 3, 0}
checksumVersionProductMariaDB int = (checksumVersionSplitMariaDB[0]*256+checksumVersionSplitMariaDB[1])*256 + checksumVersionSplitMariaDB[2]
)
// server version format X.Y.Zabc, a is not . or number
func splitServerVersion(server string) []int {
seps := strings.Split(server, ".")
if len(seps) < 3 {
return []int{0, 0, 0}
}
x, _ := strconv.Atoi(seps[0])
y, _ := strconv.Atoi(seps[1])
index := 0
for i, c := range seps[2] {
if !unicode.IsNumber(c) {
index = i
break
}
}
z, _ := strconv.Atoi(seps[2][0:index])
return []int{x, y, z}
}
func calcVersionProduct(server string) int {
versionSplit := splitServerVersion(server)
return ((versionSplit[0]*256+versionSplit[1])*256 + versionSplit[2])
}
type FormatDescriptionEvent struct {
Version uint16
//len = 50
ServerVersion []byte
CreateTimestamp uint32
EventHeaderLength uint8
EventTypeHeaderLengths []byte
// 0 is off, 1 is for CRC32, 255 is undefined
ChecksumAlgorithm byte
}
func (e *FormatDescriptionEvent) Decode(data []byte) error {
pos := 0
e.Version = binary.LittleEndian.Uint16(data[pos:])
pos += 2
e.ServerVersion = make([]byte, 50)
copy(e.ServerVersion, data[pos:])
pos += 50
e.CreateTimestamp = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.EventHeaderLength = data[pos]
pos++
if e.EventHeaderLength != byte(EventHeaderSize) {
return errors.Errorf("invalid event header length %d, must 19", e.EventHeaderLength)
}
server := string(e.ServerVersion)
checksumProduct := checksumVersionProductMysql
if strings.Contains(strings.ToLower(server), "mariadb") {
checksumProduct = checksumVersionProductMariaDB
}
if calcVersionProduct(string(e.ServerVersion)) >= checksumProduct {
// here, the last 5 bytes is 1 byte check sum alg type and 4 byte checksum if exists
e.ChecksumAlgorithm = data[len(data)-5]
e.EventTypeHeaderLengths = data[pos : len(data)-5]
} else {
e.ChecksumAlgorithm = BINLOG_CHECKSUM_ALG_UNDEF
e.EventTypeHeaderLengths = data[pos:]
}
return nil
}
func (e *FormatDescriptionEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Version: %d\n", e.Version)
fmt.Fprintf(w, "Server version: %s\n", e.ServerVersion)
//fmt.Fprintf(w, "Create date: %s\n", time.Unix(int64(e.CreateTimestamp), 0).Format(TimeFormat))
fmt.Fprintf(w, "Checksum algorithm: %d\n", e.ChecksumAlgorithm)
//fmt.Fprintf(w, "Event header lengths: \n%s", hex.Dump(e.EventTypeHeaderLengths))
fmt.Fprintln(w)
}
type RotateEvent struct {
Position uint64
NextLogName []byte
}
func (e *RotateEvent) Decode(data []byte) error {
e.Position = binary.LittleEndian.Uint64(data[0:])
e.NextLogName = data[8:]
return nil
}
func (e *RotateEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Position: %d\n", e.Position)
fmt.Fprintf(w, "Next log name: %s\n", e.NextLogName)
fmt.Fprintln(w)
}
type XIDEvent struct {
XID uint64
// in fact XIDEvent dosen't have the GTIDSet information, just for beneficial to use
GSet GTIDSet
}
func (e *XIDEvent) Decode(data []byte) error {
e.XID = binary.LittleEndian.Uint64(data)
return nil
}
func (e *XIDEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "XID: %d\n", e.XID)
if e.GSet != nil {
fmt.Fprintf(w, "GTIDSet: %s\n", e.GSet.String())
}
fmt.Fprintln(w)
}
type QueryEvent struct {
SlaveProxyID uint32
ExecutionTime uint32
ErrorCode uint16
StatusVars []byte
Schema []byte
Query []byte
// in fact QueryEvent dosen't have the GTIDSet information, just for beneficial to use
GSet GTIDSet
}
func (e *QueryEvent) Decode(data []byte) error {
pos := 0
e.SlaveProxyID = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.ExecutionTime = binary.LittleEndian.Uint32(data[pos:])
pos += 4
schemaLength := uint8(data[pos])
pos++
e.ErrorCode = binary.LittleEndian.Uint16(data[pos:])
pos += 2
statusVarsLength := binary.LittleEndian.Uint16(data[pos:])
pos += 2
e.StatusVars = data[pos : pos+int(statusVarsLength)]
pos += int(statusVarsLength)
e.Schema = data[pos : pos+int(schemaLength)]
pos += int(schemaLength)
//skip 0x00
pos++
e.Query = data[pos:]
return nil
}
func (e *QueryEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Slave proxy ID: %d\n", e.SlaveProxyID)
fmt.Fprintf(w, "Execution time: %d\n", e.ExecutionTime)
fmt.Fprintf(w, "Error code: %d\n", e.ErrorCode)
//fmt.Fprintf(w, "Status vars: \n%s", hex.Dump(e.StatusVars))
fmt.Fprintf(w, "Schema: %s\n", e.Schema)
fmt.Fprintf(w, "Query: %s\n", e.Query)
if e.GSet != nil {
fmt.Fprintf(w, "GTIDSet: %s\n", e.GSet.String())
}
fmt.Fprintln(w)
}
type GTIDEvent struct {
CommitFlag uint8
SID []byte
GNO int64
LastCommitted int64
SequenceNumber int64
}
func (e *GTIDEvent) Decode(data []byte) error {
pos := 0
e.CommitFlag = uint8(data[pos])
pos++
e.SID = data[pos : pos+SidLength]
pos += SidLength
e.GNO = int64(binary.LittleEndian.Uint64(data[pos:]))
pos += 8
if len(data) >= 42 {
if uint8(data[pos]) == LogicalTimestampTypeCode {
pos++
e.LastCommitted = int64(binary.LittleEndian.Uint64(data[pos:]))
pos += PartLogicalTimestampLength
e.SequenceNumber = int64(binary.LittleEndian.Uint64(data[pos:]))
}
}
return nil
}
func (e *GTIDEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Commit flag: %d\n", e.CommitFlag)
u, _ := uuid.FromBytes(e.SID)
fmt.Fprintf(w, "GTID_NEXT: %s:%d\n", u.String(), e.GNO)
fmt.Fprintf(w, "LAST_COMMITTED: %d\n", e.LastCommitted)
fmt.Fprintf(w, "SEQUENCE_NUMBER: %d\n", e.SequenceNumber)
fmt.Fprintln(w)
}
type BeginLoadQueryEvent struct {
FileID uint32
BlockData []byte
}
func (e *BeginLoadQueryEvent) Decode(data []byte) error {
pos := 0
e.FileID = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.BlockData = data[pos:]
return nil
}
func (e *BeginLoadQueryEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "File ID: %d\n", e.FileID)
fmt.Fprintf(w, "Block data: %s\n", e.BlockData)
fmt.Fprintln(w)
}
type ExecuteLoadQueryEvent struct {
SlaveProxyID uint32
ExecutionTime uint32
SchemaLength uint8
ErrorCode uint16
StatusVars uint16
FileID uint32
StartPos uint32
EndPos uint32
DupHandlingFlags uint8
}
func (e *ExecuteLoadQueryEvent) Decode(data []byte) error {
pos := 0
e.SlaveProxyID = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.ExecutionTime = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.SchemaLength = uint8(data[pos])
pos++
e.ErrorCode = binary.LittleEndian.Uint16(data[pos:])
pos += 2
e.StatusVars = binary.LittleEndian.Uint16(data[pos:])
pos += 2
e.FileID = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.StartPos = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.EndPos = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.DupHandlingFlags = uint8(data[pos])
return nil
}
func (e *ExecuteLoadQueryEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Slave proxy ID: %d\n", e.SlaveProxyID)
fmt.Fprintf(w, "Execution time: %d\n", e.ExecutionTime)
fmt.Fprintf(w, "Schame length: %d\n", e.SchemaLength)
fmt.Fprintf(w, "Error code: %d\n", e.ErrorCode)
fmt.Fprintf(w, "Status vars length: %d\n", e.StatusVars)
fmt.Fprintf(w, "File ID: %d\n", e.FileID)
fmt.Fprintf(w, "Start pos: %d\n", e.StartPos)
fmt.Fprintf(w, "End pos: %d\n", e.EndPos)
fmt.Fprintf(w, "Dup handling flags: %d\n", e.DupHandlingFlags)
fmt.Fprintln(w)
}
// case MARIADB_ANNOTATE_ROWS_EVENT:
// return "MariadbAnnotateRowsEvent"
type MariadbAnnotateRowsEvent struct {
Query []byte
}
func (e *MariadbAnnotateRowsEvent) Decode(data []byte) error {
e.Query = data
return nil
}
func (e *MariadbAnnotateRowsEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Query: %s\n", e.Query)
fmt.Fprintln(w)
}
type MariadbBinlogCheckPointEvent struct {
Info []byte
}
func (e *MariadbBinlogCheckPointEvent) Decode(data []byte) error {
e.Info = data
return nil
}
func (e *MariadbBinlogCheckPointEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Info: %s\n", e.Info)
fmt.Fprintln(w)
}
type MariadbGTIDEvent struct {
GTID MariadbGTID
}
func (e *MariadbGTIDEvent) Decode(data []byte) error {
e.GTID.SequenceNumber = binary.LittleEndian.Uint64(data)
e.GTID.DomainID = binary.LittleEndian.Uint32(data[8:])
// we don't care commit id now, maybe later
return nil
}
func (e *MariadbGTIDEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "GTID: %s\n", e.GTID)
fmt.Fprintln(w)
}
type MariadbGTIDListEvent struct {
GTIDs []MariadbGTID
}
func (e *MariadbGTIDListEvent) Decode(data []byte) error {
pos := 0
v := binary.LittleEndian.Uint32(data[pos:])
pos += 4
count := v & uint32((1<<28)-1)
e.GTIDs = make([]MariadbGTID, count)
for i := uint32(0); i < count; i++ {
e.GTIDs[i].DomainID = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.GTIDs[i].ServerID = binary.LittleEndian.Uint32(data[pos:])
pos += 4
e.GTIDs[i].SequenceNumber = binary.LittleEndian.Uint64(data[pos:])
}
return nil
}
func (e *MariadbGTIDListEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Lists: %v\n", e.GTIDs)
fmt.Fprintln(w)
}

View File

@@ -0,0 +1,171 @@
package replication
import (
"encoding/hex"
"fmt"
"io"
)
// we don't parse all event, so some we will use GenericEvent instead
type GenericEvent struct {
Data []byte
}
func (e *GenericEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Event data: \n%s", hex.Dump(e.Data))
fmt.Fprintln(w)
}
func (e *GenericEvent) Decode(data []byte) error {
e.Data = data
return nil
}
//below events are generic events, maybe later I will consider handle some.
// type StartEventV3 struct {
// Version uint16
// ServerVersion [50]byte
// CreateTimestamp uint32
// }
// type StopEvent struct{}
// type LoadEvent struct {
// SlaveProxyID uint32
// ExecTime uint32
// SkipLines uint32
// TableNameLen uint8
// SchemaLen uint8
// NumFileds uint32
// FieldTerm uint8
// EnclosedBy uint8
// LineTerm uint8
// LineStart uint8
// EscapedBy uint8
// OptFlags uint8
// EmptyFlags uint8
// //len = 1 * NumFields
// FieldNameLengths []byte
// //len = sum(FieldNameLengths) + NumFields
// //array of nul-terminated strings
// FieldNames []byte
// //len = TableNameLen + 1, nul-terminated string
// TableName []byte
// //len = SchemaLen + 1, nul-terminated string
// SchemaName []byte
// //string.NUL
// FileName []byte
// }
// type NewLoadEvent struct {
// SlaveProxyID uint32
// ExecTime uint32
// SkipLines uint32
// TableNameLen uint8
// SchemaLen uint8
// NumFields uint32
// FieldTermLen uint8
// FieldTerm []byte
// EnclosedByLen uint8
// EnclosedBy []byte
// LineTermLen uint8
// LineTerm []byte
// LineStartLen uint8
// LineStart []byte
// EscapedByLen uint8
// EscapedBy []byte
// OptFlags uint8
// //len = 1 * NumFields
// FieldNameLengths []byte
// //len = sum(FieldNameLengths) + NumFields
// //array of nul-terminated strings
// FieldNames []byte
// //len = TableNameLen, nul-terminated string
// TableName []byte
// //len = SchemaLen, nul-terminated string
// SchemaName []byte
// //string.EOF
// FileName []byte
// }
// type CreateFileEvent struct {
// FileID uint32
// BlockData []byte
// }
// type AppendBlockEvent struct {
// FileID uint32
// BlockData []byte
// }
// type ExecLoadEvent struct {
// FileID uint32
// }
// type BeginLoadQueryEvent struct {
// FileID uint32
// BlockData []byte
// }
// type ExecuteLoadQueryEvent struct {
// SlaveProxyID uint32
// ExecutionTime uint32
// SchemaLength uint8
// ErrorCode uint16
// StatusVarsLength uint16
// FileID uint32
// StartPos uint32
// EndPos uint32
// DupHandlingFlags uint8
// }
// type DeleteFileEvent struct {
// FileID uint32
// }
// type RandEvent struct {
// Seed1 uint64
// Seed2 uint64
// }
// type IntVarEvent struct {
// Type uint8
// Value uint64
// }
// type UserVarEvent struct {
// NameLength uint32
// Name []byte
// IsNull uint8
// //if not is null
// Type uint8
// Charset uint32
// ValueLength uint32
// Value []byte
// //if more data
// Flags uint8
// }
// type IncidentEvent struct {
// Type uint16
// MessageLength uint8
// Message []byte
// }
// type HeartbeatEvent struct {
// }

View File

@@ -0,0 +1,480 @@
package replication
import (
"encoding/json"
"fmt"
"math"
"github.com/juju/errors"
. "github.com/siddontang/go-mysql/mysql"
"github.com/siddontang/go/hack"
)
const (
JSONB_SMALL_OBJECT byte = iota // small JSON object
JSONB_LARGE_OBJECT // large JSON object
JSONB_SMALL_ARRAY // small JSON array
JSONB_LARGE_ARRAY // large JSON array
JSONB_LITERAL // literal (true/false/null)
JSONB_INT16 // int16
JSONB_UINT16 // uint16
JSONB_INT32 // int32
JSONB_UINT32 // uint32
JSONB_INT64 // int64
JSONB_UINT64 // uint64
JSONB_DOUBLE // double
JSONB_STRING // string
JSONB_OPAQUE byte = 0x0f // custom data (any MySQL data type)
)
const (
JSONB_NULL_LITERAL byte = 0x00
JSONB_TRUE_LITERAL byte = 0x01
JSONB_FALSE_LITERAL byte = 0x02
)
const (
jsonbSmallOffsetSize = 2
jsonbLargeOffsetSize = 4
jsonbKeyEntrySizeSmall = 2 + jsonbSmallOffsetSize
jsonbKeyEntrySizeLarge = 2 + jsonbLargeOffsetSize
jsonbValueEntrySizeSmall = 1 + jsonbSmallOffsetSize
jsonbValueEntrySizeLarge = 1 + jsonbLargeOffsetSize
)
func jsonbGetOffsetSize(isSmall bool) int {
if isSmall {
return jsonbSmallOffsetSize
}
return jsonbLargeOffsetSize
}
func jsonbGetKeyEntrySize(isSmall bool) int {
if isSmall {
return jsonbKeyEntrySizeSmall
}
return jsonbKeyEntrySizeLarge
}
func jsonbGetValueEntrySize(isSmall bool) int {
if isSmall {
return jsonbValueEntrySizeSmall
}
return jsonbValueEntrySizeLarge
}
// decodeJsonBinary decodes the JSON binary encoding data and returns
// the common JSON encoding data.
func (e *RowsEvent) decodeJsonBinary(data []byte) ([]byte, error) {
d := jsonBinaryDecoder{useDecimal: e.useDecimal}
if d.isDataShort(data, 1) {
return nil, d.err
}
v := d.decodeValue(data[0], data[1:])
if d.err != nil {
return nil, d.err
}
return json.Marshal(v)
}
type jsonBinaryDecoder struct {
useDecimal bool
err error
}
func (d *jsonBinaryDecoder) decodeValue(tp byte, data []byte) interface{} {
if d.err != nil {
return nil
}
switch tp {
case JSONB_SMALL_OBJECT:
return d.decodeObjectOrArray(data, true, true)
case JSONB_LARGE_OBJECT:
return d.decodeObjectOrArray(data, false, true)
case JSONB_SMALL_ARRAY:
return d.decodeObjectOrArray(data, true, false)
case JSONB_LARGE_ARRAY:
return d.decodeObjectOrArray(data, false, false)
case JSONB_LITERAL:
return d.decodeLiteral(data)
case JSONB_INT16:
return d.decodeInt16(data)
case JSONB_UINT16:
return d.decodeUint16(data)
case JSONB_INT32:
return d.decodeInt32(data)
case JSONB_UINT32:
return d.decodeUint32(data)
case JSONB_INT64:
return d.decodeInt64(data)
case JSONB_UINT64:
return d.decodeUint64(data)
case JSONB_DOUBLE:
return d.decodeDouble(data)
case JSONB_STRING:
return d.decodeString(data)
case JSONB_OPAQUE:
return d.decodeOpaque(data)
default:
d.err = errors.Errorf("invalid json type %d", tp)
}
return nil
}
func (d *jsonBinaryDecoder) decodeObjectOrArray(data []byte, isSmall bool, isObject bool) interface{} {
offsetSize := jsonbGetOffsetSize(isSmall)
if d.isDataShort(data, 2*offsetSize) {
return nil
}
count := d.decodeCount(data, isSmall)
size := d.decodeCount(data[offsetSize:], isSmall)
if d.isDataShort(data, int(size)) {
return nil
}
keyEntrySize := jsonbGetKeyEntrySize(isSmall)
valueEntrySize := jsonbGetValueEntrySize(isSmall)
headerSize := 2*offsetSize + count*valueEntrySize
if isObject {
headerSize += count * keyEntrySize
}
if headerSize > size {
d.err = errors.Errorf("header size %d > size %d", headerSize, size)
return nil
}
var keys []string
if isObject {
keys = make([]string, count)
for i := 0; i < count; i++ {
// decode key
entryOffset := 2*offsetSize + keyEntrySize*i
keyOffset := d.decodeCount(data[entryOffset:], isSmall)
keyLength := int(d.decodeUint16(data[entryOffset+offsetSize:]))
// Key must start after value entry
if keyOffset < headerSize {
d.err = errors.Errorf("invalid key offset %d, must > %d", keyOffset, headerSize)
return nil
}
if d.isDataShort(data, keyOffset+keyLength) {
return nil
}
keys[i] = hack.String(data[keyOffset : keyOffset+keyLength])
}
}
if d.err != nil {
return nil
}
values := make([]interface{}, count)
for i := 0; i < count; i++ {
// decode value
entryOffset := 2*offsetSize + valueEntrySize*i
if isObject {
entryOffset += keyEntrySize * count
}
tp := data[entryOffset]
if isInlineValue(tp, isSmall) {
values[i] = d.decodeValue(tp, data[entryOffset+1:entryOffset+valueEntrySize])
continue
}
valueOffset := d.decodeCount(data[entryOffset+1:], isSmall)
if d.isDataShort(data, valueOffset) {
return nil
}
values[i] = d.decodeValue(tp, data[valueOffset:])
}
if d.err != nil {
return nil
}
if !isObject {
return values
}
m := make(map[string]interface{}, count)
for i := 0; i < count; i++ {
m[keys[i]] = values[i]
}
return m
}
func isInlineValue(tp byte, isSmall bool) bool {
switch tp {
case JSONB_INT16, JSONB_UINT16, JSONB_LITERAL:
return true
case JSONB_INT32, JSONB_UINT32:
return !isSmall
}
return false
}
func (d *jsonBinaryDecoder) decodeLiteral(data []byte) interface{} {
if d.isDataShort(data, 1) {
return nil
}
tp := data[0]
switch tp {
case JSONB_NULL_LITERAL:
return nil
case JSONB_TRUE_LITERAL:
return true
case JSONB_FALSE_LITERAL:
return false
}
d.err = errors.Errorf("invalid literal %c", tp)
return nil
}
func (d *jsonBinaryDecoder) isDataShort(data []byte, expected int) bool {
if d.err != nil {
return true
}
if len(data) < expected {
d.err = errors.Errorf("data len %d < expected %d", len(data), expected)
}
return d.err != nil
}
func (d *jsonBinaryDecoder) decodeInt16(data []byte) int16 {
if d.isDataShort(data, 2) {
return 0
}
v := ParseBinaryInt16(data[0:2])
return v
}
func (d *jsonBinaryDecoder) decodeUint16(data []byte) uint16 {
if d.isDataShort(data, 2) {
return 0
}
v := ParseBinaryUint16(data[0:2])
return v
}
func (d *jsonBinaryDecoder) decodeInt32(data []byte) int32 {
if d.isDataShort(data, 4) {
return 0
}
v := ParseBinaryInt32(data[0:4])
return v
}
func (d *jsonBinaryDecoder) decodeUint32(data []byte) uint32 {
if d.isDataShort(data, 4) {
return 0
}
v := ParseBinaryUint32(data[0:4])
return v
}
func (d *jsonBinaryDecoder) decodeInt64(data []byte) int64 {
if d.isDataShort(data, 8) {
return 0
}
v := ParseBinaryInt64(data[0:8])
return v
}
func (d *jsonBinaryDecoder) decodeUint64(data []byte) uint64 {
if d.isDataShort(data, 8) {
return 0
}
v := ParseBinaryUint64(data[0:8])
return v
}
func (d *jsonBinaryDecoder) decodeDouble(data []byte) float64 {
if d.isDataShort(data, 8) {
return 0
}
v := ParseBinaryFloat64(data[0:8])
return v
}
func (d *jsonBinaryDecoder) decodeString(data []byte) string {
if d.err != nil {
return ""
}
l, n := d.decodeVariableLength(data)
if d.isDataShort(data, l+n) {
return ""
}
data = data[n:]
v := hack.String(data[0:l])
return v
}
func (d *jsonBinaryDecoder) decodeOpaque(data []byte) interface{} {
if d.isDataShort(data, 1) {
return nil
}
tp := data[0]
data = data[1:]
l, n := d.decodeVariableLength(data)
if d.isDataShort(data, l+n) {
return nil
}
data = data[n : l+n]
switch tp {
case MYSQL_TYPE_NEWDECIMAL:
return d.decodeDecimal(data)
case MYSQL_TYPE_TIME:
return d.decodeTime(data)
case MYSQL_TYPE_DATE, MYSQL_TYPE_DATETIME, MYSQL_TYPE_TIMESTAMP:
return d.decodeDateTime(data)
default:
return hack.String(data)
}
return nil
}
func (d *jsonBinaryDecoder) decodeDecimal(data []byte) interface{} {
precision := int(data[0])
scale := int(data[1])
v, _, err := decodeDecimal(data[2:], precision, scale, d.useDecimal)
d.err = err
return v
}
func (d *jsonBinaryDecoder) decodeTime(data []byte) interface{} {
v := d.decodeInt64(data)
if v == 0 {
return "00:00:00"
}
sign := ""
if v < 0 {
sign = "-"
v = -v
}
intPart := v >> 24
hour := (intPart >> 12) % (1 << 10)
min := (intPart >> 6) % (1 << 6)
sec := intPart % (1 << 6)
frac := v % (1 << 24)
return fmt.Sprintf("%s%02d:%02d:%02d.%06d", sign, hour, min, sec, frac)
}
func (d *jsonBinaryDecoder) decodeDateTime(data []byte) interface{} {
v := d.decodeInt64(data)
if v == 0 {
return "0000-00-00 00:00:00"
}
// handle negative?
if v < 0 {
v = -v
}
intPart := v >> 24
ymd := intPart >> 17
ym := ymd >> 5
hms := intPart % (1 << 17)
year := ym / 13
month := ym % 13
day := ymd % (1 << 5)
hour := (hms >> 12)
minute := (hms >> 6) % (1 << 6)
second := hms % (1 << 6)
frac := v % (1 << 24)
return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d.%06d", year, month, day, hour, minute, second, frac)
}
func (d *jsonBinaryDecoder) decodeCount(data []byte, isSmall bool) int {
if isSmall {
v := d.decodeUint16(data)
return int(v)
}
return int(d.decodeUint32(data))
}
func (d *jsonBinaryDecoder) decodeVariableLength(data []byte) (int, int) {
// The max size for variable length is math.MaxUint32, so
// here we can use 5 bytes to save it.
maxCount := 5
if len(data) < maxCount {
maxCount = len(data)
}
pos := 0
length := uint64(0)
for ; pos < maxCount; pos++ {
v := data[pos]
length |= uint64(v&0x7F) << uint(7*pos)
if v&0x80 == 0 {
if length > math.MaxUint32 {
d.err = errors.Errorf("variable length %d must <= %d", length, math.MaxUint32)
return 0, 0
}
pos += 1
// TODO: should consider length overflow int here.
return int(length), pos
}
}
d.err = errors.New("decode variable length failed")
return 0, 0
}

View File

@@ -0,0 +1,334 @@
package replication
import (
"bytes"
"fmt"
"io"
"os"
"sync/atomic"
"github.com/juju/errors"
)
type BinlogParser struct {
format *FormatDescriptionEvent
tables map[uint64]*TableMapEvent
// for rawMode, we only parse FormatDescriptionEvent and RotateEvent
rawMode bool
parseTime bool
// used to start/stop processing
stopProcessing uint32
useDecimal bool
}
func NewBinlogParser() *BinlogParser {
p := new(BinlogParser)
p.tables = make(map[uint64]*TableMapEvent)
// p.stop = make(uint32)
return p
}
func (p *BinlogParser) Stop() {
atomic.StoreUint32(&p.stopProcessing, 1)
}
func (p *BinlogParser) Resume() {
atomic.StoreUint32(&p.stopProcessing, 0)
}
func (p *BinlogParser) Reset() {
p.format = nil
}
type OnEventFunc func(*BinlogEvent) error
func (p *BinlogParser) ParseFile(name string, offset int64, onEvent OnEventFunc) error {
f, err := os.Open(name)
if err != nil {
return errors.Trace(err)
}
defer f.Close()
b := make([]byte, 4)
if _, err = f.Read(b); err != nil {
return errors.Trace(err)
} else if !bytes.Equal(b, BinLogFileHeader) {
return errors.Errorf("%s is not a valid binlog file, head 4 bytes must fe'bin' ", name)
}
if offset < 4 {
offset = 4
} else if offset > 4 {
// FORMAT_DESCRIPTION event should be read by default always (despite that fact passed offset may be higher than 4)
if _, err = f.Seek(4, os.SEEK_SET); err != nil {
return errors.Errorf("seek %s to %d error %v", name, offset, err)
}
p.getFormatDescriptionEvent(f, onEvent)
}
if _, err = f.Seek(offset, os.SEEK_SET); err != nil {
return errors.Errorf("seek %s to %d error %v", name, offset, err)
}
return p.ParseReader(f, onEvent)
}
func (p *BinlogParser) getFormatDescriptionEvent(r io.Reader, onEvent OnEventFunc) error {
_, err := p.parseSingleEvent(&r, onEvent)
return err
}
func (p *BinlogParser) parseSingleEvent(r *io.Reader, onEvent OnEventFunc) (bool, error) {
var err error
var n int64
headBuf := make([]byte, EventHeaderSize)
if _, err = io.ReadFull(*r, headBuf); err == io.EOF {
return true, nil
} else if err != nil {
return false, errors.Trace(err)
}
var h *EventHeader
h, err = p.parseHeader(headBuf)
if err != nil {
return false, errors.Trace(err)
}
if h.EventSize <= uint32(EventHeaderSize) {
return false, errors.Errorf("invalid event header, event size is %d, too small", h.EventSize)
}
var buf bytes.Buffer
if n, err = io.CopyN(&buf, *r, int64(h.EventSize)-int64(EventHeaderSize)); err != nil {
return false, errors.Errorf("get event body err %v, need %d - %d, but got %d", err, h.EventSize, EventHeaderSize, n)
}
data := buf.Bytes()
rawData := data
eventLen := int(h.EventSize) - EventHeaderSize
if len(data) != eventLen {
return false, errors.Errorf("invalid data size %d in event %s, less event length %d", len(data), h.EventType, eventLen)
}
var e Event
e, err = p.parseEvent(h, data)
if err != nil {
if _, ok := err.(errMissingTableMapEvent); ok {
return false, nil
}
return false, errors.Trace(err)
}
if err = onEvent(&BinlogEvent{rawData, h, e}); err != nil {
return false, errors.Trace(err)
}
return false, nil
}
func (p *BinlogParser) ParseReader(r io.Reader, onEvent OnEventFunc) error {
for {
if atomic.LoadUint32(&p.stopProcessing) == 1 {
break
}
done, err := p.parseSingleEvent(&r, onEvent)
if err != nil {
if _, ok := err.(errMissingTableMapEvent); ok {
continue
}
return errors.Trace(err)
}
if done {
break
}
}
return nil
}
func (p *BinlogParser) SetRawMode(mode bool) {
p.rawMode = mode
}
func (p *BinlogParser) SetParseTime(parseTime bool) {
p.parseTime = parseTime
}
func (p *BinlogParser) SetUseDecimal(useDecimal bool) {
p.useDecimal = useDecimal
}
func (p *BinlogParser) parseHeader(data []byte) (*EventHeader, error) {
h := new(EventHeader)
err := h.Decode(data)
if err != nil {
return nil, err
}
return h, nil
}
func (p *BinlogParser) parseEvent(h *EventHeader, data []byte) (Event, error) {
var e Event
if h.EventType == FORMAT_DESCRIPTION_EVENT {
p.format = &FormatDescriptionEvent{}
e = p.format
} else {
if p.format != nil && p.format.ChecksumAlgorithm == BINLOG_CHECKSUM_ALG_CRC32 {
data = data[0 : len(data)-4]
}
if h.EventType == ROTATE_EVENT {
e = &RotateEvent{}
} else if !p.rawMode {
switch h.EventType {
case QUERY_EVENT:
e = &QueryEvent{}
case XID_EVENT:
e = &XIDEvent{}
case TABLE_MAP_EVENT:
te := &TableMapEvent{}
if p.format.EventTypeHeaderLengths[TABLE_MAP_EVENT-1] == 6 {
te.tableIDSize = 4
} else {
te.tableIDSize = 6
}
e = te
case WRITE_ROWS_EVENTv0,
UPDATE_ROWS_EVENTv0,
DELETE_ROWS_EVENTv0,
WRITE_ROWS_EVENTv1,
DELETE_ROWS_EVENTv1,
UPDATE_ROWS_EVENTv1,
WRITE_ROWS_EVENTv2,
UPDATE_ROWS_EVENTv2,
DELETE_ROWS_EVENTv2:
e = p.newRowsEvent(h)
case ROWS_QUERY_EVENT:
e = &RowsQueryEvent{}
case GTID_EVENT:
e = &GTIDEvent{}
case BEGIN_LOAD_QUERY_EVENT:
e = &BeginLoadQueryEvent{}
case EXECUTE_LOAD_QUERY_EVENT:
e = &ExecuteLoadQueryEvent{}
case MARIADB_ANNOTATE_ROWS_EVENT:
e = &MariadbAnnotateRowsEvent{}
case MARIADB_BINLOG_CHECKPOINT_EVENT:
e = &MariadbBinlogCheckPointEvent{}
case MARIADB_GTID_LIST_EVENT:
e = &MariadbGTIDListEvent{}
case MARIADB_GTID_EVENT:
ee := &MariadbGTIDEvent{}
ee.GTID.ServerID = h.ServerID
e = ee
default:
e = &GenericEvent{}
}
} else {
e = &GenericEvent{}
}
}
if err := e.Decode(data); err != nil {
return nil, &EventError{h, err.Error(), data}
}
if te, ok := e.(*TableMapEvent); ok {
p.tables[te.TableID] = te
}
if re, ok := e.(*RowsEvent); ok {
if (re.Flags & RowsEventStmtEndFlag) > 0 {
// Refer https://github.com/alibaba/canal/blob/38cc81b7dab29b51371096fb6763ca3a8432ffee/dbsync/src/main/java/com/taobao/tddl/dbsync/binlog/event/RowsLogEvent.java#L176
p.tables = make(map[uint64]*TableMapEvent)
}
}
return e, nil
}
// Given the bytes for a a binary log event: return the decoded event.
// With the exception of the FORMAT_DESCRIPTION_EVENT event type
// there must have previously been passed a FORMAT_DESCRIPTION_EVENT
// into the parser for this to work properly on any given event.
// Passing a new FORMAT_DESCRIPTION_EVENT into the parser will replace
// an existing one.
func (p *BinlogParser) Parse(data []byte) (*BinlogEvent, error) {
rawData := data
h, err := p.parseHeader(data)
if err != nil {
return nil, err
}
data = data[EventHeaderSize:]
eventLen := int(h.EventSize) - EventHeaderSize
if len(data) != eventLen {
return nil, fmt.Errorf("invalid data size %d in event %s, less event length %d", len(data), h.EventType, eventLen)
}
e, err := p.parseEvent(h, data)
if err != nil {
return nil, err
}
return &BinlogEvent{rawData, h, e}, nil
}
func (p *BinlogParser) newRowsEvent(h *EventHeader) *RowsEvent {
e := &RowsEvent{}
if p.format.EventTypeHeaderLengths[h.EventType-1] == 6 {
e.tableIDSize = 4
} else {
e.tableIDSize = 6
}
e.needBitmap2 = false
e.tables = p.tables
e.parseTime = p.parseTime
e.useDecimal = p.useDecimal
switch h.EventType {
case WRITE_ROWS_EVENTv0:
e.Version = 0
case UPDATE_ROWS_EVENTv0:
e.Version = 0
case DELETE_ROWS_EVENTv0:
e.Version = 0
case WRITE_ROWS_EVENTv1:
e.Version = 1
case DELETE_ROWS_EVENTv1:
e.Version = 1
case UPDATE_ROWS_EVENTv1:
e.Version = 1
e.needBitmap2 = true
case WRITE_ROWS_EVENTv2:
e.Version = 2
case UPDATE_ROWS_EVENTv2:
e.Version = 2
e.needBitmap2 = true
case DELETE_ROWS_EVENTv2:
e.Version = 2
}
return e
}

View File

@@ -0,0 +1,844 @@
package replication
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"strconv"
"time"
"github.com/juju/errors"
"github.com/shopspring/decimal"
. "github.com/siddontang/go-mysql/mysql"
"github.com/siddontang/go/hack"
log "github.com/sirupsen/logrus"
)
type errMissingTableMapEvent error
type TableMapEvent struct {
tableIDSize int
TableID uint64
Flags uint16
Schema []byte
Table []byte
ColumnCount uint64
ColumnType []byte
ColumnMeta []uint16
//len = (ColumnCount + 7) / 8
NullBitmap []byte
}
func (e *TableMapEvent) Decode(data []byte) error {
pos := 0
e.TableID = FixedLengthInt(data[0:e.tableIDSize])
pos += e.tableIDSize
e.Flags = binary.LittleEndian.Uint16(data[pos:])
pos += 2
schemaLength := data[pos]
pos++
e.Schema = data[pos : pos+int(schemaLength)]
pos += int(schemaLength)
//skip 0x00
pos++
tableLength := data[pos]
pos++
e.Table = data[pos : pos+int(tableLength)]
pos += int(tableLength)
//skip 0x00
pos++
var n int
e.ColumnCount, _, n = LengthEncodedInt(data[pos:])
pos += n
e.ColumnType = data[pos : pos+int(e.ColumnCount)]
pos += int(e.ColumnCount)
var err error
var metaData []byte
if metaData, _, n, err = LengthEnodedString(data[pos:]); err != nil {
return errors.Trace(err)
}
if err = e.decodeMeta(metaData); err != nil {
return errors.Trace(err)
}
pos += n
if len(data[pos:]) != bitmapByteSize(int(e.ColumnCount)) {
return io.EOF
}
e.NullBitmap = data[pos:]
return nil
}
func bitmapByteSize(columnCount int) int {
return int(columnCount+7) / 8
}
// see mysql sql/log_event.h
/*
0 byte
MYSQL_TYPE_DECIMAL
MYSQL_TYPE_TINY
MYSQL_TYPE_SHORT
MYSQL_TYPE_LONG
MYSQL_TYPE_NULL
MYSQL_TYPE_TIMESTAMP
MYSQL_TYPE_LONGLONG
MYSQL_TYPE_INT24
MYSQL_TYPE_DATE
MYSQL_TYPE_TIME
MYSQL_TYPE_DATETIME
MYSQL_TYPE_YEAR
1 byte
MYSQL_TYPE_FLOAT
MYSQL_TYPE_DOUBLE
MYSQL_TYPE_BLOB
MYSQL_TYPE_GEOMETRY
//maybe
MYSQL_TYPE_TIME2
MYSQL_TYPE_DATETIME2
MYSQL_TYPE_TIMESTAMP2
2 byte
MYSQL_TYPE_VARCHAR
MYSQL_TYPE_BIT
MYSQL_TYPE_NEWDECIMAL
MYSQL_TYPE_VAR_STRING
MYSQL_TYPE_STRING
This enumeration value is only used internally and cannot exist in a binlog.
MYSQL_TYPE_NEWDATE
MYSQL_TYPE_ENUM
MYSQL_TYPE_SET
MYSQL_TYPE_TINY_BLOB
MYSQL_TYPE_MEDIUM_BLOB
MYSQL_TYPE_LONG_BLOB
*/
func (e *TableMapEvent) decodeMeta(data []byte) error {
pos := 0
e.ColumnMeta = make([]uint16, e.ColumnCount)
for i, t := range e.ColumnType {
switch t {
case MYSQL_TYPE_STRING:
var x uint16 = uint16(data[pos]) << 8 //real type
x += uint16(data[pos+1]) //pack or field length
e.ColumnMeta[i] = x
pos += 2
case MYSQL_TYPE_NEWDECIMAL:
var x uint16 = uint16(data[pos]) << 8 //precision
x += uint16(data[pos+1]) //decimals
e.ColumnMeta[i] = x
pos += 2
case MYSQL_TYPE_VAR_STRING,
MYSQL_TYPE_VARCHAR,
MYSQL_TYPE_BIT:
e.ColumnMeta[i] = binary.LittleEndian.Uint16(data[pos:])
pos += 2
case MYSQL_TYPE_BLOB,
MYSQL_TYPE_DOUBLE,
MYSQL_TYPE_FLOAT,
MYSQL_TYPE_GEOMETRY,
MYSQL_TYPE_JSON:
e.ColumnMeta[i] = uint16(data[pos])
pos++
case MYSQL_TYPE_TIME2,
MYSQL_TYPE_DATETIME2,
MYSQL_TYPE_TIMESTAMP2:
e.ColumnMeta[i] = uint16(data[pos])
pos++
case MYSQL_TYPE_NEWDATE,
MYSQL_TYPE_ENUM,
MYSQL_TYPE_SET,
MYSQL_TYPE_TINY_BLOB,
MYSQL_TYPE_MEDIUM_BLOB,
MYSQL_TYPE_LONG_BLOB:
return errors.Errorf("unsupport type in binlog %d", t)
default:
e.ColumnMeta[i] = 0
}
}
return nil
}
func (e *TableMapEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "TableID: %d\n", e.TableID)
fmt.Fprintf(w, "TableID size: %d\n", e.tableIDSize)
fmt.Fprintf(w, "Flags: %d\n", e.Flags)
fmt.Fprintf(w, "Schema: %s\n", e.Schema)
fmt.Fprintf(w, "Table: %s\n", e.Table)
fmt.Fprintf(w, "Column count: %d\n", e.ColumnCount)
fmt.Fprintf(w, "Column type: \n%s", hex.Dump(e.ColumnType))
fmt.Fprintf(w, "NULL bitmap: \n%s", hex.Dump(e.NullBitmap))
fmt.Fprintln(w)
}
// RowsEventStmtEndFlag is set in the end of the statement.
const RowsEventStmtEndFlag = 0x01
type RowsEvent struct {
//0, 1, 2
Version int
tableIDSize int
tables map[uint64]*TableMapEvent
needBitmap2 bool
Table *TableMapEvent
TableID uint64
Flags uint16
//if version == 2
ExtraData []byte
//lenenc_int
ColumnCount uint64
//len = (ColumnCount + 7) / 8
ColumnBitmap1 []byte
//if UPDATE_ROWS_EVENTv1 or v2
//len = (ColumnCount + 7) / 8
ColumnBitmap2 []byte
//rows: invalid: int64, float64, bool, []byte, string
Rows [][]interface{}
parseTime bool
useDecimal bool
}
func (e *RowsEvent) Decode(data []byte) error {
pos := 0
e.TableID = FixedLengthInt(data[0:e.tableIDSize])
pos += e.tableIDSize
e.Flags = binary.LittleEndian.Uint16(data[pos:])
pos += 2
if e.Version == 2 {
dataLen := binary.LittleEndian.Uint16(data[pos:])
pos += 2
e.ExtraData = data[pos : pos+int(dataLen-2)]
pos += int(dataLen - 2)
}
var n int
e.ColumnCount, _, n = LengthEncodedInt(data[pos:])
pos += n
bitCount := bitmapByteSize(int(e.ColumnCount))
e.ColumnBitmap1 = data[pos : pos+bitCount]
pos += bitCount
if e.needBitmap2 {
e.ColumnBitmap2 = data[pos : pos+bitCount]
pos += bitCount
}
var ok bool
e.Table, ok = e.tables[e.TableID]
if !ok {
if len(e.tables) > 0 {
return errors.Errorf("invalid table id %d, no corresponding table map event", e.TableID)
} else {
return errMissingTableMapEvent(errors.Errorf("invalid table id %d, no corresponding table map event", e.TableID))
}
}
var err error
// ... repeat rows until event-end
defer func() {
if r := recover(); r != nil {
log.Fatalf("parse rows event panic %v, data %q, parsed rows %#v, table map %#v\n%s", r, data, e, e.Table, Pstack())
}
}()
for pos < len(data) {
if n, err = e.decodeRows(data[pos:], e.Table, e.ColumnBitmap1); err != nil {
return errors.Trace(err)
}
pos += n
if e.needBitmap2 {
if n, err = e.decodeRows(data[pos:], e.Table, e.ColumnBitmap2); err != nil {
return errors.Trace(err)
}
pos += n
}
}
return nil
}
func isBitSet(bitmap []byte, i int) bool {
return bitmap[i>>3]&(1<<(uint(i)&7)) > 0
}
func (e *RowsEvent) decodeRows(data []byte, table *TableMapEvent, bitmap []byte) (int, error) {
row := make([]interface{}, e.ColumnCount)
pos := 0
// refer: https://github.com/alibaba/canal/blob/c3e38e50e269adafdd38a48c63a1740cde304c67/dbsync/src/main/java/com/taobao/tddl/dbsync/binlog/event/RowsLogBuffer.java#L63
count := 0
for i := 0; i < int(e.ColumnCount); i++ {
if isBitSet(bitmap, i) {
count++
}
}
count = (count + 7) / 8
nullBitmap := data[pos : pos+count]
pos += count
nullbitIndex := 0
var n int
var err error
for i := 0; i < int(e.ColumnCount); i++ {
if !isBitSet(bitmap, i) {
continue
}
isNull := (uint32(nullBitmap[nullbitIndex/8]) >> uint32(nullbitIndex%8)) & 0x01
nullbitIndex++
if isNull > 0 {
row[i] = nil
continue
}
row[i], n, err = e.decodeValue(data[pos:], table.ColumnType[i], table.ColumnMeta[i])
if err != nil {
return 0, err
}
pos += n
}
e.Rows = append(e.Rows, row)
return pos, nil
}
func (e *RowsEvent) parseFracTime(t interface{}) interface{} {
v, ok := t.(fracTime)
if !ok {
return t
}
if !e.parseTime {
// Don't parse time, return string directly
return v.String()
}
// return Golang time directly
return v.Time
}
// see mysql sql/log_event.cc log_event_print_value
func (e *RowsEvent) decodeValue(data []byte, tp byte, meta uint16) (v interface{}, n int, err error) {
var length int = 0
if tp == MYSQL_TYPE_STRING {
if meta >= 256 {
b0 := uint8(meta >> 8)
b1 := uint8(meta & 0xFF)
if b0&0x30 != 0x30 {
length = int(uint16(b1) | (uint16((b0&0x30)^0x30) << 4))
tp = byte(b0 | 0x30)
} else {
length = int(meta & 0xFF)
tp = b0
}
} else {
length = int(meta)
}
}
switch tp {
case MYSQL_TYPE_NULL:
return nil, 0, nil
case MYSQL_TYPE_LONG:
n = 4
v = ParseBinaryInt32(data)
case MYSQL_TYPE_TINY:
n = 1
v = ParseBinaryInt8(data)
case MYSQL_TYPE_SHORT:
n = 2
v = ParseBinaryInt16(data)
case MYSQL_TYPE_INT24:
n = 3
v = ParseBinaryInt24(data)
case MYSQL_TYPE_LONGLONG:
n = 8
v = ParseBinaryInt64(data)
case MYSQL_TYPE_NEWDECIMAL:
prec := uint8(meta >> 8)
scale := uint8(meta & 0xFF)
v, n, err = decodeDecimal(data, int(prec), int(scale), e.useDecimal)
case MYSQL_TYPE_FLOAT:
n = 4
v = ParseBinaryFloat32(data)
case MYSQL_TYPE_DOUBLE:
n = 8
v = ParseBinaryFloat64(data)
case MYSQL_TYPE_BIT:
nbits := ((meta >> 8) * 8) + (meta & 0xFF)
n = int(nbits+7) / 8
//use int64 for bit
v, err = decodeBit(data, int(nbits), int(n))
case MYSQL_TYPE_TIMESTAMP:
n = 4
t := binary.LittleEndian.Uint32(data)
v = e.parseFracTime(fracTime{time.Unix(int64(t), 0), 0})
case MYSQL_TYPE_TIMESTAMP2:
v, n, err = decodeTimestamp2(data, meta)
v = e.parseFracTime(v)
case MYSQL_TYPE_DATETIME:
n = 8
i64 := binary.LittleEndian.Uint64(data)
d := i64 / 1000000
t := i64 % 1000000
v = e.parseFracTime(fracTime{time.Date(int(d/10000),
time.Month((d%10000)/100),
int(d%100),
int(t/10000),
int((t%10000)/100),
int(t%100),
0,
time.UTC), 0})
case MYSQL_TYPE_DATETIME2:
v, n, err = decodeDatetime2(data, meta)
v = e.parseFracTime(v)
case MYSQL_TYPE_TIME:
n = 3
i32 := uint32(FixedLengthInt(data[0:3]))
if i32 == 0 {
v = "00:00:00"
} else {
sign := ""
if i32 < 0 {
sign = "-"
}
v = fmt.Sprintf("%s%02d:%02d:%02d", sign, i32/10000, (i32%10000)/100, i32%100)
}
case MYSQL_TYPE_TIME2:
v, n, err = decodeTime2(data, meta)
case MYSQL_TYPE_DATE:
n = 3
i32 := uint32(FixedLengthInt(data[0:3]))
if i32 == 0 {
v = "0000-00-00"
} else {
v = fmt.Sprintf("%04d-%02d-%02d", i32/(16*32), i32/32%16, i32%32)
}
case MYSQL_TYPE_YEAR:
n = 1
v = int(data[0]) + 1900
case MYSQL_TYPE_ENUM:
l := meta & 0xFF
switch l {
case 1:
v = int64(data[0])
n = 1
case 2:
v = int64(binary.BigEndian.Uint16(data))
n = 2
default:
err = fmt.Errorf("Unknown ENUM packlen=%d", l)
}
case MYSQL_TYPE_SET:
n = int(meta & 0xFF)
nbits := n * 8
v, err = decodeBit(data, nbits, n)
case MYSQL_TYPE_BLOB:
v, n, err = decodeBlob(data, meta)
case MYSQL_TYPE_VARCHAR,
MYSQL_TYPE_VAR_STRING:
length = int(meta)
v, n = decodeString(data, length)
case MYSQL_TYPE_STRING:
v, n = decodeString(data, length)
case MYSQL_TYPE_JSON:
// Refer: https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AbstractRowsEventDataDeserializer.java#L404
length = int(FixedLengthInt(data[0:meta]))
n = length + int(meta)
v, err = e.decodeJsonBinary(data[meta:n])
case MYSQL_TYPE_GEOMETRY:
// MySQL saves Geometry as Blob in binlog
// Seem that the binary format is SRID (4 bytes) + WKB, outer can use
// MySQL GeoFromWKB or others to create the geometry data.
// Refer https://dev.mysql.com/doc/refman/5.7/en/gis-wkb-functions.html
// I also find some go libs to handle WKB if possible
// see https://github.com/twpayne/go-geom or https://github.com/paulmach/go.geo
v, n, err = decodeBlob(data, meta)
default:
err = fmt.Errorf("unsupport type %d in binlog and don't know how to handle", tp)
}
return
}
func decodeString(data []byte, length int) (v string, n int) {
if length < 256 {
length = int(data[0])
n = int(length) + 1
v = hack.String(data[1:n])
} else {
length = int(binary.LittleEndian.Uint16(data[0:]))
n = length + 2
v = hack.String(data[2:n])
}
return
}
const digitsPerInteger int = 9
var compressedBytes = []int{0, 1, 1, 2, 2, 3, 3, 4, 4, 4}
func decodeDecimalDecompressValue(compIndx int, data []byte, mask uint8) (size int, value uint32) {
size = compressedBytes[compIndx]
databuff := make([]byte, size)
for i := 0; i < size; i++ {
databuff[i] = data[i] ^ mask
}
value = uint32(BFixedLengthInt(databuff))
return
}
func decodeDecimal(data []byte, precision int, decimals int, useDecimal bool) (interface{}, int, error) {
//see python mysql replication and https://github.com/jeremycole/mysql_binlog
integral := (precision - decimals)
uncompIntegral := int(integral / digitsPerInteger)
uncompFractional := int(decimals / digitsPerInteger)
compIntegral := integral - (uncompIntegral * digitsPerInteger)
compFractional := decimals - (uncompFractional * digitsPerInteger)
binSize := uncompIntegral*4 + compressedBytes[compIntegral] +
uncompFractional*4 + compressedBytes[compFractional]
buf := make([]byte, binSize)
copy(buf, data[:binSize])
//must copy the data for later change
data = buf
// Support negative
// The sign is encoded in the high bit of the the byte
// But this bit can also be used in the value
value := uint32(data[0])
var res bytes.Buffer
var mask uint32 = 0
if value&0x80 == 0 {
mask = uint32((1 << 32) - 1)
res.WriteString("-")
}
//clear sign
data[0] ^= 0x80
pos, value := decodeDecimalDecompressValue(compIntegral, data, uint8(mask))
res.WriteString(fmt.Sprintf("%d", value))
for i := 0; i < uncompIntegral; i++ {
value = binary.BigEndian.Uint32(data[pos:]) ^ mask
pos += 4
res.WriteString(fmt.Sprintf("%09d", value))
}
res.WriteString(".")
for i := 0; i < uncompFractional; i++ {
value = binary.BigEndian.Uint32(data[pos:]) ^ mask
pos += 4
res.WriteString(fmt.Sprintf("%09d", value))
}
if size, value := decodeDecimalDecompressValue(compFractional, data[pos:], uint8(mask)); size > 0 {
res.WriteString(fmt.Sprintf("%0*d", compFractional, value))
pos += size
}
if useDecimal {
f, err := decimal.NewFromString(hack.String(res.Bytes()))
return f, pos, err
}
f, err := strconv.ParseFloat(hack.String(res.Bytes()), 64)
return f, pos, err
}
func decodeBit(data []byte, nbits int, length int) (value int64, err error) {
if nbits > 1 {
switch length {
case 1:
value = int64(data[0])
case 2:
value = int64(binary.BigEndian.Uint16(data))
case 3:
value = int64(BFixedLengthInt(data[0:3]))
case 4:
value = int64(binary.BigEndian.Uint32(data))
case 5:
value = int64(BFixedLengthInt(data[0:5]))
case 6:
value = int64(BFixedLengthInt(data[0:6]))
case 7:
value = int64(BFixedLengthInt(data[0:7]))
case 8:
value = int64(binary.BigEndian.Uint64(data))
default:
err = fmt.Errorf("invalid bit length %d", length)
}
} else {
if length != 1 {
err = fmt.Errorf("invalid bit length %d", length)
} else {
value = int64(data[0])
}
}
return
}
func decodeTimestamp2(data []byte, dec uint16) (interface{}, int, error) {
//get timestamp binary length
n := int(4 + (dec+1)/2)
sec := int64(binary.BigEndian.Uint32(data[0:4]))
usec := int64(0)
switch dec {
case 1, 2:
usec = int64(data[4]) * 10000
case 3, 4:
usec = int64(binary.BigEndian.Uint16(data[4:])) * 100
case 5, 6:
usec = int64(BFixedLengthInt(data[4:7]))
}
if sec == 0 {
return formatZeroTime(int(usec), int(dec)), n, nil
}
return fracTime{time.Unix(sec, usec*1000), int(dec)}, n, nil
}
const DATETIMEF_INT_OFS int64 = 0x8000000000
func decodeDatetime2(data []byte, dec uint16) (interface{}, int, error) {
//get datetime binary length
n := int(5 + (dec+1)/2)
intPart := int64(BFixedLengthInt(data[0:5])) - DATETIMEF_INT_OFS
var frac int64 = 0
switch dec {
case 1, 2:
frac = int64(data[5]) * 10000
case 3, 4:
frac = int64(binary.BigEndian.Uint16(data[5:7])) * 100
case 5, 6:
frac = int64(BFixedLengthInt(data[5:8]))
}
if intPart == 0 {
return formatZeroTime(int(frac), int(dec)), n, nil
}
tmp := intPart<<24 + frac
//handle sign???
if tmp < 0 {
tmp = -tmp
}
// var secPart int64 = tmp % (1 << 24)
ymdhms := tmp >> 24
ymd := ymdhms >> 17
ym := ymd >> 5
hms := ymdhms % (1 << 17)
day := int(ymd % (1 << 5))
month := int(ym % 13)
year := int(ym / 13)
second := int(hms % (1 << 6))
minute := int((hms >> 6) % (1 << 6))
hour := int((hms >> 12))
return fracTime{time.Date(year, time.Month(month), day, hour, minute, second, int(frac*1000), time.UTC), int(dec)}, n, nil
}
const TIMEF_OFS int64 = 0x800000000000
const TIMEF_INT_OFS int64 = 0x800000
func decodeTime2(data []byte, dec uint16) (string, int, error) {
//time binary length
n := int(3 + (dec+1)/2)
tmp := int64(0)
intPart := int64(0)
frac := int64(0)
switch dec {
case 1:
case 2:
intPart = int64(BFixedLengthInt(data[0:3])) - TIMEF_INT_OFS
frac = int64(data[3])
if intPart < 0 && frac > 0 {
/*
Negative values are stored with reverse fractional part order,
for binary sort compatibility.
Disk value intpart frac Time value Memory value
800000.00 0 0 00:00:00.00 0000000000.000000
7FFFFF.FF -1 255 -00:00:00.01 FFFFFFFFFF.FFD8F0
7FFFFF.9D -1 99 -00:00:00.99 FFFFFFFFFF.F0E4D0
7FFFFF.00 -1 0 -00:00:01.00 FFFFFFFFFF.000000
7FFFFE.FF -1 255 -00:00:01.01 FFFFFFFFFE.FFD8F0
7FFFFE.F6 -2 246 -00:00:01.10 FFFFFFFFFE.FE7960
Formula to convert fractional part from disk format
(now stored in "frac" variable) to absolute value: "0x100 - frac".
To reconstruct in-memory value, we shift
to the next integer value and then substruct fractional part.
*/
intPart++ /* Shift to the next integer value */
frac -= 0x100 /* -(0x100 - frac) */
}
tmp = intPart<<24 + frac*10000
case 3:
case 4:
intPart = int64(BFixedLengthInt(data[0:3])) - TIMEF_INT_OFS
frac = int64(binary.BigEndian.Uint16(data[3:5]))
if intPart < 0 && frac > 0 {
/*
Fix reverse fractional part order: "0x10000 - frac".
See comments for FSP=1 and FSP=2 above.
*/
intPart++ /* Shift to the next integer value */
frac -= 0x10000 /* -(0x10000-frac) */
}
tmp = intPart<<24 + frac*100
case 5:
case 6:
tmp = int64(BFixedLengthInt(data[0:6])) - TIMEF_OFS
default:
intPart = int64(BFixedLengthInt(data[0:3])) - TIMEF_INT_OFS
tmp = intPart << 24
}
if intPart == 0 {
return "00:00:00", n, nil
}
hms := int64(0)
sign := ""
if tmp < 0 {
tmp = -tmp
sign = "-"
}
hms = tmp >> 24
hour := (hms >> 12) % (1 << 10) /* 10 bits starting at 12th */
minute := (hms >> 6) % (1 << 6) /* 6 bits starting at 6th */
second := hms % (1 << 6) /* 6 bits starting at 0th */
secPart := tmp % (1 << 24)
if secPart != 0 {
return fmt.Sprintf("%s%02d:%02d:%02d.%06d", sign, hour, minute, second, secPart), n, nil
}
return fmt.Sprintf("%s%02d:%02d:%02d", sign, hour, minute, second), n, nil
}
func decodeBlob(data []byte, meta uint16) (v []byte, n int, err error) {
var length int
switch meta {
case 1:
length = int(data[0])
v = data[1 : 1+length]
n = length + 1
case 2:
length = int(binary.LittleEndian.Uint16(data))
v = data[2 : 2+length]
n = length + 2
case 3:
length = int(FixedLengthInt(data[0:3]))
v = data[3 : 3+length]
n = length + 3
case 4:
length = int(binary.LittleEndian.Uint32(data))
v = data[4 : 4+length]
n = length + 4
default:
err = fmt.Errorf("invalid blob packlen = %d", meta)
}
return
}
func (e *RowsEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "TableID: %d\n", e.TableID)
fmt.Fprintf(w, "Flags: %d\n", e.Flags)
fmt.Fprintf(w, "Column count: %d\n", e.ColumnCount)
fmt.Fprintf(w, "Values:\n")
for _, rows := range e.Rows {
fmt.Fprintf(w, "--\n")
for j, d := range rows {
if _, ok := d.([]byte); ok {
fmt.Fprintf(w, "%d:%q\n", j, d)
} else {
fmt.Fprintf(w, "%d:%#v\n", j, d)
}
}
}
fmt.Fprintln(w)
}
type RowsQueryEvent struct {
Query []byte
}
func (e *RowsQueryEvent) Decode(data []byte) error {
//ignore length byte 1
e.Query = data[1:]
return nil
}
func (e *RowsQueryEvent) Dump(w io.Writer) {
fmt.Fprintf(w, "Query: %s\n", e.Query)
fmt.Fprintln(w)
}

View File

@@ -0,0 +1,43 @@
package replication
import (
"fmt"
"strings"
"time"
)
var (
fracTimeFormat []string
)
// fracTime is a help structure wrapping Golang Time.
type fracTime struct {
time.Time
// Dec must in [0, 6]
Dec int
}
func (t fracTime) String() string {
return t.Format(fracTimeFormat[t.Dec])
}
func formatZeroTime(frac int, dec int) string {
if dec == 0 {
return "0000-00-00 00:00:00"
}
s := fmt.Sprintf("0000-00-00 00:00:00.%06d", frac)
// dec must < 6, if frac is 924000, but dec is 3, we must output 924 here.
return s[0 : len(s)-(6-dec)]
}
func init() {
fracTimeFormat = make([]string, 7)
fracTimeFormat[0] = "2006-01-02 15:04:05"
for i := 1; i <= 6; i++ {
fracTimeFormat[i] = fmt.Sprintf("2006-01-02 15:04:05.%s", strings.Repeat("0", i))
}
}