/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.compiler;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.voltcore.logging.VoltLogger;
import org.voltcore.messaging.HostMessenger;
import org.voltcore.messaging.LocalObjectMessage;
import org.voltcore.messaging.Mailbox;
import org.voltcore.messaging.VoltMessage;
import org.voltcore.utils.CoreUtils;
import org.voltdb.CatalogContext;
import org.voltdb.ClientInterface.ExplainMode;
import org.voltdb.OperationMode;
import org.voltdb.VoltDB;
import org.voltdb.VoltType;
import org.voltdb.client.ClientResponse;
import org.voltdb.licensetool.LicenseApi;
import org.voltdb.messaging.LocalMailbox;
import org.voltdb.parser.SQLLexer;
import org.voltdb.planner.StatementPartitioning;
import org.voltdb.utils.MiscUtils;
import com.google_voltpatches.common.util.concurrent.ListeningExecutorService;
public class AsyncCompilerAgent {
private static final VoltLogger hostLog = new VoltLogger("HOST");
private static final VoltLogger adhocLog = new VoltLogger("ADHOC");
// if more than this amount of work is queued, reject new work
static public final int MAX_QUEUE_DEPTH = 250;
// accept work via this mailbox
Mailbox m_mailbox;
public AsyncCompilerAgent(LicenseApi licenseApi) {
m_helper = new AsyncCompilerAgentHelper(licenseApi);
}
// The helper for catalog updates, back after its exclusive three year tour
// of Europe, Scandinavia, and the sub-continent.
final AsyncCompilerAgentHelper m_helper;
// do work in this executor service
final ListeningExecutorService m_es =
CoreUtils.getBoundedSingleThreadExecutor("Ad Hoc Planner", MAX_QUEUE_DEPTH);
// Enable debug hooks when the "asynccompilerdebug" sys prop is set to "true" or "yes".
private final static MiscUtils.BooleanSystemProperty DEBUG_MODE =
new MiscUtils.BooleanSystemProperty("asynccompilerdebug");
// When DEBUG_MODE is true this (valid) DDL string triggers an exception.
// Public visibility allows it to be used from a unit test.
public final static String DEBUG_EXCEPTION_DDL =
"create table DEBUG_MODE_ENG_7653_crash_me_now (die varchar(7654) not null)";
// intended for integration test use. finish planning what's in
// the queue and terminate the TPE.
public void shutdown() throws InterruptedException {
if (m_es != null) {
m_es.shutdown();
m_es.awaitTermination(120, TimeUnit.SECONDS);
}
}
public void createMailbox(final HostMessenger hostMessenger, final long hsId) {
m_mailbox = new LocalMailbox(hostMessenger) {
@Override
public void send(long destinationHSId, VoltMessage message) {
message.m_sourceHSId = hsId;
hostMessenger.send(destinationHSId, message);
}
@Override
public void deliver(final VoltMessage message) {
try {
m_es.submit(new Runnable() {
@Override
public void run() {
handleMailboxMessage(message);
}
});
} catch (RejectedExecutionException rejected) {
final LocalObjectMessage wrapper = (LocalObjectMessage)message;
AsyncCompilerWork work = (AsyncCompilerWork)(wrapper.payload);
generateErrorResult("Ad Hoc Planner task queue is full. Try again.", work);
}
}
};
hostMessenger.createMailbox(hsId, m_mailbox);
}
void generateErrorResult(String errorMsg, AsyncCompilerWork work) {
AsyncCompilerResult retval = new AsyncCompilerResult();
retval.clientHandle = work.clientHandle;
retval.errorMsg = errorMsg;
retval.connectionId = work.connectionId;
retval.hostname = work.hostname;
retval.adminConnection = work.adminConnection;
retval.clientData = work.clientData;
work.completionHandler.onCompletion(retval);
}
void handleMailboxMessage(final VoltMessage message) {
final LocalObjectMessage wrapper = (LocalObjectMessage)message;
if (wrapper.payload instanceof AsyncCompilerWork) {
AsyncCompilerWork compilerWork = (AsyncCompilerWork)wrapper.payload;
// Don't let exceptions escape
try {
if (compilerWork instanceof AdHocPlannerWork) {
handleAdHocPlannerWork((AdHocPlannerWork)(compilerWork));
}
else if (compilerWork instanceof CatalogChangeWork) {
handleCatalogChangeWork((CatalogChangeWork)(compilerWork));
}
else {
// Definitely shouldn't happen since we should be handling all possible
// AsyncCompilerWork derivative classes above.
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(compilerWork,
String.format("Unexpected compiler work class: %s %s: %s",
compilerWork.getClass().getName(),
"Please contact VoltDB support with this message and the contents:",
message.toString()));
compilerWork.completionHandler.onCompletion(errResult);
}
}
catch (RuntimeException e) {
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(compilerWork,
String.format("Unexpected async compiler exception for %s: %s: %s: %s",
compilerWork.getClass().getName(),
e.getLocalizedMessage(),
"Please contact VoltDB support with this message and the contents:",
message.toString()));
compilerWork.completionHandler.onCompletion(errResult);
}
}
else {
hostLog.error("Unexpected message received by AsyncCompilerAgent. " +
"Please contact VoltDB support with this message and the contents: " +
message.toString());
}
}
void handleAdHocPlannerWork(final AdHocPlannerWork w) {
// do initial naive scan of statements for DDL, forbid mixed DDL and (DML|DQL)
Boolean hasDDL = null;
// conflictTables tracks dropped tables before removing the ones that don't have CREATEs.
SortedSet<String> conflictTables = new TreeSet<String>();
Set<String> createdTables = new HashSet<String>();
for (String stmt : w.sqlStatements) {
// Simulate an unhandled exception? (ENG-7653)
if (DEBUG_MODE.isTrue() && stmt.equals(DEBUG_EXCEPTION_DDL)) {
throw new IndexOutOfBoundsException(DEBUG_EXCEPTION_DDL);
}
if (SQLLexer.isComment(stmt) || stmt.trim().isEmpty()) {
continue;
}
String ddlToken = SQLLexer.extractDDLToken(stmt);
if (hasDDL == null) {
hasDDL = (ddlToken != null) ? true : false;
}
else if ((hasDDL && ddlToken == null) || (!hasDDL && ddlToken != null))
{
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w,
"DDL mixed with DML and queries is unsupported.");
// No mixing DDL and DML/DQL. Turn this into an error returned to client.
w.completionHandler.onCompletion(errResult);
return;
}
// do a couple of additional checks if it's DDL
if (hasDDL) {
// check that the DDL is allowed
String rejectionExplanation = SQLLexer.checkPermitted(stmt);
if (rejectionExplanation != null) {
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w, rejectionExplanation);
w.completionHandler.onCompletion(errResult);
return;
}
// make sure not to mix drop and create in the same batch for the same table
if (ddlToken.equals("drop")) {
String tableName = SQLLexer.extractDDLTableName(stmt);
if (tableName != null) {
conflictTables.add(tableName);
}
}
else if (ddlToken.equals("create")) {
String tableName = SQLLexer.extractDDLTableName(stmt);
if (tableName != null) {
createdTables.add(tableName);
}
}
}
}
if (hasDDL == null) {
// we saw neither DDL or DQL/DML. Make sure that we get a
// response back to the client
if (w.invocationName.equals("@SwapTables")) {
final AsyncCompilerResult result = compileSysProcPlan(w);
w.completionHandler.onCompletion(result);
return;
}
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w,
"Failed to plan, no SQL statement provided.");
w.completionHandler.onCompletion(errResult);
return;
}
else if (!hasDDL) {
final AsyncCompilerResult result = compileAdHocPlan(w);
w.completionHandler.onCompletion(result);
}
else {
// We have adhoc DDL. Is it okay to run it?
// check for conflicting DDL create/drop table statements.
// unhappy if the intersection is empty
conflictTables.retainAll(createdTables);
if (!conflictTables.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("AdHoc DDL contains both DROP and CREATE statements for the following table(s):");
for (String tableName : conflictTables) {
sb.append(" ");
sb.append(tableName);
}
sb.append("\nYou cannot DROP and ADD a table with the same name in a single batch "
+ "(via @AdHoc). Issue the DROP and ADD statements as separate commands.");
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w, sb.toString());
w.completionHandler.onCompletion(errResult);
return;
}
// Is it forbidden by the replication role and configured schema change method?
// master and UAC method chosen:
if (!w.useAdhocDDL) {
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w,
"Cluster is configured to use @UpdateApplicationCatalog " +
"to change application schema. AdHoc DDL is forbidden.");
w.completionHandler.onCompletion(errResult);
return;
}
if (!allowPausedModeWork(w)) {
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w,
"Server is paused and is available in read-only mode - please try again later.",
ClientResponse.SERVER_UNAVAILABLE);
w.completionHandler.onCompletion(errResult);
return;
}
final CatalogChangeWork ccw = new CatalogChangeWork(w);
dispatchCatalogChangeWork(ccw);
}
}
private boolean allowPausedModeWork(AsyncCompilerWork w) {
return (VoltDB.instance().getMode() != OperationMode.PAUSED ||
w.isServerInitiated() ||
w.adminConnection);
}
void handleCatalogChangeWork(final CatalogChangeWork w) {
if (!allowPausedModeWork(w)) {
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w,
"Server is paused and is available in read-only mode - please try again later.",
ClientResponse.SERVER_UNAVAILABLE);
w.completionHandler.onCompletion(errResult);
return;
}
// We have an @UAC. Is it okay to run it?
// If we weren't provided operationBytes, it's a deployment-only change and okay to take
// master and adhoc DDL method chosen
if (w.invocationName.equals("@UpdateApplicationCatalog") &&
w.operationBytes != null && w.useAdhocDDL)
{
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w,
"Cluster is configured to use AdHoc DDL to change application " +
"schema. Use of @UpdateApplicationCatalog is forbidden.");
w.completionHandler.onCompletion(errResult);
return;
}
else if (w.invocationName.equals("@UpdateClasses") && !w.useAdhocDDL) {
AsyncCompilerResult errResult =
AsyncCompilerResult.makeErrorResult(w,
"Cluster is configured to use @UpdateApplicationCatalog " +
"to change application schema. Use of @UpdateClasses is forbidden.");
w.completionHandler.onCompletion(errResult);
return;
}
dispatchCatalogChangeWork(w);
}
public void compileAdHocPlanForProcedure(final AdHocPlannerWork apw) {
m_es.submit(new Runnable() {
@Override
public void run(){
apw.completionHandler.onCompletion(compileAdHocPlan(apw));
}
});
}
private void dispatchCatalogChangeWork(CatalogChangeWork work)
{
final CatalogChangeResult ccr = m_helper.prepareApplicationCatalogDiff(work);
if (ccr.errorMsg != null) {
hostLog.info("A request to update the database catalog and/or deployment settings has been rejected. More info returned to client.");
}
// Log something useful about catalog upgrades when they occur.
if (ccr.upgradedFromVersion != null) {
hostLog.info(String.format("In order to update the application catalog it was "
+ "automatically upgraded from version %s.",
ccr.upgradedFromVersion));
}
work.completionHandler.onCompletion(ccr);
}
public static final String AdHocErrorResponseMessage =
"The @AdHoc stored procedure when called with more than one parameter "
+ "must be passed a single parameterized SQL statement as its first parameter. "
+ "Pass each parameterized SQL statement to a separate callProcedure invocation.";
AsyncCompilerResult compileAdHocPlan(AdHocPlannerWork work) {
// record the catalog version the query is planned against to
// catch races vs. updateApplicationCatalog.
CatalogContext context = work.catalogContext;
if (context == null) {
context = VoltDB.instance().getCatalogContext();
}
final PlannerTool ptool = context.m_ptool;
List<String> errorMsgs = new ArrayList<>();
List<AdHocPlannedStatement> stmts = new ArrayList<>();
int partitionParamIndex = -1;
VoltType partitionParamType = null;
Object partitionParamValue = null;
assert(work.sqlStatements != null);
// Take advantage of the planner optimization for inferring single partition work
// when the batch has one statement.
StatementPartitioning partitioning = null;
boolean inferSP = (work.sqlStatements.length == 1) && work.inferPartitioning;
if (work.userParamSet != null && work.userParamSet.length > 0) {
if (work.sqlStatements.length != 1) {
return AsyncCompilerResult.makeErrorResult(work, AdHocErrorResponseMessage);
}
}
for (final String sqlStatement : work.sqlStatements) {
if (inferSP) {
partitioning = StatementPartitioning.inferPartitioning();
}
else if (work.userPartitionKey == null) {
partitioning = StatementPartitioning.forceMP();
}
else {
partitioning = StatementPartitioning.forceSP();
}
try {
AdHocPlannedStatement result = ptool.planSql(sqlStatement, partitioning,
work.explainMode != ExplainMode.NONE, work.userParamSet);
// The planning tool may have optimized for the single partition case
// and generated a partition parameter.
if (inferSP) {
partitionParamIndex = result.getPartitioningParameterIndex();
partitionParamType = result.getPartitioningParameterType();
partitionParamValue = result.getPartitioningParameterValue();
}
stmts.add(result);
}
catch (Exception e) {
errorMsgs.add("Unexpected Ad Hoc Planning Error: " + e);
}
catch (StackOverflowError error) {
// Overly long predicate expressions can cause a
// StackOverflowError in various code paths that may be
// covered by different StackOverflowError/Error/Throwable
// catch blocks. The factors that determine which code path
// and catch block get activated appears to be platform
// sensitive for reasons we do not entirely understand.
// To generate a deterministic error message regardless of
// these factors, purposely defer StackOverflowError handling
// for as long as possible, so that it can be handled
// consistently by a minimum number of high level callers like
// this one.
// This eliminates the need to synchronize error message text
// in multiple catch blocks, which becomes a problem when some
// catch blocks lead to re-wrapping of exceptions which tends
// to adorn the final error text in ways that are hard to track
// and replicate.
// Deferring StackOverflowError handling MAY mean ADDING
// explicit StackOverflowError catch blocks that re-throw
// the error to bypass more generic catch blocks
// for Error or Throwable on the same try block.
errorMsgs.add("Encountered stack overflow error. " +
"Try reducing the number of predicate expressions in the query.");
}
catch (AssertionError ae) {
errorMsgs.add("Assertion Error in Ad Hoc Planning: " + ae);
}
}
String errorSummary = null;
if (!errorMsgs.isEmpty()) {
errorSummary = StringUtils.join(errorMsgs, "\n");
}
AdHocPlannedStmtBatch plannedStmtBatch =
new AdHocPlannedStmtBatch(work, stmts,
partitionParamIndex, partitionParamType, partitionParamValue,
errorSummary);
if (adhocLog.isDebugEnabled()) {
logBatch(plannedStmtBatch);
}
return plannedStmtBatch;
}
/**
* A simplified variant of compileAdHoc that translates a
* pseudo-statement generated internally from a system stored proc
* invocation into a multi-part EE statement plan.
* @param work
* @return
*/
AsyncCompilerResult compileSysProcPlan(AdHocPlannerWork work) {
// record the catalog version the query is planned against to
// catch races vs. updateApplicationCatalog.
CatalogContext context = work.catalogContext;
if (context == null) {
context = VoltDB.instance().getCatalogContext();
}
final PlannerTool ptool = context.m_ptool;
List<String> errorMsgs = new ArrayList<>();
List<AdHocPlannedStatement> stmts = new ArrayList<>();
assert(work.sqlStatements != null);
assert(work.sqlStatements.length == 1);
String sqlStatement = work.sqlStatements[0];
StatementPartitioning partitioning = StatementPartitioning.forceMP();
try {
AdHocPlannedStatement result = ptool.planSql(sqlStatement, partitioning,
false, work.userParamSet);
stmts.add(result);
}
catch (Exception ex) {
errorMsgs.add("Unexpected System Stored Procedure Planning Error: " + ex);
}
catch (AssertionError ae) {
errorMsgs.add("Assertion Error in System Stored Procedure Planning: " + ae);
}
String errorSummary = null;
if ( ! errorMsgs.isEmpty()) {
errorSummary = StringUtils.join(errorMsgs, "\n");
}
AdHocPlannedStmtBatch plannedStmtBatch = new AdHocPlannedStmtBatch(work,
stmts, -1, null, null, errorSummary);
if (adhocLog.isDebugEnabled()) {
logBatch(plannedStmtBatch);
}
return plannedStmtBatch;
}
/**
* Log ad hoc batch info
* @param batch planned statement batch
*/
private void logBatch(final AdHocPlannedStmtBatch batch)
{
final int numStmts = batch.work.getStatementCount();
final int numParams = batch.work.getParameterCount();
final String readOnly = batch.readOnly ? "yes" : "no";
final String singlePartition = batch.isSinglePartitionCompatible() ? "yes" : "no";
final String user = batch.work.user.m_name;
final CatalogContext context = (batch.work.catalogContext != null
? batch.work.catalogContext
: VoltDB.instance().getCatalogContext());
final String[] groupNames = context.authSystem.getGroupNamesForUser(user);
final String groupList = StringUtils.join(groupNames, ',');
adhocLog.debug(String.format(
"=== statements=%d parameters=%d read-only=%s single-partition=%s user=%s groups=[%s]",
numStmts, numParams, readOnly, singlePartition, user, groupList));
if (batch.work.sqlStatements != null) {
for (int i = 0; i < batch.work.sqlStatements.length; ++i) {
adhocLog.debug(String.format("Statement #%d: %s", i + 1, batch.work.sqlStatements[i]));
}
}
if (batch.work.userParamSet != null) {
for (int i = 0; i < batch.work.userParamSet.length; ++i) {
Object value = batch.work.userParamSet[i];
final String valueString = (value != null ? value.toString() : "NULL");
adhocLog.debug(String.format("Parameter #%d: %s", i + 1, valueString));
}
}
}
}