/*****************************************************************************
* Copyright (C) 2008 EnterpriseDB Corporation.
* Copyright (C) 2011 Stado Global Development Group.
*
* This file is part of Stado.
*
* Stado is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Stado is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Stado. If not, see <http://www.gnu.org/licenses/>.
*
* You can find Stado at http://www.stado.us
*
****************************************************************************/
/*
*
*/
package org.postgresql.stado.engine;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Level;
import org.postgresql.stado.common.util.XLogger;
import org.postgresql.stado.communication.message.NodeMessage;
import org.postgresql.stado.exception.XDBServerException;
import org.postgresql.stado.metadata.DBNode;
import org.postgresql.stado.metadata.IMetaDataUpdate;
import org.postgresql.stado.metadata.MetaData;
import org.postgresql.stado.metadata.SysDatabase;
import org.postgresql.stado.optimizer.SqlExpression;
import org.postgresql.stado.planner.QueryPlan;
// This is the start of a class that will enclose the
// various parts of the system and glue them together
public class Engine {
private static final XLogger logger = XLogger.getLogger(Engine.class);
private static final Engine engine = new Engine();
/**
*
* @return
*/
public static final Engine getInstance() {
return engine;
}
// ----------------------------------------------------------------
private Engine() {
// logger.throwing(new XDBServerException("Engine is created"));
}
/**
* Begins a user-defined transaction
*
* @param client
* @param nodeList
* @throws org.postgresql.stado.exception.XDBServerException
*/
public void beginTransaction(XDBSessionContext client,
Collection<DBNode> nodeList) throws XDBServerException {
final String method = "beginTransaction";
logger.entering(method, new Object[] { client });
try {
client.setInTransaction(true);
} finally {
logger.exiting(method);
}
}
/**
* Begins a user-defined transaction
*
* @param client
* @param nodeList
* @throws org.postgresql.stado.exception.XDBServerException
*/
public void commitTransaction(XDBSessionContext client,
Collection<DBNode> nodeList) throws XDBServerException {
final String method = "commitTransaction";
logger.entering(method, new Object[] { client });
try {
client.getSysDatabase();
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList);
aMultinodeExecutor.commit();
} finally {
logger.exiting(method);
}
}
/**
* Begins a user-defined transaction
*
* @param client
* @param nodeList
* @throws java.sql.SQLException
* @throws org.postgresql.stado.exception.XDBServerException
*/
public void rollbackTransaction(XDBSessionContext client,
Collection<DBNode> nodeList) throws SQLException,
XDBServerException {
final String method = "rollbackTransaction";
logger.entering(method, new Object[] { client });
try {
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList);
aMultinodeExecutor.rollback();
} finally {
logger.exiting(method);
}
}
/**
*
* @param dropList
* @param nodeList
* @param client
* @throws java.lang.Exception
*/
public void dropNodeTempTables(Collection<String> dropList,
Collection<DBNode> nodeList, XDBSessionContext client)
throws Exception {
final String method = "dropNodeTempTables";
logger.entering(method, new Object[] { dropList, nodeList, client });
try {
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList);
aMultinodeExecutor.dropNodeTempTables(dropList);
} finally {
logger.exiting(method);
}
}
/**
*
* @param tableName
* @param address
* @param nodeList
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
*/
public void startLoaders(String tableName, String address,
Collection<DBNode> nodeList, XDBSessionContext client)
throws XDBServerException {
final String method = "startLoaders";
logger.entering(method, new Object[] { client });
try {
SysDatabase database = client.getSysDatabase();
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList == null ? new ArrayList<DBNode>(
database.getDBNodeList())
: nodeList);
aMultinodeExecutor.startLoaders(tableName, address);
} finally {
logger.exiting(method);
}
}
/**
*
* @param sqlCommand
* @param nodeList
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
* @return
*/
public Map<Integer, ResultSet> executeQueryOnMultipleNodes(
String sqlCommand, Collection<DBNode> nodeList,
XDBSessionContext client) throws XDBServerException {
final String method = "executeSingleRowQueryOnMultipleNodes";
logger.entering(method, new Object[] { sqlCommand, nodeList, client });
try {
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList);
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Executing query %0% on Nodes %1%", new Object[] {
sqlCommand, nodeList });
Map<Integer, ResultSet> results = new HashMap<Integer, ResultSet>();
for (NodeMessage aMessage : aMultinodeExecutor.execute(sqlCommand,
nodeList, false)) {
results.put(aMessage.getSourceNodeID(), aMessage.getResultSet());
}
return results;
} finally {
logger.exiting(method);
}
}
// ----------------------------------------------------------------
// This is a generic method to execute a command on multiple nodes.
// It assumes that there is a single SQL statement as part of the
// "transaction.".
//
// If/when we write something to allow transactions
// over multiple commands, this can be readily adopted.
// ----------------------------------------------------------------
/**
*
* @param sqlCommand
* @param nodeList
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
* @return
*/
public int executeOnMultipleNodes(String sqlCommand,
Collection<DBNode> nodeList, XDBSessionContext client)
throws XDBServerException {
final String method = "executeOnMultipleNodes";
logger.entering(method, new Object[] { sqlCommand, nodeList, client });
int numRowsAffected = 0;
try {
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList);
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Executing command %0% on Nodes %1%", new Object[] {
sqlCommand, nodeList });
// amart:
// if we are running the command on single node in autocommit mode
// do autocommit immediately
numRowsAffected = aMultinodeExecutor.executeCommand(sqlCommand,
nodeList, nodeList.size() == 1 && !client.isInTransaction()
&& !client.isInSubTransaction());
return numRowsAffected;
} finally {
logger.exiting(method, new Integer(numRowsAffected));
}
}
// ----------------------------------------------------------------
// This is a generic method to execute a command on multiple nodes.
// It assumes that there is a single SQL statement as part of the
// "transaction.".
//
// If/when we write something to allow transactions
// over multiple commands, this can be readily adopted.
// ----------------------------------------------------------------
/**
*
* @param commands
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
* @return
*/
public int executeOnMultipleNodes(Map<DBNode, String> commands,
XDBSessionContext client) throws XDBServerException {
final String method = "executeOnMultipleNodes";
logger.entering(method, new Object[] { commands, client });
int numRowsAffected = 0;
try {
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(commands.keySet());
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Executing commands %0%", new Object[] { commands });
// amart:
// if we are running the command on single node in autocommit mode
// do autocommit immediately
numRowsAffected = aMultinodeExecutor.executeCommand(commands,
commands.size() == 1 && !client.isInTransaction()
&& !client.isInSubTransaction());
return numRowsAffected;
} finally {
logger.exiting(method, new Integer(numRowsAffected));
}
}
/**
* executeDDLOnMultipleNodes()
*
* This method executes the DDL on multiple nodes and also updates the
* MetaData DB. It also refreshes the MetaData cache.
*
* NOTES - The following plan is used to update the databases Phase 1 Begin
* Transaction on user db Execute statements on user db
*
* Phase 2 Begin Transaction for MetaData db Execute statements for MetaData
* db update ( by calling updater.execute() )
*
* Phase 3 If all is well, Commit on user db
*
* Phase 4 Commit on MetaData db ( will also refresh the cache by calling
* updater.refresh())
*
* @param sqlCommand
* @param nodeList
* @param updater
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
*/
public void executeDDLOnMultipleNodes(String sqlCommand,
Collection<DBNode> nodeList, IMetaDataUpdate updater,
XDBSessionContext client) throws XDBServerException {
final String method = "executeDDLOnMultipleNodes";
logger.entering(method, new Object[] { sqlCommand, nodeList, updater,
client });
try {
String commands[] = new String[1];
commands[0] = sqlCommand;
executeDDLOnMultipleNodes(commands, nodeList, updater, client);
} finally {
logger.exiting(method);
}
}
/**
*
* @param sqlCommands
* @param nodeList
* @param updater
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
*/
public void executeDDLOnMultipleNodes(String sqlCommands[],
Collection<DBNode> nodeList, IMetaDataUpdate updater,
XDBSessionContext client) throws XDBServerException {
final String method = "executeDDLOnMultipleNodes";
logger.entering(method, new Object[] { sqlCommands, nodeList, updater,
client });
try {
String sqlCommand = "";
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList);
beginTransaction(client, nodeList);
try {
for (String element : sqlCommands) {
if (element != null) {
sqlCommand = element;
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Executing DDL command %0% on Nodes %1%",
new Object[] { sqlCommand, nodeList });
aMultinodeExecutor.executeCommand(sqlCommand, nodeList);
}
}
} catch (XDBServerException e) {
aMultinodeExecutor.rollback();
throw e;
}
doMetadataUpdate(aMultinodeExecutor, updater, client);
} finally {
logger.exiting(method);
}
} // end executeDDLOnMultipleNodes()
/**
*
* @param sqlCommands
* @param nodeList
* @param updater
* @param client
* @throws com.edb.gridsql.exception.XDBServerException
*/
public Map<Integer, ResultSet> executeDDLFunctionOnMultipleNodes(String sqlCommand,
Collection<DBNode> nodeList, IMetaDataUpdate updater,
XDBSessionContext client) throws XDBServerException {
final String method = "executeDDLFunctionOnMultipleNodes";
logger.entering(method, new Object[] { sqlCommand, nodeList, updater,
client });
try {
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList);
Map<Integer, ResultSet> results = new HashMap<Integer, ResultSet>();
beginTransaction(client, nodeList);
try {
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Executing query %0% on Nodes %1%", new Object[] {
sqlCommand, nodeList });
for (NodeMessage aMessage : aMultinodeExecutor.execute(sqlCommand,
nodeList, false)) {
results.put(aMessage.getSourceNodeID(), aMessage.getResultSet());
}
} catch (XDBServerException e) {
aMultinodeExecutor.rollback();
throw e;
}
doMetadataUpdate(aMultinodeExecutor, updater, client);
return results;
} finally {
logger.exiting(method);
}
} // end executeDDLOnMultipleNodes()
/**
* A kind of two-phase commit to ensure consistency.
* Update the metadata database, if succeeded commit on nodes, then commit metadata changes.
* This method is synchronized, to avoid concurrency issues
*
* @param aMultinodeExecutor
* @param updater
* @param client
*/
synchronized private void doMetadataUpdate(MultinodeExecutor aMultinodeExecutor,
IMetaDataUpdate updater, XDBSessionContext client) {
// phase 2
try {
MetaData.getMetaData().beginTransaction();
updater.execute(client);
} catch (Exception e) {
try {
aMultinodeExecutor.rollback();
} catch (Exception e2) {
}
try {
MetaData.getMetaData().rollbackTransaction();
} catch (Exception e4) {
}
if (e instanceof XDBServerException) {
throw (XDBServerException) e;
} else {
throw new XDBServerException(e.getMessage(), e);
}
}
// phase 3
try {
aMultinodeExecutor.commit();
} catch (XDBServerException e) {
// rollback MetaData DB
try {
MetaData.getMetaData().rollbackTransaction();
} catch (Exception e4) {
}
throw e;
}
// phase 4
try {
MetaData.getMetaData().commitTransaction(updater);
} catch (XDBServerException e) {
try {
MetaData.getMetaData().rollbackTransaction();
} catch (Exception e4) {
}
throw e;
}
}
/**
* @param statements
* @param updater
* @param client
*/
public void executeDDLOnMultipleNodes(Map<DBNode, String> statements,
IMetaDataUpdate updater, XDBSessionContext client) {
final String method = "executeOnMultipleNodes";
logger.entering(method, new Object[] { statements, updater, client });
int numRowsAffected = 0;
try {
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(statements.keySet());
beginTransaction(client, statements.keySet());
try {
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Executing commands %0%", new Object[] { statements });
aMultinodeExecutor.executeCommand(statements);
} catch (XDBServerException e) {
aMultinodeExecutor.rollback();
throw e;
}
doMetadataUpdate(aMultinodeExecutor, updater, client);
} finally {
logger.exiting(method, new Integer(numRowsAffected));
}
}
/**
*
* @param statements
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
* @return
*/
public boolean addToBatchOnNodes(Map<DBNode, String> statements,
XDBSessionContext client) throws XDBServerException {
// This line makes client persistent
client.setInBatch(true);
MultinodeExecutor aExecutor = client.getMultinodeExecutor(statements.keySet());
return aExecutor.addBatchOnNodeList(statements);
}
/**
*
* @param statement
* @param nodeList
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
* @return
*/
public boolean addToBatchOnNodes(String statement,
Collection<DBNode> nodeList, XDBSessionContext client)
throws XDBServerException {
// This line makes client persistent
client.setInBatch(true);
MultinodeExecutor aExecutor = client.getMultinodeExecutor(nodeList);
return aExecutor.addBatchOnNodeList(statement, nodeList);
}
/**
*
* @param statement
* @param node
* @param client
* @throws org.postgresql.stado.exception.XDBServerException
* @return
*/
public boolean addToBatchOnNode(String statement, DBNode node,
XDBSessionContext client) throws XDBServerException {
// This line makes client persistent
client.setInBatch(true);
MultinodeExecutor aExecutor = client.getMultinodeExecutor(Collections.singleton(node));
return aExecutor.addBatchOnNode(statement, node.getNodeId());
}
/**
*
* @param client
* @param strict
* @return
*/
public int[] executeBatchOnNodes(XDBSessionContext client, boolean strict) {
// If addToBatchOnNodes() was called client is persistent and does not
// release
// NodeThreads, just returns latest ME
List<DBNode> emptyNodeList = Collections.emptyList();
MultinodeExecutor aExecutor = client.getMultinodeExecutor(emptyNodeList);
int[] result = aExecutor.executeBatch(strict);
client.setInBatch(false);
return result;
}
/**
*
* @param client
*/
public void clearBatchOnNodes(XDBSessionContext client) {
// If addToBatchOnNodes() was called client is persistent and does not
// release
// NodeThreads, just returns latest ME
List<DBNode> emptyNodeList = Collections.emptyList();
MultinodeExecutor aExecutor = client.getMultinodeExecutor(emptyNodeList);
aExecutor.clearBatch();
client.setInBatch(false);
}
/**
*
* @param commandID
* @param sqlCommand
* @param nodeList
* @param client
* @return
* @throws XDBServerException
*/
public String prepareStatement(String commandID, String sqlCommand,
int[] paramTypes, Collection<DBNode> nodeList,
XDBSessionContext client) throws XDBServerException {
final String method = "prepareStatement";
logger.entering(method, new Object[] { commandID, sqlCommand, nodeList,
client });
int numRowsAffected = 0;
try {
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(nodeList);
commandID = aMultinodeExecutor.prepareStatement(commandID,
sqlCommand, paramTypes, nodeList);
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Creating prepared statement %0%",
new Object[] { commandID });
return commandID;
} finally {
logger.exiting(method, new Integer(numRowsAffected));
}
}
/**
*
* @param commandID
* @param parameters
* @param client
* @return
* @throws XDBServerException
*/
public int executePreparedCommand(String commandID, String[] parameters,
Collection<DBNode> nodeList, XDBSessionContext client)
throws XDBServerException {
final String method = "executePreparedStatemnt";
logger.entering(method, new Object[] { commandID, parameters, client });
int numRowsAffected = 0;
try {
Collection<DBNode> empty = Collections.emptySet();
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(empty);
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Executing prepared statement %0%",
new Object[] { commandID });
numRowsAffected = aMultinodeExecutor.executePreparedCommand(
commandID, parameters, nodeList);
return numRowsAffected;
} finally {
logger.exiting(method, new Integer(numRowsAffected));
}
}
public void closePreparedStatement(String commandID,
XDBSessionContext client) throws XDBServerException {
final String method = "closePreparedStatement";
logger.entering(method, new Object[] { commandID, client });
int numRowsAffected = 0;
try {
Collection<DBNode> empty = Collections.emptySet();
MultinodeExecutor aMultinodeExecutor = client.getMultinodeExecutor(empty);
QueryPlan.CATEGORY_QUERYFLOW.log(Level.DEBUG,
"Closing prepared statement %0%",
new Object[] { commandID });
aMultinodeExecutor.closePrepared(commandID);
} finally {
logger.exiting(method, new Integer(numRowsAffected));
}
}
/**
* Evaluates SqlExpression using Coordinator connection.
* Returns result as a constant SqlExpression
* A query like SELECT <the_expression> is build and executed on Coordinator
* connection
* Only works if backend supports SELECT without FROM clause.
* Execution will fail if the expression contain column reference
* Currently used to evaluate and normalize partitioning expression
* May be improved in future to leave off coordinator connection. Tasks &
* issues to target:
* 1. Federated database. Evaluation result may depend on backend type and
* may cause partitioning errors. Complete internal evaluator will target
* this issue.
* 2. Do better job analyzing the expression. This function may partly
* calculate (reduce) the expression to speed up backend query and return
* reduced Expression (or original if not reduceable) instead of throwing.
* 3. Loading. Current approach is too slow to use when loading data.
* @param client Session object (to get coordinator connection)
* @param expr SqlExpression to evaluate
* @return Constant SqlExpression
* @throws SQLException if evaluation failed
*/
public SqlExpression evaluate(XDBSessionContext client, SqlExpression expr)
throws SQLException {
if (expr == null || expr.getExprType() == SqlExpression.SQLEX_CONSTANT) {
return expr;
}
Connection oConn = client.getAndSetCoordinatorConnection();
Statement stmt = oConn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT " + expr.rebuildString());
try {
rs.next();
String value = rs.getString(1);
SqlExpression constExpr = new SqlExpression(expr.getBelongsToTree());
constExpr.setExprType(SqlExpression.SQLEX_CONSTANT);
constExpr.setExprDataType(expr.getExprDataType());
constExpr.setConstantValue(value);
return constExpr;
} finally {
rs.close();
}
}
/**
* Evaluates the SQL expressions using Coordinator connection.
* Only works if backend supports SELECT without FROM clause.
* Execution will fail if any the expression contains column reference
* @see #evaluate(XDBSessionContext, SqlExpression)
* @param client Session object (to get coordinator connection)
* @param expr SQL expression to evaluate
* @return SQL literal
* @throws SQLException if evaluation failed
*/
public String evaluate(XDBSessionContext client, String expr)
throws SQLException {
Connection oConn = client.getAndSetCoordinatorConnection();
Statement stmt = oConn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT " + expr);
try {
rs.next();
return rs.getString(1);
} finally {
rs.close();
}
}
}