oracle.kv.impl.client.admin.DdlFuture Maven / Gradle / Ivy
/*-
* Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle NoSQL
* Database made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle NoSQL Database for a copy of the license and
* additional information.
*/
package oracle.kv.impl.client.admin;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.rmi.RemoteException;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import oracle.kv.FaultException;
import oracle.kv.KVSecurityException;
import oracle.kv.KVVersion;
import oracle.kv.StatementResult;
import oracle.kv.impl.admin.AdminFaultException;
/**
* DdlFutures are returned by TableAPI.execute(), to provide a handle to a
* asynchronous DDL operation.
*
* A DdlFuture is thread safe. Multiple threads may safely call get, cancel,
* etc on a single instance of DdlFuture. Alternatively, the application may
* call TableAPI.execute() multiple times on the same statement, obtaining
* multiple DdlFuture instances. For example:
*
* {@literal
* thread 1 --+
* +---> DdlFutureX --------> polling task (DdlCheckTask)
* thread 2 --+
*
* or
*
* thread 1 --> DdlFutureX--+
* +-----------> polling task (DdlCheckTask)
* thread 2 --> DdlFutureY--+
* }
*
* Checking for operation completion:
* ---------------------------------
* The DdlFuture can instigate polling for operation completion. Polling is
* started lazily; it's initiated by a call to get() or get(time, unit). At
* that point the DdlFuture calls DdlStatementExecutor to instigate polling,
* and to register itself as an interested party.
*
* DdlFutures may also ask for current status. In that case, each
* DdlFuture would make its own direct call to the AdminService
*
* Synchronization hierarchy:
* --------------------------
* The order of locking is DdlStatementExecutor, followed by DdlFuture.
* Any DdlFuture synchronization must be sure not to call a synchronized
* DdlStatementExecutor method.
*
* Serialization
* -------------
* The DdlFuture can be serialized by the application, and saved for future
* use. This is particularly useful for stateless clients which wish to
* persist their ExecutionFuture handles.
*/
public class DdlFuture implements oracle.kv.ExecutionFuture {
/*
* A version value for serialization. See toByteArray() for a description
* of the format.
*/
private static final short CURRENT_VERSION = 1;
/* The charset used for en/decoding strings and char arrays */
static Charset UTF8 = Charset.forName("UTF-8");
/* The statement issued by the user */
private final char[] statement;
/* true if Cancel() was called */
private volatile boolean cancelRequested;
/*
* isCancelled applies to the semantics of Future.isCancelled. It is
* supposed to be true if the app explicitly requested a cancel, and the
* cancel has completed. This helps to distinguish between these semantics,
* and the fact that plan cancellation can happen on the server side in
* order to terminate a plan that has incurred an error.
*
* If the plan was in error, and has been canceled on the server side to
* terminate it, isCancelled will be false.
*/
private volatile boolean isCancelled;
/* true if the operation has ended */
private volatile boolean isDone;
private final DdlStatementExecutor statementExec;
private final CountDownLatch countdown;
private final int planId;
/*
* Most recent plan status. Initialized at first with the planInfo returned
* by the initial request to AdminStatementService.execute(), and updated
* every time a new status and planInfo is obtained by the polling task or
* by a call to getStatus(). May be null in several cases:
* - there has not yet been any contact with the kvstore, which happens
* if the future was created by TableAPI.getFuture(), and get() and
* getStatus haven't yet been called.
* - the DDLFuture was created from a serialized version of the future,
* as a byte array, and the plan id is 0.
*/
private volatile ExecutionInfo lastExecInfo;
/*
* Hang onto the first exception seen by a status call. We need the
* exception type so we can know whether to wrap the problem as a
* ExecutionException, or to directly throw a KVSecurityException.
*/
private volatile Throwable firstProblem;
/**
* Create a DdlFuture when there's only a planId available, from
* TableAPIImpl.getFuture();
*/
public DdlFuture(int planId,
DdlStatementExecutor statementExecutor) {
assert planId > 0;
this.statement = null;
this.planId = planId;
this.statementExec = statementExecutor;
countdown = new CountDownLatch(1);
}
/**
* Create a DdlFuture from a serialized ExecutionFuture.
*
* @throws IllegalArgumentException if the serialized future has a version
* that we can't handle.
*/
public DdlFuture(byte[] futureBytes,
DdlStatementExecutor statementExecutor) {
this.statementExec = statementExecutor;
ByteBuffer buf = ByteBuffer.wrap(futureBytes);
short version = buf.getShort();
/*
* In the future, if we support more versions, do more intelligent
* checking.
*/
if (version != 1) {
throw new IllegalArgumentException
("Version " + KVVersion.CURRENT_VERSION +
"of the NoSQL DB client cannot accept a serialized " +
"ExecutionFuture that is version "+ version);
}
/* Note that the planId may be 0 if the statement didn't execute */
planId = buf.getInt();
short statementLen = buf.getShort();
if (statementLen > 0) {
this.statement = readCharArrayAndReposition(buf, statementLen);
} else {
this.statement = new char[0];
}
/* PlanId != 0: there's no more information in the serialized result.*/
if (planId > 0) {
countdown = new CountDownLatch(1);
return;
}
/* PlanId == 0: Nothing to wait for, no plan is executing. */
isDone = true;
countdown = new CountDownLatch(0);
int statusLen = buf.getInt();
String status = readStringAndReposition(buf, statusLen);
int statusAsJsonLen = buf.getInt();
String statusAsJson = readStringAndReposition(buf, statusAsJsonLen);
int resultLen = buf.getInt();
String result = readStringAndReposition(buf, resultLen);
this.lastExecInfo = new ExecutionInfoImpl(planId,
true, /* isDone */
status,
statusAsJson,
true, /* isSuccess */
false, /* isCancelled */
null, /* errMsg */
false, /* needsCancel*/
result);
}
/**
* Read a string that is at this ByteBuffer's position, and then reposition
* the ByteBuffer to leave it ready to read the next field.
*/
private String readStringAndReposition(ByteBuffer buf, int stringLen) {
String result = null;
if (stringLen > 0) {
result = new String(buf.array(), buf.position(), stringLen, UTF8);
buf.position(buf.position() + stringLen);
}
return result;
}
/**
* Read a char[] that is at this ByteBuffer's position, and then reposition
* the ByteBuffer to leave it ready to read the next field.
*/
private char[] readCharArrayAndReposition(ByteBuffer buf, int stringLen) {
char[] res = null;
if (stringLen > 0) {
ByteBuffer buf2 = ByteBuffer.wrap(buf.array(), buf.position(),
stringLen);
res = UTF8.decode(buf2).array();
buf.position(buf.position() + stringLen);
}
return res;
}
public DdlFuture(char[] statement, ExecutionInfo executionInfo,
DdlStatementExecutor statementExecutor) {
this.statement = statement;
this.planId = executionInfo.getPlanId();
this.lastExecInfo = executionInfo;
this.statementExec = statementExecutor;
if (planId > 0) {
countdown = new CountDownLatch(1);
} else {
/* Nothing to wait for, no plan is executing. */
isDone = true;
countdown = new CountDownLatch(0);
}
/*
* We got some information already about this command. Make note of
* it -- the command may even have already finished.
*/
applyNewInfo(executionInfo, null);
}
/**
* Returns false if the operation couldn't be cancelled, true otherwise;
* The contract for Future.cancel() says that cancel can only execute
* successfully once. From Future.java:
*
* This attempt will fail if the task has already completed, has already
* been cancelled, or could not be cancelled for some other reason.
*/
@Override
synchronized public boolean cancel(boolean mayInterruptIfRunning)
throws FaultException {
/*
* Already completed. This covers all cases where the planId == 0:
* the futures for those kinds of operations are all marked as done.
*/
if (isDone) {
return false;
}
/* Already cancelled. */
if (isCancelled) {
return false;
}
if (!mayInterruptIfRunning) {
/*
* By definition, once the table statement is issued and
* sent to the server, it is running. If the caller specifies
* that the statement should not be interrupted, we will always
* return false if the command has not finished.
*/
return false;
}
try {
cancelRequested = true;
ExecutionInfo newInfo = cancelAndGetStatus(statementExec, planId);
applyNewInfo(newInfo, null);
statementExec.updateWaiters(newInfo);
return isCancelled();
} catch (AdminFaultException e) {
throw new FaultException("Problem cancelling " +
new String(statement) + ", planId = " + planId + ": " + e,
false);
} catch (RemoteException e) {
throw new FaultException("Communication problem cancelling " +
" for " + new String(statement) + ", " +
"plan " + planId + ", retry:" + e, false);
}
}
/**
* Block until the plan has finished.
* @return the outcome of the plan.
*/
@Override
public StatementResult get()
throws InterruptedException,
ExecutionException {
if (isCancelled) {
throw new CancellationException();
}
if (!isDone) {
statementExec.startPolling(planId, this);
}
countdown.await();
checkForError();
return getLastStatus();
}
/**
* Block until the plan has finished or the timeout period is exceeded.
* @throws InterruptedException
* @throws TimeoutException
* @throws ExecutionException
*/
@Override
public StatementResult get(long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException, ExecutionException {
if (isCancelled) {
throw new CancellationException();
}
if (!isDone) {
statementExec.startPolling(planId, this);
}
boolean finished = countdown.await(timeout, unit);
if (!finished) {
throw new TimeoutException("Statement '" + new String(statement) +
"' did not finish within " + timeout + " " + unit);
}
checkForError();
return getLastStatus();
}
@Override
public boolean isCancelled() {
return isCancelled;
}
@Override
public boolean isDone() {
updateStatus();
return isDone;
}
/**
* Reach out to the kvstore server and get current status. Update this
* future.
*/
@Override
public StatementResult updateStatus() {
if (isDone) {
return getLastStatus();
}
try {
ClientAdminServiceAPI ca = statementExec.getClientAdminService();
ExecutionInfo newInfo = ca.getExecutionStatus(planId);
newInfo = checkForNeedsCancel(newInfo, statementExec, planId);
applyNewInfo(newInfo, null);
} catch (AdminFaultException e) {
throw new FaultException("Problem updating status for '" +
new String(statement) + "', planId = " + planId + ": " + e,
false);
} catch (RemoteException e) {
throw new FaultException("Communication problem updating status " +
" for '" + new String(statement) + "', plan " +
planId + ", retry:" + e, false);
}
return getLastStatus();
}
/**
* Return the last information known about the execution. Does not require
* communication with the server.
*/
@Override
public synchronized StatementResult getLastStatus() {
/*
* synchronized so we get a consistent snapshot of lastExecInfo,
* isDone, isCancelled.
*/
return new AdminResult(planId, lastExecInfo, isDone, isCancelled);
}
/**
* Return the statement which has been executed.
*/
@Override
public String getStatement() {
return new String(statement);
}
/**
* If the operation failed and it's a security issue, throw a
* KVSecurityException. Otherwise, throw an ExecutionException which wraps
* the original exception
*/
private synchronized void checkForError()
throws ExecutionException, KVSecurityException {
if ((firstProblem != null) &&
(firstProblem instanceof KVSecurityException)) {
KVSecurityException k = (KVSecurityException) firstProblem;
throw k;
}
/* All other problems are wrapped in ExecutionException */
if ((lastExecInfo != null) &&
(lastExecInfo.getErrorMessage() != null)) {
throw new ExecutionException
(new FaultException(lastExecInfo.getErrorMessage(), false));
}
}
/**
* A new status has been obtained, update the future.
*/
synchronized void applyNewInfo(ExecutionInfo newInfo, Throwable t) {
/* Already done, nothing to do */
if (isDone) {
return;
}
/*
* Update all the state required before releasing latch and
* letting others see state.
*/
if (firstProblem == null) {
/* Save the first problem. Note that t may be null */
firstProblem = t;
}
lastExecInfo = newInfo;
isDone = newInfo.isTerminated();
isCancelled = newInfo.isCancelled() && cancelRequested;
if (newInfo.isTerminated() || t != null) {
countdown.countDown();
}
}
static ExecutionInfo checkForNeedsCancel(ExecutionInfo info,
DdlStatementExecutor statementExec,
int planId)
throws RemoteException {
if (info.needsTermination()) {
return cancelAndGetStatus(statementExec, planId);
}
return info;
}
static ExecutionInfo cancelAndGetStatus(DdlStatementExecutor statementExec,
int planId) throws RemoteException {
ClientAdminServiceAPI ca = statementExec.getClientAdminService();
return ca.interruptAndCancel(planId);
}
/**
* Return a serialized version of the ExecutionFuture.
*/
@Override
public byte[] toByteArray() {
String status = null;
String statusAsJson = null;
String result = null;
if (lastExecInfo != null) {
status = lastExecInfo.getInfo();
statusAsJson = lastExecInfo.getJSONInfo();
result = lastExecInfo.getResult();
}
return toByteArray(planId, statement, status, statusAsJson, result);
}
public static byte[] toByteArray(int planId) {
return toByteArray(planId, new char[0], null, null, null);
}
/**
* Internal use method to get plan id
*/
public int getPlanId() {
return planId;
}
/**
* Convert a planId and statement to a serialized DDLFuture handle. Public
* for support for the deprecated TableAPI.getFuture();
*
* In version 1, we chose to serialize the result only for a successfully
* completed statement, when the result is not available on the server (the
* planId is 0). That means we can infer what the value of other pieces of
* the future and statement result should be, i.e.
* - the operation is finished (isDone == true)
* - isCancelled = false
* - the error message is null
*
* Version 1 serialization is:
* (short) version,
* (int) planId
* (short) statement length
* (string) statement
* -- optional --
* (int) status length
* (string) status
* (int) status as Json length
* (string) statusAsJson
* (int) result length
* (string) result
*/
private static byte[] toByteArray(int planId,
char[] statement,
String status,
String statusAsJson,
String result) {
/*
* Doing version 1 serialization. Figure out the buffer length
* version + planId + statement length = 8
*/
int serializedLen = 8;
ByteBuffer bb = UTF8.encode(CharBuffer.wrap(statement));
byte[] statementBytes = bb.array();
int statementLength = bb.remaining();
serializedLen += statementLength;
int statusLen = 0;
int statusAsJsonLen = 0;
int resultLen = 0;
byte[] statusBytes = null;
byte[] statusAsJsonBytes = null;
byte[] resultBytes = null;
if (planId == 0) {
if (status != null) {
statusBytes = status.getBytes(UTF8);
statusLen = statusBytes.length;
}
if (statusAsJson != null) {
statusAsJsonBytes = statusAsJson.getBytes(UTF8);
statusAsJsonLen = statusAsJsonBytes.length;
}
if (result != null) {
resultBytes = result.getBytes(UTF8);
resultLen = resultBytes.length;
}
serializedLen += (12 + statusLen + statusAsJsonLen + resultLen);
}
ByteBuffer buf = ByteBuffer.allocate(serializedLen);
buf.putShort(CURRENT_VERSION);
buf.putInt(planId);
buf.putShort((short) statementLength);
if (statementLength > 0) {
buf.put(statementBytes, 0, statementLength);
}
/*
* Serialize the result if the statement is finished, and the result
* will not be available on the server. This is only true if the planId
* is 0.
* If the planId is nonzero, and there is a corresponding plan on the
* server, don't serialize the result, even if the operation was
* done. In those cases, the client will have to make a request to the
* server to get status, so that it can get the detailed execution
* history that is available for plans.
*/
if (planId == 0) {
buf.putInt(statusLen);
if (statusLen > 0) {
buf.put(statusBytes);
}
buf.putInt(statusAsJsonLen);
if (statusAsJsonLen > 0) {
buf.put(statusAsJsonBytes);
}
buf.putInt(resultLen);
if (resultLen > 0) {
buf.put(resultBytes);
}
}
return buf.array();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy