/**
* VMware Continuent Tungsten Replicator
* Copyright (C) 2015 VMware, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Initial developer(s): Robert Hodges
* Contributor(s): Stephane Giron
*/
package com.continuent.tungsten.replicator.heartbeat;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.log4j.Logger;
import com.continuent.tungsten.replicator.database.Column;
import com.continuent.tungsten.replicator.database.Database;
import com.continuent.tungsten.replicator.database.DatabaseFactory;
import com.continuent.tungsten.replicator.database.Key;
import com.continuent.tungsten.replicator.database.Table;
/**
* Provides a definition for a heartbeat table, which measures latency between
* master and slave. The heartbeat table is created with a single row that is
* then update to track changes. This class provides methods to update the
* table.
*
* @author <a href="mailto:robert.hodges@continuent.com">Robert Hodges</a>
* @version 1.0
*/
public class HeartbeatTable
{
private static Logger logger = Logger.getLogger(HeartbeatTable.class);
public static final String TABLE_NAME = "heartbeat";
public static final String STAGE_TABLE_NAME = "stage_xxx_heartbeat";
private static final long KEY = 1;
private static AtomicLong saltValue = new AtomicLong(0);
private Table hbTable;
private Table hbStageTable;
private Column hbTOpcode;
private Column hbTSeqno;
private Column hbTRowId;
private Column hbTCommitTstamp;;
private Column hbId;
private Column hbSeqno;
private Column hbEventId;
private Column hbSourceTstamp;
private Column hbTargetTstamp;
private Column hbLagMillis;
private Column hbSalt;
private Column hbName;
String sourceTsQuery = null;
private String tableType;
private String serviceName;
public HeartbeatTable(String schema, String tableType)
{
this.tableType = tableType;
initialize(schema);
initializeStage(schema);
}
public HeartbeatTable(String schema, String tableType, String serviceName)
{
this.tableType = tableType;
this.serviceName = serviceName;
initialize(schema);
initializeStage(schema);
}
private void initialize(String schema)
{
hbTable = new Table(schema, TABLE_NAME);
hbId = new Column("id", Types.BIGINT, true); // true => isNotNull
hbSeqno = new Column("seqno", Types.BIGINT);
hbEventId = new Column("eventid", Types.VARCHAR, 128);
hbSourceTstamp = new Column("source_tstamp", Types.TIMESTAMP);
hbTargetTstamp = new Column("target_tstamp", Types.TIMESTAMP);
hbLagMillis = new Column("lag_millis", Types.BIGINT);
hbSalt = new Column("salt", Types.BIGINT);
hbName = new Column("name", Types.VARCHAR, 128);
Key hbKey = new Key(Key.Primary);
hbKey.AddColumn(hbId);
hbTable.AddColumn(hbId);
hbTable.AddColumn(hbSeqno);
hbTable.AddColumn(hbEventId);
hbTable.AddColumn(hbSourceTstamp);
hbTable.AddColumn(hbTargetTstamp);
hbTable.AddColumn(hbLagMillis);
hbTable.AddColumn(hbSalt);
hbTable.AddColumn(hbName);
hbTable.AddKey(hbKey);
sourceTsQuery = "SELECT source_tstamp from " + schema + "." + TABLE_NAME
+ " where id=" + KEY;
}
private void initializeStage(String schema)
{
hbStageTable = new Table(schema, STAGE_TABLE_NAME);
hbTOpcode = new Column("tungsten_opcode", Types.VARCHAR, 2);
hbTSeqno = new Column("tungsten_seqno", Types.BIGINT, true);
hbTRowId = new Column("tungsten_row_id", Types.BIGINT, true);
hbTCommitTstamp = new Column("tungsten_commit_timestamp",
Types.TIMESTAMP);
Key hbKey = new Key(Key.Primary);
hbKey.AddColumn(hbTOpcode);
hbKey.AddColumn(hbTSeqno);
hbKey.AddColumn(hbTRowId);
hbStageTable.AddColumn(hbTOpcode);
hbStageTable.AddColumn(hbTSeqno);
hbStageTable.AddColumn(hbTRowId);
hbStageTable.AddColumn(hbTCommitTstamp);
hbStageTable.AddColumn(hbId);
hbStageTable.AddColumn(hbSeqno);
hbStageTable.AddColumn(hbEventId);
hbStageTable.AddColumn(hbSourceTstamp);
hbStageTable.AddColumn(hbTargetTstamp);
hbStageTable.AddColumn(hbLagMillis);
hbStageTable.AddColumn(hbSalt);
hbStageTable.AddColumn(hbName);
hbStageTable.AddKey(hbKey);
}
/**
* Returns metadata used to create the underlying heartbeat table.
*/
public Table getTable()
{
return hbTable;
}
/**
* Returns metadata used to create the underlying heartbeat table.
*/
public Table getStageTable()
{
return hbStageTable;
}
/**
* Set up the heartbeat table on the master.
*/
public void initializeHeartbeatTable(Database database) throws SQLException
{
if (logger.isDebugEnabled())
logger.debug("Initializing heartbeat table");
// Create the table if it does not exist.
if (database.findTungstenTable(hbTable.getSchema(), hbTable.getName()) == null)
{
database.createTable(this.hbTable, false, this.hbTable.getSchema(),
tableType, serviceName);
}
// Add an initial heartbeat value if needed
ResultSet res = null;
PreparedStatement hbRowCount = null;
int rows = 0;
try
{
hbRowCount = database.prepareStatement("SELECT count(*) from "
+ this.hbTable.getSchema() + "." + this.hbTable.getName());
res = hbRowCount.executeQuery();
if (res.next())
{
rows = res.getInt(1);
}
}
finally
{
if (res != null)
{
try
{
res.close();
}
catch (SQLException e)
{
}
}
if (hbRowCount != null)
{
try
{
hbRowCount.close();
}
catch (Exception e)
{
}
}
}
if (rows == 0)
{
hbId.setValue(KEY);
hbSourceTstamp.setValue(new Timestamp(System.currentTimeMillis()));
hbSalt.setValue(saltValue.getAndIncrement());
database.insert(hbTable);
}
}
public void initializeHeartbeatStageTable(Database database)
throws SQLException
{
if (logger.isDebugEnabled())
logger.debug("Initializing heartbeat staging table");
// Create the table if it does not exist.
if (database.findTable(hbStageTable.getSchema(),
hbStageTable.getName()) == null)
{
database.createTable(this.hbStageTable, false,
this.hbStageTable.getSchema(), tableType, serviceName);
}
}
/**
* Execute this call to start a named heartbeat on the master. The heartbeat
* table update must be logged as we will expect to see it as a DBMSEvent.
*/
public void startHeartbeat(Database database, String name)
throws SQLException
{
ArrayList<Column> whereClause = new ArrayList<Column>();
ArrayList<Column> values = new ArrayList<Column>();
Timestamp now = new Timestamp(System.currentTimeMillis());
if (logger.isDebugEnabled())
logger.debug("Processing master heartbeat update: name=" + name
+ " time=" + now);
hbId.setValue(KEY);
whereClause.add(hbId);
hbSourceTstamp.setValue(now);
hbSalt.setValue(saltValue.getAndIncrement());
hbName.setValue(name);
values.add(hbSourceTstamp);
values.add(hbSalt);
values.add(hbName);
database.update(hbTable, whereClause, values);
}
/**
* Wrapper for startHeartbeat() call.
*/
public void startHeartbeat(String url, String user, String password,
String name, String initScript) throws SQLException
{
Database db = null;
try
{
db = DatabaseFactory.createDatabase(url, user, password);
if (initScript != null)
db.setInitScript(initScript);
db.connect();
startHeartbeat(db, name);
}
finally
{
db.close();
}
}
/**
* Execute this call to fill in heartbeat data on the slave. This call must
* be invoked after a heartbeat event is applied.
*/
public void completeHeartbeat(Database database, long seqno, String eventId)
throws SQLException
{
if (logger.isDebugEnabled())
logger.debug("Processing slave heartbeat update");
Statement st = null;
ResultSet rs = null;
Timestamp sts = new Timestamp(0);
Timestamp now = new Timestamp(System.currentTimeMillis());
ArrayList<Column> whereClause = new ArrayList<Column>();
ArrayList<Column> values = new ArrayList<Column>();
if (logger.isDebugEnabled())
logger.debug("Processing slave heartbeat update: " + now);
// Get the source timestamp.
try
{
st = database.createStatement();
rs = st.executeQuery(sourceTsQuery);
if (rs.next())
sts = rs.getTimestamp(1);
}
finally
{
if (rs != null)
{
try
{
rs.close();
}
catch (SQLException e)
{
}
}
if (st != null)
{
try
{
st.close();
}
catch (SQLException e)
{
}
}
}
// Compute the difference between source and target.
long lag_millis = now.getTime() - sts.getTime();
// Update the heartbeat record with target time and difference.
hbId.setValue(KEY);
whereClause.add(hbId);
hbSeqno.setValue(seqno);
hbEventId.setValue(eventId);
hbTargetTstamp.setValue(now);
hbLagMillis.setValue(lag_millis);
values.add(hbSeqno);
values.add(hbEventId);
values.add(hbTargetTstamp);
values.add(hbLagMillis);
database.update(hbTable, whereClause, values);
}
/**
* Applies a heartbeat update on the slave. This call is designed for data
* warehouses that cannot apply a heartbeat using batch loading mechanisms.
*/
public void applyHeartbeat(Database database, Timestamp sourceTimestamp,
String name) throws SQLException
{
ArrayList<Column> whereClause = new ArrayList<Column>();
ArrayList<Column> values = new ArrayList<Column>();
if (logger.isDebugEnabled())
logger.debug("Applying heartbeat to slave: name=" + name
+ " sourceTstamp=" + sourceTimestamp);
hbId.setValue(KEY);
whereClause.add(hbId);
hbSourceTstamp.setValue(sourceTimestamp);
hbName.setValue(name);
values.add(hbSourceTstamp);
values.add(hbName);
database.update(hbTable, whereClause, values);
}
}