/*
* Copyright 2012-2015, the original author or authors.
* 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.
*/
package com.flipkart.aesop.bootstrap.mysql;
import com.flipkart.aesop.bootstrap.mysql.eventlistener.OpenReplicationListener;
import com.flipkart.aesop.bootstrap.mysql.eventprocessor.BinLogEventProcessor;
import com.flipkart.aesop.bootstrap.mysql.txnprocessor.MysqlTransactionManager;
import com.flipkart.aesop.bootstrap.mysql.txnprocessor.impl.MysqlTransactionManagerImpl;
import com.flipkart.aesop.event.AbstractEvent;
import com.flipkart.aesop.runtime.bootstrap.producer.BlockingEventProducer;
import com.google.code.or.OpenReplicator;
import com.linkedin.databus.core.util.InvalidConfigException;
import com.linkedin.databus2.relay.config.LogicalSourceStaticConfig;
import org.trpr.platform.core.impl.logging.LogFactory;
import org.trpr.platform.core.spi.logging.Logger;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <code>MysqlEventProducer</code> starts OpenReplicator bin log event listener to listen to MySQL events & registers an
* instance of {@link OpenReplicationListener} to process the events
* @author nrbafna
*/
public class MysqlEventProducer<T extends AbstractEvent> extends BlockingEventProducer
{
private static Long startTime = System.nanoTime();
/** Logger for this class */
private static final Logger LOGGER = LogFactory.getLogger(MysqlEventProducer.class);
/** Default Mysql port */
private static final Integer DEFAULT_MYSQL_PORT = 3306;
/** Pattern for extracting /3306/mysql-bin out of mysql://or_test%2For_test@localhost:3306/3306/mysql-bin */
private static final Pattern PATH_PATTERN = Pattern.compile("/([0-9]+)/[a-z|A-Z|0-9|-]+");
/** Index of server id in pattern match group */
private static final int SERVER_ID = 1;
/** Index of bin log prefix in pattern match group */
private static final int BIN_LOG_PREFIX = 2;
/** Open Replicator which listens and parses bin log events from Mysql */
protected OpenReplicator openReplicator;
/** Mysql transaction manager */
protected MysqlTransactionManager mysqlTxnManager;
/** Event Id to Event Processor map */
protected Map<Integer, BinLogEventProcessor> eventsMap;
@Override
public void start(long sinceSCN)
{
this.sinceSCN.set(sinceSCN);
openReplicator = new OpenReplicator();
String binlogFile;
try
{
String binlogFilePrefix = processUri(new URI(physicalSourceStaticConfig.getUri()));
int offset = offset(sinceSCN);
int logid = logid(sinceSCN);
LOGGER.info("SCN : " + offset + " logid : " + logid);
binlogFile = String.format("%s.%06d", binlogFilePrefix, logid);
LOGGER.info("Bin Log File Name : " + binlogFile);
Map<String, Short> tableUriToSrcIdMap = new HashMap<String, Short>();
Map<String, String> tableUriToSrcNameMap = new HashMap<String, String>();
for (LogicalSourceStaticConfig sourceConfig : physicalSourceStaticConfig.getSources())
{
tableUriToSrcIdMap.put(sourceConfig.getUri().toLowerCase(), sourceConfig.getId());
tableUriToSrcNameMap.put(sourceConfig.getUri().toLowerCase(), sourceConfig.getName());
}
mysqlTxnManager =
new MysqlTransactionManagerImpl<T>(logid,tableUriToSrcIdMap, tableUriToSrcNameMap, schemaRegistryService,
this,sourceEventConsumer);
mysqlTxnManager.setShutdownRequested(false);
OpenReplicationListener orl =
new OpenReplicationListener(mysqlTxnManager, eventsMap,
binlogFilePrefix);
openReplicator.setBinlogFileName(binlogFile);
openReplicator.setBinlogPosition(offset);
openReplicator.setBinlogEventListener(orl);
openReplicator.start();
}
catch (URISyntaxException u)
{
LOGGER.error("Exception occurred while processing uri : " + u);
return;
}
catch (InvalidConfigException e)
{
LOGGER.error("Exception occurred while processing uri : " + e);
return;
}
catch (Exception e)
{
LOGGER.error("Error occurred while starting open replication.." + e);
return;
}
LOGGER.info("Open Replicator has been started successfully for the file " + binlogFile);
}
/**
* Extracts individual attributes such as username, password, hostname, port ,server id etc from the uri of the
* format mysql://or_test%2For_test@localhost:3306/3306/mysql-bin
* @param uri uri of the format mysql://or_test%2For_test@localhost:3306/3306/mysql-bin
* @return returns bin log prefix
*/
protected String processUri(URI uri) throws InvalidConfigException
{
String userInfo = uri.getUserInfo();
if (null == userInfo)
{
String errorMessage = "missing user info in: " + uri;
LOGGER.error(errorMessage);
throw new InvalidConfigException(errorMessage);
}
int slashPos = userInfo.indexOf('/');
if (slashPos < 0)
{
slashPos = userInfo.length();
}
else if (0 == slashPos)
{
String errorMessage = "missing user name in user info: " + userInfo;
LOGGER.error(errorMessage);
throw new InvalidConfigException(errorMessage);
}
String userName = userInfo.substring(0, slashPos);
String userPass = slashPos < userInfo.length() - 1 ? userInfo.substring(slashPos + 1) : null;
String hostName = uri.getHost();
int port = uri.getPort();
if (port < 0)
port = DEFAULT_MYSQL_PORT;
String path = uri.getPath();
if (null == path)
{
String errorMessage = "missing path: " + uri;
LOGGER.error(errorMessage);
throw new InvalidConfigException(errorMessage);
}
Matcher matcher = PATH_PATTERN.matcher(path);
if (!matcher.matches())
{
String errorMessage = "invalid path:" + path;
LOGGER.error(errorMessage);
throw new InvalidConfigException(errorMessage);
}
String[] group = matcher.group().split("/");
if (group.length != 3)
{
String errorMessage = "Invalid format " + Arrays.toString(group);
LOGGER.error(errorMessage);
throw new InvalidConfigException(errorMessage);
}
String serverIdStr = group[SERVER_ID];
int serverId = -1;
try
{
serverId = Integer.parseInt(serverIdStr);
}
catch (NumberFormatException e)
{
String errorMessage = "incorrect mysql serverid:" + serverId;
LOGGER.error(errorMessage);
throw new InvalidConfigException(errorMessage);
}
/** Assign them to incoming variables */
if (null != openReplicator)
{
openReplicator.setUser(userName);
if (null != userPass)
{
openReplicator.setPassword(userPass);
}
openReplicator.setHost(hostName);
openReplicator.setPort(port);
openReplicator.setServerId(serverId);
}
LOGGER.debug("Extracted bin log prefix is " + group[BIN_LOG_PREFIX]);
return group[BIN_LOG_PREFIX];
}
/**
* Returns the logid ( upper 32 bits of the SCN )
* For e.g., mysql-bin.000001 is said to have an id 000001
* @param scn system change number
* @return logid
*/
public static int logid(long scn)
{
if (scn == -1 || scn == 0)
{
return 1;
}
return (int) ((scn >> 32) & 0xFFFFFFFF);
}
/**
* Returns the binlogoffset ( lower 32 bits of the SCN )
* @param scn system change number
* @return binlogoffset
*/
public static int offset(long scn)
{
if (scn == -1 || scn == 0)
{
return 4;
}
return (int) (scn & 0xFFFFFFFF);
}
/** Getters and Setters for this class */
public void setEventsMap(Map<Integer, BinLogEventProcessor> eventsMap){
this.eventsMap = eventsMap;
}
@Override
public String getName()
{
return this.getClass().getName();
}
@Override
public long getSCN()
{
return this.sinceSCN.get();
}
/**
* Setting Updated SCN into Metrics and producer
* @param latestScn
*/
public void updateSCN(long latestScn)
{
this.sinceSCN.set(latestScn);
this.metricsCollector.setProducerSCN(this.name,latestScn);
}
@Override
public boolean isRunning()
{
return this.openReplicator.isRunning();
}
@Override
public boolean isPaused()
{
return !this.openReplicator.isRunning();
}
@Override
public void unpause()
{
throw new UnsupportedOperationException("'unpause' is not supported on this event producer");
}
@Override
public void pause()
{
throw new UnsupportedOperationException("'unpause' is not supported on this event producer");
}
@Override
public void shutdown()
{
try
{
LOGGER.info("Shutdown has been requested. MYSQLEventProducer shutting down");
this.openReplicator.stop(5, TimeUnit.SECONDS);
LOGGER.info("### Bootstrap Process completed successfully ###");
LOGGER.info("Time Taken:" + (System.nanoTime() - startTime));
}
catch (Exception e)
{
LOGGER.error("Error while stopping mysql bootstrap", e);
}
}
@Override
public void waitForShutdown() throws InterruptedException, IllegalStateException
{
throw new UnsupportedOperationException("'waitForShutdown' is not supported on this event producer");
}
@Override
public void waitForShutdown(long l) throws InterruptedException, IllegalStateException
{
throw new UnsupportedOperationException("'waitForShutdown' is not supported on this event producer");
}
}