package com.zendesk.maxwell.replication;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.zendesk.maxwell.metrics.MaxwellMetrics;
import com.zendesk.maxwell.MaxwellFilter;
import com.zendesk.maxwell.bootstrap.AbstractBootstrapper;
import com.zendesk.maxwell.producer.AbstractProducer;
import com.zendesk.maxwell.row.HeartbeatRowMap;
import com.zendesk.maxwell.row.RowMap;
import com.zendesk.maxwell.schema.SchemaStore;
import com.zendesk.maxwell.schema.ddl.DDLMap;
import com.zendesk.maxwell.schema.ddl.ResolvedSchemaChange;
import com.zendesk.maxwell.util.RunLoopProcess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.util.List;
import java.util.Objects;
public abstract class AbstractReplicator extends RunLoopProcess implements Replicator {
private static Logger LOGGER = LoggerFactory.getLogger(AbstractReplicator.class);
protected final String clientID;
protected final AbstractProducer producer;
protected final AbstractBootstrapper bootstrapper;
protected final String maxwellSchemaDatabaseName;
protected final TableCache tableCache = new TableCache();
protected Position lastHeartbeatPosition;
protected Long stopAtHeartbeat;
protected MaxwellFilter filter;
private final Counter rowCounter = MaxwellMetrics.metricRegistry.counter(
MetricRegistry.name(MaxwellMetrics.getMetricsPrefix(), "row", "count")
);
private final Meter rowMeter = MaxwellMetrics.metricRegistry.meter(
MetricRegistry.name(MaxwellMetrics.getMetricsPrefix(), "row", "meter")
);
protected Long replicationLag = 0L;
public AbstractReplicator(String clientID, AbstractBootstrapper bootstrapper, String maxwellSchemaDatabaseName, AbstractProducer producer, Position initialPosition) {
this.clientID = clientID;
this.bootstrapper = bootstrapper;
this.maxwellSchemaDatabaseName = maxwellSchemaDatabaseName;
this.producer = producer;
this.lastHeartbeatPosition = initialPosition;
}
/**
* Possibly convert a RowMap object into a HeartbeatRowMap
*
* Process a rowmap that represents a write to `maxwell`.`heartbeats`.
* If it's a write for a different client_id, we return the input (which
* will signify to the rest of the chain to ignore it). Otherwise, we
* transform it into a HeartbeatRowMap (which will not be output, but will
* advance the binlog position) and set `this.lastHeartbeatPosition`
*
* @return either a RowMap or a HeartbeatRowMap
*/
protected RowMap processHeartbeats(RowMap row) throws SQLException {
String hbClientID = (String) row.getData("client_id");
if ( !Objects.equals(hbClientID, this.clientID) )
return row; // plain row -- do not process.
long lastHeartbeatRead = (Long) row.getData("heartbeat");
LOGGER.debug("replicator picked up heartbeat: " + lastHeartbeatRead);
this.lastHeartbeatPosition = row.getPosition().withHeartbeat(lastHeartbeatRead);
return HeartbeatRowMap.valueOf(row.getDatabase(), this.lastHeartbeatPosition);
}
/**
* Parse a DDL statement and output the results to the producer
*
* @param dbName The database "context" under which the SQL is to be processed. think "use db; alter table foo ..."
* @param sql The DDL SQL to be processed
* @param schemaStore A SchemaStore object to which we delegate the parsing of the sql
* @param position The position that the SQL happened at
* @param timestamp The timestamp of the SQL binlog event
*/
protected void processQueryEvent(String dbName, String sql, SchemaStore schemaStore, Position position, Long timestamp) throws Exception {
List<ResolvedSchemaChange> changes = schemaStore.processSQL(sql, dbName, position);
for (ResolvedSchemaChange change : changes) {
if (change.shouldOutput(filter)) {
DDLMap ddl = new DDLMap(change, timestamp, sql, position);
producer.push(ddl);
}
}
tableCache.clear();
}
/**
* Should we output an event for the given database and table?
*
* Here we check against a whitelist/blacklist/filter. The whitelist
* passes updates to `maxwell.bootstrap` through (those are control
* mechanisms for bootstrap), the blacklist gets rid of the
* `ha_health_check` table which shows up erroneously in Alibaba RDS.
*
* @param database The database of the DML
* @param table The table of the DML
* @param filter A table-filter, or null
* @return Whether we should write the event to the producer
*/
protected boolean shouldOutputEvent(String database, String table, MaxwellFilter filter) {
Boolean isSystemWhitelisted = this.maxwellSchemaDatabaseName.equals(database)
&& "bootstrap".equals(table);
if ( MaxwellFilter.isSystemBlacklisted(database, table) )
return false;
else if ( isSystemWhitelisted)
return true;
else
return MaxwellFilter.matches(filter, database, table);
}
/**
* Get the last heartbeat that the replicator has processed.
*
* We pass along the value of the heartbeat to the producer inside the row map.
* @return the millisecond value ot the last heartbeat read
*/
public Long getLastHeartbeatRead() {
return lastHeartbeatPosition.getLastHeartbeatRead();
}
/**
* get a single row from the replicator and pass it to the producer or bootstrapper.
*
* This is the top-level function in the run-loop.
*/
public void work() throws Exception {
RowMap row = getRow();
rowCounter.inc();
rowMeter.mark();
if ( row == null )
return;
processRow(row);
}
public void stopAtHeartbeat(long heartbeat) {
stopAtHeartbeat = heartbeat;
}
protected void processRow(RowMap row) throws Exception {
if ( row instanceof HeartbeatRowMap) {
producer.push(row);
if (stopAtHeartbeat != null) {
long thisHeartbeat = row.getPosition().getLastHeartbeatRead();
if (thisHeartbeat >= stopAtHeartbeat) {
LOGGER.info("received final heartbeat " + thisHeartbeat + "; stopping replicator");
// terminate runLoop
this.taskState.stopped();
}
}
} else if (!bootstrapper.shouldSkip(row) && !isMaxwellRow(row))
producer.push(row);
else
bootstrapper.work(row, producer, this);
}
/**
* Is this RowMap an update to one of maxwell's own tables?
*
* If so we will often suppress the output.
* @param row The RowMap in question
* @return whether the update is something maxwell itself generated
*/
protected boolean isMaxwellRow(RowMap row) {
return row.getDatabase().equals(this.maxwellSchemaDatabaseName);
}
/**
* The main entry point into the event reading loop.
*
* We maintain a buffer of events in a transaction,
* and each subsequent call to `getRow` can grab one from
* the buffer. If that buffer is empty, we'll go check
* the open-replicator buffer for rows to process. If that
* buffer is empty, we return null.
*
* @return either a RowMap or null
*/
public abstract RowMap getRow() throws Exception;
public void setFilter(MaxwellFilter filter) {
this.filter = filter;
}
}