2016-04-06 11:05:21 +00:00
|
|
|
/*
|
|
|
|
Copyright 2016 GitHub Inc.
|
|
|
|
See https://github.com/github/gh-osc/blob/master/LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
package logic
|
|
|
|
|
|
|
|
import (
|
|
|
|
gosql "database/sql"
|
|
|
|
"fmt"
|
2016-04-07 13:57:12 +00:00
|
|
|
"strings"
|
|
|
|
|
2016-04-06 11:05:21 +00:00
|
|
|
"github.com/github/gh-osc/go/base"
|
2016-04-06 16:44:54 +00:00
|
|
|
"github.com/github/gh-osc/go/binlog"
|
2016-04-06 11:05:21 +00:00
|
|
|
"github.com/github/gh-osc/go/mysql"
|
|
|
|
|
|
|
|
"github.com/outbrain/golib/log"
|
|
|
|
"github.com/outbrain/golib/sqlutils"
|
|
|
|
)
|
|
|
|
|
|
|
|
type BinlogEventListener struct {
|
|
|
|
async bool
|
|
|
|
databaseName string
|
|
|
|
tableName string
|
2016-04-06 16:44:54 +00:00
|
|
|
onDmlEvent func(event *binlog.BinlogDMLEvent) error
|
2016-04-06 11:05:21 +00:00
|
|
|
}
|
|
|
|
|
2016-04-07 13:57:12 +00:00
|
|
|
const (
|
|
|
|
EventsChannelBufferSize = 1
|
|
|
|
)
|
|
|
|
|
2016-04-06 11:05:21 +00:00
|
|
|
// EventsStreamer reads data from binary logs and streams it on. It acts as a publisher,
|
|
|
|
// and interested parties may subscribe for per-table events.
|
|
|
|
type EventsStreamer struct {
|
|
|
|
connectionConfig *mysql.ConnectionConfig
|
|
|
|
db *gosql.DB
|
|
|
|
migrationContext *base.MigrationContext
|
|
|
|
nextBinlogCoordinates *mysql.BinlogCoordinates
|
|
|
|
listeners [](*BinlogEventListener)
|
2016-04-07 13:57:12 +00:00
|
|
|
eventsChannel chan *binlog.BinlogEntry
|
|
|
|
binlogReader binlog.BinlogReader
|
2016-04-06 11:05:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewEventsStreamer() *EventsStreamer {
|
|
|
|
return &EventsStreamer{
|
|
|
|
connectionConfig: base.GetMigrationContext().InspectorConnectionConfig,
|
|
|
|
migrationContext: base.GetMigrationContext(),
|
|
|
|
listeners: [](*BinlogEventListener){},
|
2016-04-07 13:57:12 +00:00
|
|
|
eventsChannel: make(chan *binlog.BinlogEntry, EventsChannelBufferSize),
|
2016-04-06 11:05:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *EventsStreamer) AddListener(
|
2016-04-06 16:44:54 +00:00
|
|
|
async bool, databaseName string, tableName string, onDmlEvent func(event *binlog.BinlogDMLEvent) error) (err error) {
|
2016-04-06 11:05:21 +00:00
|
|
|
if databaseName == "" {
|
|
|
|
return fmt.Errorf("Empty database name in AddListener")
|
|
|
|
}
|
|
|
|
if tableName == "" {
|
|
|
|
return fmt.Errorf("Empty table name in AddListener")
|
|
|
|
}
|
|
|
|
listener := &BinlogEventListener{
|
|
|
|
async: async,
|
|
|
|
databaseName: databaseName,
|
|
|
|
tableName: tableName,
|
2016-04-06 16:44:54 +00:00
|
|
|
onDmlEvent: onDmlEvent,
|
2016-04-06 11:05:21 +00:00
|
|
|
}
|
|
|
|
this.listeners = append(this.listeners, listener)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-04-06 16:44:54 +00:00
|
|
|
func (this *EventsStreamer) notifyListeners(binlogEvent *binlog.BinlogDMLEvent) {
|
2016-04-06 11:05:21 +00:00
|
|
|
for _, listener := range this.listeners {
|
2016-04-07 13:57:12 +00:00
|
|
|
if strings.ToLower(listener.databaseName) != strings.ToLower(binlogEvent.DatabaseName) {
|
2016-04-06 11:05:21 +00:00
|
|
|
continue
|
|
|
|
}
|
2016-04-07 13:57:12 +00:00
|
|
|
if strings.ToLower(listener.tableName) != strings.ToLower(binlogEvent.TableName) {
|
2016-04-06 11:05:21 +00:00
|
|
|
continue
|
|
|
|
}
|
2016-04-06 16:44:54 +00:00
|
|
|
onDmlEvent := listener.onDmlEvent
|
2016-04-06 11:05:21 +00:00
|
|
|
if listener.async {
|
|
|
|
go func() {
|
2016-04-06 16:44:54 +00:00
|
|
|
onDmlEvent(binlogEvent)
|
2016-04-06 11:05:21 +00:00
|
|
|
}()
|
|
|
|
} else {
|
2016-04-06 16:44:54 +00:00
|
|
|
onDmlEvent(binlogEvent)
|
2016-04-06 11:05:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *EventsStreamer) InitDBConnections() (err error) {
|
|
|
|
EventsStreamerUri := this.connectionConfig.GetDBUri(this.migrationContext.DatabaseName)
|
|
|
|
if this.db, _, err = sqlutils.GetDB(EventsStreamerUri); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := this.validateConnection(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := this.readCurrentBinlogCoordinates(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-04-07 13:57:12 +00:00
|
|
|
goMySQLReader, err := binlog.NewGoMySQLReader(this.migrationContext.InspectorConnectionConfig)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := goMySQLReader.ConnectBinlogStreamer(*this.nextBinlogCoordinates); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
this.binlogReader = goMySQLReader
|
|
|
|
|
2016-04-06 11:05:21 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// validateConnection issues a simple can-connect to MySQL
|
|
|
|
func (this *EventsStreamer) validateConnection() error {
|
|
|
|
query := `select @@global.port`
|
|
|
|
var port int
|
|
|
|
if err := this.db.QueryRow(query).Scan(&port); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if port != this.connectionConfig.Key.Port {
|
|
|
|
return fmt.Errorf("Unexpected database port reported: %+v", port)
|
|
|
|
}
|
|
|
|
log.Infof("connection validated on %+v", this.connectionConfig.Key)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// validateGrants verifies the user by which we're executing has necessary grants
|
|
|
|
// to do its thang.
|
|
|
|
func (this *EventsStreamer) readCurrentBinlogCoordinates() error {
|
|
|
|
query := `show /* gh-osc readCurrentBinlogCoordinates */ master status`
|
|
|
|
foundMasterStatus := false
|
|
|
|
err := sqlutils.QueryRowsMap(this.db, query, func(m sqlutils.RowMap) error {
|
|
|
|
this.nextBinlogCoordinates = &mysql.BinlogCoordinates{
|
|
|
|
LogFile: m.GetString("File"),
|
|
|
|
LogPos: m.GetInt64("Position"),
|
|
|
|
}
|
|
|
|
foundMasterStatus = true
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !foundMasterStatus {
|
|
|
|
return fmt.Errorf("Got no results from SHOW MASTER STATUS. Bailing out")
|
|
|
|
}
|
|
|
|
log.Debugf("Streamer binlog coordinates: %+v", *this.nextBinlogCoordinates)
|
|
|
|
return nil
|
|
|
|
}
|
2016-04-07 13:57:12 +00:00
|
|
|
|
|
|
|
// StreamEvents will begin streaming events. It will be blocking, so should be
|
|
|
|
// executed by a goroutine
|
|
|
|
func (this *EventsStreamer) StreamEvents(canStopStreaming func() bool) error {
|
|
|
|
go func() {
|
|
|
|
for binlogEntry := range this.eventsChannel {
|
|
|
|
if binlogEntry.DmlEvent != nil {
|
|
|
|
this.notifyListeners(binlogEntry.DmlEvent)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
return this.binlogReader.StreamEvents(canStopStreaming, this.eventsChannel)
|
|
|
|
}
|