Files
replica/replica/position.go
2026-02-12 15:18:55 +01:00

141 lines
3.2 KiB
Go

package replica
import (
"database/sql"
"encoding/json"
"os"
"github.com/go-mysql-org/go-mysql/mysql"
)
// PositionManager handles binlog position persistence
type PositionManager struct {
db *sql.DB
positionFile string
}
// NewPositionManager creates a new position manager
func NewPositionManager(db *sql.DB, positionFile string) *PositionManager {
return &PositionManager{
db: db,
positionFile: positionFile,
}
}
// InitTable creates the binlog_position table if it doesn't exist
func (pm *PositionManager) InitTable() error {
if pm.db == nil {
return nil
}
createTableSQL := `
CREATE TABLE IF NOT EXISTS binlog_position (
id INT PRIMARY KEY,
log_file VARCHAR(255),
log_pos BIGINT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
`
_, err := pm.db.Exec(createTableSQL)
if err != nil {
Errorf("[ERROR] Failed to create binlog_position table: %v", err)
return err
}
return nil
}
// Load loads the last saved position from database or file
func (pm *PositionManager) Load() (mysql.Position, error) {
if pm.db != nil {
return pm.loadFromDB()
}
return pm.loadFromFile()
}
func (pm *PositionManager) loadFromDB() (mysql.Position, error) {
var name string
var pos uint32
err := pm.db.QueryRow(
"SELECT log_file, log_pos FROM binlog_position WHERE id = 1",
).Scan(&name, &pos)
if err == sql.ErrNoRows {
return mysql.Position{Name: "", Pos: 0}, nil
}
if err != nil {
return mysql.Position{Name: "", Pos: 0}, err
}
Infof("[INFO] Loaded position: %s:%d", name, pos)
return mysql.Position{Name: name, Pos: pos}, nil
}
func (pm *PositionManager) loadFromFile() (mysql.Position, error) {
data, err := os.ReadFile(pm.positionFile)
if err != nil {
return mysql.Position{Name: "", Pos: 0}, nil
}
var pos mysql.Position
err = json.Unmarshal(data, &pos)
return pos, err
}
// Save saves the current position to database or file
func (pm *PositionManager) Save(pos mysql.Position) error {
if pm.db != nil {
return pm.saveToDB(pos)
}
return pm.saveToFile(pos)
}
// Reset clears the saved position
func (pm *PositionManager) Reset() error {
if pm.db != nil {
return pm.resetInDB()
}
return pm.resetInFile()
}
func (pm *PositionManager) resetInDB() error {
_, err := pm.db.Exec("DELETE FROM binlog_position WHERE id = 1")
return err
}
func (pm *PositionManager) resetInFile() error {
return os.Remove(pm.positionFile)
}
func (pm *PositionManager) saveToDB(pos mysql.Position) error {
result, err := pm.db.Exec(
"INSERT INTO binlog_position (id, log_file, log_pos) VALUES (1, ?, ?) "+
"ON DUPLICATE KEY UPDATE log_file = VALUES(log_file), log_pos = VALUES(log_pos)",
pos.Name, pos.Pos,
)
if err != nil {
Errorf("[ERROR] Failed to save position: %v", err)
return err
}
rowsAffected, _ := result.RowsAffected()
Infof("[INFO] Saved position: %s:%d (rows: %d)", pos.Name, pos.Pos, rowsAffected)
return nil
}
func (pm *PositionManager) saveToFile(pos mysql.Position) error {
data, err := json.Marshal(pos)
if err != nil {
return err
}
return os.WriteFile(pm.positionFile, data, 0644)
}
// GetPositionFile returns the position file path
func (pm *PositionManager) GetPositionFile() string {
return pm.positionFile
}