package com.zendesk.maxwell.schema; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException; import com.zendesk.maxwell.MaxwellConfig; import com.zendesk.maxwell.recovery.RecoveryInfo; import com.zendesk.maxwell.replication.BinlogPosition; import com.zendesk.maxwell.errors.DuplicateProcessException; import com.zendesk.maxwell.replication.Position; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import snaq.db.ConnectionPool; public class MysqlPositionStore { static final Logger LOGGER = LoggerFactory.getLogger(MysqlPositionStore.class); private static final Long DEFAULT_GTID_SERVER_ID = new Long(0); private final Long serverID; private String clientID; private final boolean gtidMode; private final ConnectionPool connectionPool; public MysqlPositionStore(ConnectionPool pool, Long serverID, String clientID, boolean gtidMode) { this.connectionPool = pool; this.clientID = clientID; this.gtidMode = gtidMode; if (gtidMode) { // we don't use server id for position store in gtid mode this.serverID = DEFAULT_GTID_SERVER_ID; } else { this.serverID = serverID; } } public void set(Position newPosition) throws SQLException { if ( newPosition == null ) return; Long heartbeat = newPosition.getLastHeartbeatRead(); String sql = "INSERT INTO `positions` set " + "server_id = ?, " + "gtid_set = ?, " + "binlog_file = ?, " + "binlog_position = ?, " + "last_heartbeat_read = ?, " + "client_id = ? " + "ON DUPLICATE KEY UPDATE " + "last_heartbeat_read = ?, " + "gtid_set = ?, binlog_file = ?, binlog_position=?"; BinlogPosition binlogPosition = newPosition.getBinlogPosition(); try( Connection c = connectionPool.getConnection() ){ PreparedStatement s = c.prepareStatement(sql); LOGGER.debug("Writing binlog position to " + c.getCatalog() + ".positions: " + newPosition + ", last heartbeat read: " + heartbeat); s.setLong(1, serverID); s.setString(2, binlogPosition.getGtidSetStr()); s.setString(3, binlogPosition.getFile()); s.setLong(4, binlogPosition.getOffset()); s.setLong(5, heartbeat); s.setString(6, clientID); s.setLong(7, heartbeat); s.setString(8, binlogPosition.getGtidSetStr()); s.setString(9, binlogPosition.getFile()); s.setLong(10, binlogPosition.getOffset()); s.execute(); } } public long heartbeat() throws Exception { long heartbeatValue = System.currentTimeMillis(); heartbeat(heartbeatValue); return heartbeatValue; } public synchronized void heartbeat(long heartbeatValue) throws Exception { try ( Connection c = connectionPool.getConnection() ) { heartbeat(c, heartbeatValue); } } /* * the heartbeat system performs two functions: * 1 - it leaves pointers in the binlog in order to facilitate master recovery * 2 - it detects duplicate maxwell processes configured with the same client_id, aborting if we detect a dupe. */ private Long lastHeartbeat = null; private Long insertHeartbeat(Connection c, Long thisHeartbeat) throws SQLException, DuplicateProcessException { String heartbeatInsert = "insert into `heartbeats` set `heartbeat` = ?, `server_id` = ?, `client_id` = ?"; PreparedStatement s = c.prepareStatement(heartbeatInsert); s.setLong(1, thisHeartbeat); s.setLong(2, serverID); s.setString(3, clientID); try { s.execute(); return thisHeartbeat; } catch ( MySQLIntegrityConstraintViolationException e ) { throw new DuplicateProcessException("Found heartbeat row for client,position while trying to insert. Is another maxwell running?"); } } private void heartbeat(Connection c, long thisHeartbeat) throws SQLException, DuplicateProcessException, InterruptedException { if ( lastHeartbeat == null ) { PreparedStatement s = c.prepareStatement("SELECT `heartbeat` from `heartbeats` where server_id = ? and client_id = ?"); s.setLong(1, serverID); s.setString(2, clientID); ResultSet rs = s.executeQuery(); if ( !rs.next() ) { insertHeartbeat(c, thisHeartbeat); lastHeartbeat = thisHeartbeat; return; } else { lastHeartbeat = rs.getLong("heartbeat"); } } String heartbeatUpdate = "update `heartbeats` set `heartbeat` = ? where `server_id` = ? and `client_id` = ? and `heartbeat` = ?"; PreparedStatement s = c.prepareStatement(heartbeatUpdate); s.setLong(1, thisHeartbeat); s.setLong(2, serverID); s.setString(3, clientID); s.setLong(4, lastHeartbeat); LOGGER.debug("writing heartbeat: " + thisHeartbeat + " (last heartbeat written: " + lastHeartbeat + ")"); int nRows = s.executeUpdate(); if ( nRows != 1 ) { String msg = String.format( "Expected a heartbeat value of %d but didn't find it. Is another Maxwell process running with the same client_id?", lastHeartbeat ); throw new DuplicateProcessException(msg); } lastHeartbeat = thisHeartbeat; } public Position get() throws SQLException { try ( Connection c = connectionPool.getConnection() ) { PreparedStatement s = c.prepareStatement("SELECT * from `positions` where server_id = ? and client_id = ?"); s.setLong(1, serverID); s.setString(2, clientID); ResultSet rs = s.executeQuery(); if ( !rs.next() ) return null; String gtid = gtidMode ? rs.getString("gtid_set") : null; return new Position( new BinlogPosition(gtid, null, rs.getLong("binlog_position"), rs.getString("binlog_file") ), rs.getLong("last_heartbeat_read") ); } } /** * grabs a position from a different server_id */ public RecoveryInfo getRecoveryInfo(MaxwellConfig config) throws SQLException { try ( Connection c = connectionPool.getConnection() ) { return getRecoveryInfo(config, c); } } protected RecoveryInfo getRecoveryInfo(MaxwellConfig config, Connection c) throws SQLException { List<RecoveryInfo> recoveries = getAllRecoveryInfos(c); if (recoveries.size() == 1) { return recoveries.get(0); } else { for (String line: formatRecoveryFailure(config, recoveries)) { LOGGER.error(line); } return null; } } protected List<RecoveryInfo> getAllRecoveryInfos() throws SQLException { try ( Connection c = connectionPool.getConnection() ) { return getAllRecoveryInfos(c); } } protected List<RecoveryInfo> getAllRecoveryInfos(Connection c) throws SQLException { PreparedStatement s = c.prepareStatement("SELECT * from `positions` where client_id = ? order by last_heartbeat_read DESC"); s.setString(1, clientID); ResultSet rs = s.executeQuery(); ArrayList<RecoveryInfo> recoveries = new ArrayList<>(); while ( rs.next() ) { Long server_id = rs.getLong("server_id"); String gtid = gtidMode ? rs.getString("gtid_set") : null; Position position = new Position( BinlogPosition.at(gtid, rs.getLong("binlog_position"), rs.getString("binlog_file") ), rs.getLong("last_heartbeat_read")); if ( rs.wasNull() ) { LOGGER.warn("master recovery is ignoring position with NULL heartbeat"); } else { recoveries.add(new RecoveryInfo(position, server_id, clientID)); } } return recoveries; } protected List<String> formatRecoveryFailure(MaxwellConfig config, List<RecoveryInfo> recoveries) { if (recoveries.size() == 0) { return Collections.singletonList("Unable to find any binlog positions in `positions` table"); } ArrayList<String> result = new ArrayList<>(); Long mostRecentMaster = recoveries.get(0).serverID; result.add("Found multiple binlog positions for cluster in `positions` table. Not attempting position recovery."); result.add("Positions found (most recent heartbeat first):"); for (RecoveryInfo recovery : recoveries) { result.add(" - " + recovery); } result.add("Most likely the first is the most recent master, in which case you should:"); result.add("1. stop maxwell"); result.add("2. execute: DELETE FROM " + config.databaseName + ".positions WHERE server_id <> " + mostRecentMaster + ";"); result.add("3. restart maxwell"); return result; } public int delete(Long serverID, String clientID, Position position) throws SQLException { try ( Connection c = connectionPool.getConnection()) { PreparedStatement s = c.prepareStatement( "DELETE from `positions` where server_id = ? and client_id = ? and binlog_file = ? and binlog_position = ?" ); BinlogPosition binlogPosition = position.getBinlogPosition(); s.setLong(1, serverID); s.setString(2, clientID); s.setString(3, binlogPosition.getFile()); s.setLong(4, binlogPosition.getOffset()); s.execute(); return s.getUpdateCount(); } } }