/*
* Copyright 1999-2012 Alibaba Group.
*
* 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.alibaba.cobar.mysql;
import java.sql.SQLSyntaxErrorException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.log4j.Logger;
import com.alibaba.cobar.config.Alarms;
import com.alibaba.cobar.config.model.DataNodeConfig;
import com.alibaba.cobar.config.util.ConfigException;
import com.alibaba.cobar.heartbeat.MySQLHeartbeat;
import com.alibaba.cobar.mysql.bio.Channel;
import com.alibaba.cobar.mysql.nio.MySQLConnectionPool;
import com.alibaba.cobar.mysql.nio.handler.ResponseHandler;
import com.alibaba.cobar.parser.ast.expression.primary.PlaceHolder;
import com.alibaba.cobar.parser.ast.stmt.SQLStatement;
import com.alibaba.cobar.parser.recognizer.SQLParserDelegate;
import com.alibaba.cobar.parser.visitor.EmptySQLASTVisitor;
import com.alibaba.cobar.parser.visitor.MySQLOutputASTVisitor;
import com.alibaba.cobar.util.TimeUtil;
/**
* @author xianmao.hexm 2011-4-26 上午11:12:24
*/
public final class MySQLDataNode {
private static final Logger LOGGER = Logger.getLogger(MySQLDataNode.class);
private static final Logger ALARM = Logger.getLogger("alarm");
private final String name;
private final DataNodeConfig config;
private MySQLDataSource[] sources;
private MySQLConnectionPool[] dataSources;
private int activedIndex;
private long executeCount;
private long heartbeatRecoveryTime;
private volatile boolean initSuccess;
private final ReentrantLock switchLock;
private SQLStatement heartbeatAST;// 动态心跳语句AST
private Map<PlaceHolder, Object> placeHolderToStringer;
public MySQLDataNode(DataNodeConfig config) {
this.name = config.getName();
this.config = config;
this.setHeartbeat(config.getHeartbeatSQL());
this.activedIndex = 0;
this.heartbeatRecoveryTime = -1L;
this.switchLock = new ReentrantLock();
}
public void init(int size, int index) {
if (!checkIndex(index)) {
index = 0;
}
int active = -1;
for (int i = 0; i < sources.length; i++) {
int j = loop(i + index);
if (initSource(sources[j], size)) {
active = j;
break;
}
}
if (checkIndex(active)) {
activedIndex = active;
initSuccess = true;
LOGGER.info(getMessage(active, " init success"));
} else {
initSuccess = false;
StringBuilder s = new StringBuilder();
s.append(Alarms.DEFAULT).append(name).append(" init failure");
ALARM.error(s.toString());
}
}
private void setHeartbeat(String heartbeat) {
if (heartbeat == null) {
heartbeatAST = null;
placeHolderToStringer = null;
return;
}
try {
final Set<PlaceHolder> plist = new HashSet<PlaceHolder>(1, 1);
SQLStatement ast = SQLParserDelegate.parse(heartbeat);
ast.accept(new EmptySQLASTVisitor() {
@Override
public void visit(PlaceHolder node) {
plist.add(node);
}
});
if (plist.isEmpty()) {
heartbeatAST = null;
placeHolderToStringer = null;
return;
}
Map<PlaceHolder, Object> phm = new HashMap<PlaceHolder, Object>(plist.size(), 1);
for (PlaceHolder ph : plist) {
final String content = ph.getName();
final int low = Integer.parseInt(content.substring(content.indexOf('(') + 1, content.indexOf(','))
.trim());
final int high = Integer.parseInt(content.substring(content.indexOf(',') + 1, content.indexOf(')'))
.trim());
phm.put(ph, new Object() {
private Random rnd = new Random();
@Override
public String toString() {
return String.valueOf(rnd.nextInt(high - low + 1) + low);
}
});
}
heartbeatAST = ast;
placeHolderToStringer = phm;
} catch (SQLSyntaxErrorException e) {
throw new ConfigException("heartbeat syntax err: " + heartbeat, e);
}
}
public String getHeartbeatSQL() {
if (heartbeatAST == null) {
return config.getHeartbeatSQL();
}
MySQLOutputASTVisitor sqlGen = new MySQLOutputASTVisitor(new StringBuilder());
sqlGen.setPlaceHolderToString(placeHolderToStringer);
heartbeatAST.accept(sqlGen);
return sqlGen.getSql();
}
public String getName() {
return name;
}
public DataNodeConfig getConfig() {
return config;
}
public long getExecuteCount() {
return executeCount;
}
public int getActivedIndex() {
return activedIndex;
}
public boolean isInitSuccess() {
return initSuccess;
}
public long getHeartbeatRecoveryTime() {
return heartbeatRecoveryTime;
}
public void setHeartbeatRecoveryTime(long time) {
this.heartbeatRecoveryTime = time;
}
public Channel getChannel() throws Exception {
return getChannel(activedIndex);
}
/**
* 取得数据源通道
*/
public Channel getChannel(int i) throws Exception {
if (initSuccess) {
Channel c = sources[i].getChannel();
++executeCount;
return c;
} else {
throw new IllegalArgumentException("Invalid DataSource:" + i);
}
}
public void getConnection(ResponseHandler handler, Object attachment) throws Exception {
getConnection(handler, attachment, activedIndex);
}
public void getConnection(ResponseHandler handler, Object attachment, int i) throws Exception {
if (initSuccess) {
MySQLConnectionPool pool = dataSources[i];
pool.getConnection(handler, attachment);
} else {
throw new IllegalArgumentException("Invalid DataSource:" + activedIndex);
}
}
public MySQLConnectionPool[] getDataSources() {
return dataSources;
}
public void setDataSources(MySQLConnectionPool[] dataSources) {
this.dataSources = dataSources;
}
public MySQLDataSource[] getSources() {
return sources;
}
public void setSources(MySQLDataSource[] sources) {
this.sources = sources;
}
public MySQLDataSource getSource() {
return sources[activedIndex];
}
/**
* 切换数据源
*/
public boolean switchSource(int newIndex, boolean isAlarm, String reason) {
if (!checkIndex(newIndex)) {
return false;
}
final ReentrantLock lock = this.switchLock;
lock.lock();
try {
int current = activedIndex;
if (current != newIndex) {
// 清理即将使用的数据源并开启心跳
sources[newIndex].clear();
sources[newIndex].startHeartbeat();
// 执行切换赋值
activedIndex = newIndex;
// 清理切换前的数据源
sources[current].clear();
sources[current].stopHeartbeat();
// 记录切换日志
if (isAlarm) {
ALARM.error(switchMessage(current, newIndex, true, reason));
} else {
LOGGER.warn(switchMessage(current, newIndex, false, reason));
}
return true;
}
} finally {
lock.unlock();
}
return false;
}
/**
* 空闲检查
*/
public void idleCheck() {
for (MySQLDataSource ds : sources) {
if (ds != null) {
ds.idleCheck(config.getIdleTimeout());
}
}
}
public MySQLHeartbeat getHeartbeat() {
MySQLDataSource source = this.getSource();
if (source != null) {
return source.getHeartbeat();
} else {
StringBuilder s = new StringBuilder();
s.append(Alarms.DEFAULT).append(name).append(" current dataSource is null!");
ALARM.error(s.toString());
return null;
}
}
public void startHeartbeat() {
MySQLDataSource source = this.getSource();
if (source != null) {
source.startHeartbeat();
} else {
StringBuilder s = new StringBuilder();
s.append(Alarms.DEFAULT).append(name).append(" current dataSource is null!");
ALARM.error(s.toString());
}
}
public void stopHeartbeat() {
MySQLDataSource source = this.getSource();
if (source != null) {
source.stopHeartbeat();
} else {
StringBuilder s = new StringBuilder();
s.append(Alarms.DEFAULT).append(name).append(" current dataSource is null!");
ALARM.error(s.toString());
}
}
public void doHeartbeat() {
// 判断是否需要执行心跳检查
if (!config.isNeedHeartbeat()) {
return;
}
// 检查内部是否有连接池配置信息
if (sources == null || sources.length == 0) {
return;
}
// 未到预定恢复时间,不执行心跳检测。
if (TimeUtil.currentTimeMillis() < heartbeatRecoveryTime) {
return;
}
// 准备执行心跳检测
MySQLDataSource source = this.getSource();
if (source != null) {
source.doHeartbeat();
} else {
StringBuilder s = new StringBuilder();
s.append(Alarms.DEFAULT).append(name).append(" current dataSource is null!");
ALARM.error(s.toString());
}
}
public int next(int i) {
if (checkIndex(i)) {
return (++i == sources.length) ? 0 : i;
} else {
return 0;
}
}
private int loop(int i) {
return i < sources.length ? i : (i - sources.length);
}
private boolean checkIndex(int i) {
return i >= 0 && i < sources.length;
}
private boolean initSource(MySQLDataSource ds, int size) {
boolean success = true;
Channel[] list = new Channel[size < ds.size() ? size : ds.size()];
for (int i = 0; i < list.length; i++) {
try {
list[i] = ds.getChannel();
} catch (Exception e) {
success = false;
LOGGER.warn(getMessage(ds.getIndex(), " init error."), e);
break;
}
}
for (Channel c : list) {
if (c == null) {
continue;
}
if (success) {
c.release();
} else {
c.close();
}
}
return success;
}
private String getMessage(int index, String info) {
return new StringBuilder().append(name).append(':').append(index).append(info).toString();
}
private String switchMessage(int current, int newIndex, boolean alarm, String reason) {
StringBuilder s = new StringBuilder();
if (alarm) {
s.append(Alarms.DATANODE_SWITCH);
}
s.append("[name=").append(name).append(",result=[").append(current).append("->");
s.append(newIndex).append("],reason=").append(reason).append(']');
return s.toString();
}
}