/* This file is part of VoltDB.
* Copyright (C) 2008-2010 VoltDB Inc.
*
* VoltDB 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.
*
* VoltDB 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 VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.compiler;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.log4j.Logger;
import org.voltdb.CatalogContext;
import org.voltdb.VoltDB;
import org.voltdb.catalog.Catalog;
import org.voltdb.catalog.CatalogDiffEngine;
import org.voltdb.utils.CatalogUtil;
import org.voltdb.utils.DumpManager;
import org.voltdb.utils.Encoder;
import edu.brown.hstore.HStoreSite;
import edu.brown.hstore.txns.LocalTransaction;
import edu.brown.interfaces.Shutdownable;
import edu.brown.logging.LoggerUtil;
import edu.brown.logging.LoggerUtil.LoggerBoolean;
public class AsyncCompilerWorkThread extends Thread implements DumpManager.Dumpable, Shutdownable {
private static final Logger LOG = Logger.getLogger(AsyncCompilerWorkThread.class);
private static final LoggerBoolean debug = new LoggerBoolean();
private static final LoggerBoolean trace = new LoggerBoolean();
static {
LoggerUtil.attachObserver(LOG, debug, trace);
}
LinkedBlockingQueue<AsyncCompilerWork> m_work = new LinkedBlockingQueue<AsyncCompilerWork>();
final ArrayDeque<AsyncCompilerResult> m_finished = new ArrayDeque<AsyncCompilerResult>();
//HSQLInterface m_hsql;
PlannerTool m_ptool;
int counter = 0;
final int m_siteId;
boolean m_isLoaded = false;
CatalogContext m_context;
HStoreSite m_hStoreSite;
/** If this is true, update the catalog */
private final AtomicBoolean m_shouldUpdateCatalog = new AtomicBoolean(false);
// store the id used by the DumpManager to identify this execution site
final String m_dumpId;
long m_currentDumpTimestamp = 0;
public AsyncCompilerWorkThread(CatalogContext context, int siteId) {
m_ptool = null;
//m_hsql = null;
m_siteId = siteId;
m_context = context;
setName("Ad Hoc Planner");
m_dumpId = "AdHocPlannerThread." + String.valueOf(m_siteId);
DumpManager.register(m_dumpId, this);
}
public AsyncCompilerWorkThread(HStoreSite hStoreSite, int siteId) {
m_ptool = null;
//m_hsql = null;
m_siteId = siteId;
//m_context = context;
m_hStoreSite = hStoreSite;
setName("Ad Hoc Planner");
m_dumpId = "AdHocPlannerThread." + String.valueOf(m_siteId);
DumpManager.register(m_dumpId, this);
}
public synchronized void ensureLoadedPlanner() {
// if the process was created but is dead, clear the placeholder
if ((m_ptool != null) && (m_ptool.expensiveIsRunningCheck() == false)) {
LOG.error("Planner process died on its own. It will be restarted if needed.");
m_ptool = null;
}
// if no placeholder, create a new plannertool
if (m_ptool == null) {
m_ptool = PlannerTool.createPlannerToolProcess(m_hStoreSite.getCatalogContext().catalog.serialize());
}
}
public void verifyEverthingIsKosher() {
if (m_ptool != null) {
// check if the planner process has been blocked for 2 seconds
if (m_ptool.perhapsIsHung(5000)) {
LOG.error("Was forced to kill the planner process due to a timeout. It will be restarted if needed.");
m_ptool.kill();
}
}
}
@Override
public void prepareShutdown(boolean error) {
AdHocPlannerWork work = new AdHocPlannerWork(null);
work.shouldShutdown = true;
m_work.add(work);
}
@Override
public boolean isShuttingDown() {
return false; // FIXME
}
public void shutdown() {
try {
this.join();
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
/**
* Set the flag that tells this thread to update its
* catalog when it's threadsafe.
*/
public void notifyOfCatalogUpdate() {
m_shouldUpdateCatalog.set(true);
}
/**
*
* @param sql
* @param clientHandle Handle provided by the client application (not ClientInterface) HStoreSite?
* @param connectionId
* @param hostname Hostname of the other end of the connection
* @param sequenceNumber
* @param clientData Data supplied by ClientInterface (typically a VoltPort) that will be in the PlannedStmt produced later.
*/
public void planSQL(
LocalTransaction ts,
String sql) {
AdHocPlannerWork work = new AdHocPlannerWork(ts);
work.clientHandle = ts.getClientHandle();
work.sql = sql;
// work.connectionId = connectionId;
// work.hostname = hostname;
// work.sequenceNumber = sequenceNumber;
// work.clientData = clientData;
m_work.add(work);
}
public void prepareCatalogUpdate(
String catalogURL,
long clientHandle,
long connectionId,
String hostname,
int sequenceNumber,
Object clientData) {
CatalogChangeWork work = new CatalogChangeWork();
work.clientHandle = clientHandle;
// work.connectionId = connectionId;
// work.hostname = hostname;
// work.sequenceNumber = sequenceNumber;
// work.clientData = clientData;
work.catalogURL = catalogURL;
m_work.add(work);
}
public AsyncCompilerResult getPlannedStmt() {
synchronized (m_finished) {
return m_finished.poll();
}
}
@Override
public void run() {
AsyncCompilerWork work = null;
try {
work = m_work.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
while (work.shouldShutdown == false) {
// handle a dump if needed
if (work.shouldDump == true) {
// DumpManager.putDump(m_dumpId, m_currentDumpTimestamp, true, getDumpContents());
}
else {
// deal with reloading the global catalog
if (m_shouldUpdateCatalog.compareAndSet(true, false)) {
//TODO: @AdHoc for hstoresite, how to switch catalogcontext for hstoresite?
m_context = VoltDB.instance().getCatalogContext();
// kill the planner process which has an outdated catalog
// it will get created again for the next stmt
if (m_ptool != null) {
m_ptool.kill();
m_ptool = null;
}
}
AsyncCompilerResult result = null;
if (work instanceof AdHocPlannerWork)
result = compileAdHocPlan((AdHocPlannerWork) work);
if (work instanceof CatalogChangeWork)
result = prepareApplicationCatalogDiff((CatalogChangeWork) work);
assert(result != null);
synchronized (m_finished) {
m_finished.add(result);
}
}
try {
work = m_work.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (m_ptool != null)
m_ptool.kill();
}
public void notifyShouldUpdateCatalog() {
m_shouldUpdateCatalog.set(true);
}
@Override
public void goDumpYourself(long timestamp) {
m_currentDumpTimestamp = timestamp;
AdHocPlannerWork work = new AdHocPlannerWork(null);
work.shouldDump = true;
m_work.add(work);
// DumpManager.putDump(m_dumpId, timestamp, false, getDumpContents());
}
// /**
// * Get the actual file contents for a dump of state reachable by
// * this thread. Can be called unsafely or safely.
// */
// public PlannerThreadContext getDumpContents() {
// PlannerThreadContext context = new PlannerThreadContext();
// context.siteId = m_siteId;
//
// // doing this with arraylists before arrays seems more stable
// // if the contents change while iterating
//
// ArrayList<AsyncCompilerWork> toplan = new ArrayList<AsyncCompilerWork>();
// ArrayList<AsyncCompilerResult> planned = new ArrayList<AsyncCompilerResult>();
//
// for (AsyncCompilerWork work : m_work)
// toplan.add(work);
// for (AsyncCompilerResult stmt : m_finished)
// planned.add(stmt);
//
// context.compilerWork = new AsyncCompilerWork[toplan.size()];
// for (int i = 0; i < toplan.size(); i++)
// context.compilerWork[i] = toplan.get(i);
//
// context.compilerResults = new AsyncCompilerResult[planned.size()];
// for (int i = 0; i < planned.size(); i++)
// context.compilerResults[i] = planned.get(i);
//
// return context;
// }
private AsyncCompilerResult compileAdHocPlan(AdHocPlannerWork work) {
AdHocPlannedStmt plannedStmt = new AdHocPlannedStmt(work.ts);
plannedStmt.clientHandle = work.clientHandle;
// plannedStmt.connectionId = work.connectionId;
// plannedStmt.hostname = work.hostname;
// plannedStmt.clientData = work.clientData;
try {
ensureLoadedPlanner();
PlannerTool.Result result = m_ptool.planSql(work.sql);
plannedStmt.aggregatorFragment = result.onePlan;
plannedStmt.collectorFragment = result.allPlan;
plannedStmt.isReplicatedTableDML = result.replicatedDML;
plannedStmt.sql = work.sql;
plannedStmt.errorMsg = result.errors;
if (plannedStmt.errorMsg != null)
LOG.error("PlannerTool Error: " + result.errors);
}
catch (Exception e) {
String msg = "Unexpected Ad Hoc Planning Error";
LOG.warn(msg, e);
plannedStmt.errorMsg = msg + ": " + e.getMessage();
}
return plannedStmt;
}
private AsyncCompilerResult prepareApplicationCatalogDiff(CatalogChangeWork work) {
// create the change result and set up all the boiler plate
CatalogChangeResult retval = new CatalogChangeResult();
// retval.clientData = work.clientData;
retval.clientHandle = work.clientHandle;
// retval.connectionId = work.connectionId;
// retval.hostname = work.hostname;
// catalog change specific boiler plate
retval.catalogURL = work.catalogURL;
// get the diff between catalogs
try {
// try to get the new catalog from the params
String newCatalogCommands = CatalogUtil.loadCatalogFromJar(work.catalogURL, null);
if (newCatalogCommands == null) {
retval.errorMsg = "Unable to read from catalog at: " + work.catalogURL;
return retval;
}
Catalog newCatalog = new Catalog();
newCatalog.execute(newCatalogCommands);
// get the current catalog
CatalogContext context = VoltDB.instance().getCatalogContext();
// store the version of the catalog the diffs were created against.
// verified when / if the update procedure runs in order to verify
// catalogs only move forward
retval.expectedCatalogVersion = context.catalog.getCatalogVersion();
// compute the diff in StringBuilder
CatalogDiffEngine diff = new CatalogDiffEngine(context.catalog, newCatalog);
if (!diff.supported()) {
throw new Exception("The requested catalog change is not a supported change at this time. " + diff.errors());
}
// since diff commands can be stupidly big, compress them here
retval.encodedDiffCommands = Encoder.compressAndBase64Encode(diff.commands());
// check if the resulting string is small enough to fit in our parameter sets (about 2mb)
if (retval.encodedDiffCommands.length() > (2 * 1000 * 1000)) {
throw new Exception("The requested catalog change is too large for this version of VoltDB. " +
"Try a series of smaller updates.");
}
}
catch (Exception e) {
e.printStackTrace();
retval.encodedDiffCommands = null;
retval.errorMsg = e.getMessage();
}
return retval;
}
private boolean verifyCatalogChangesAreAllowed(String diffCommands) {
String[] allowedCommandPrefixes = { "set /clusters[cluster]/databases[database]/users[",
"set /clusters[cluster]/databases[database]/groups[",
"set /clusters[cluster]/databases[database]/procedures[",
"add /clusters[cluster]/databases[database] users ",
"add /clusters[cluster]/databases[database] groups",
"add /clusters[cluster]/databases[database] procedures ",
"delete /clusters[cluster]/databases[database] users ",
"delete /clusters[cluster]/databases[database] groups ",
"delete /clusters[cluster]/databases[database] procedures ",
"add /clusters[cluster]/databases[database]/users[",
"add /clusters[cluster]/databases[database]/groups[",
"add /clusters[cluster]/databases[database]/procedures[",
"delete /clusters[cluster]/databases[database]/users[",
"delete /clusters[cluster]/databases[database]/groups[",
"delete /clusters[cluster]/databases[database]/procedures[" };
for (String command : diffCommands.split("\r\n|\r|\n")) {
command = command.trim();
if (command.length() == 0)
continue;
boolean foundMatch = false;
for (String prefix : allowedCommandPrefixes) {
if (command.startsWith(prefix))
foundMatch = true;
}
if (foundMatch == false)
return false;
}
return true;
}
}