/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
licenses@blazegraph.com
This program 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; version 2 of the License.
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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.bigdata.rdf.sail.webapp;
import java.io.IOException;
import java.io.StringWriter;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import com.bigdata.journal.ITx;
import com.bigdata.journal.Journal;
import com.bigdata.journal.ValidationError;
import com.bigdata.rdf.sail.BigdataSail;
import com.bigdata.rdf.sail.webapp.XMLBuilder.Node;
import com.bigdata.service.IBigdataFederation;
import com.bigdata.service.ITxState;
import com.bigdata.util.InnerCause;
import com.bigdata.util.NV;
/**
* Servlet provides a REST interface for managing stand-off read/write
* transaction. Given a namespace that is provisioned for read/write
* transactions, a client can create a transaction (obtaining an identifier for
* that transaction), do work with that transaction (including mutation and
* query), prepare the transaction, and finally commit or abort the transaction.
* <p>
* Transaction isolation requires that the namespace is provisioned with support
* for isolatable indices. See {@link BigdataSail.Options#ISOLATABLE_INDICES}.
* When isolation is enabled for a namespace, the updates for the namespace will
* be buffered by isolated indices. When a transaction prepares, the write set
* will be validated by comparing the modified tuples against the unisolated
* indices touched by the write set and looking for conflicts (revision
* timestamps that have been updated since the transaction start). If a conflict
* can not be reconciled, then the transaction will be unable to commit.
* <p>
* Read only transactions may also be requested in order to have snapshot
* isolation across a series of queries. blazegraph provides snapshot isolation
* for queries regardless. The use of an explicit read only transaction is only
* required to maintain the same snapshot across multiple queries.
*
* @see <a href="http://trac.bigdata.com/ticket/1156"> Support read/write
* transactions in the REST API</a>
*
* @see BigdataSail.Options#ISOLATABLE_INDICES
*/
public class TxServlet extends BigdataRDFServlet {
/**
*
*/
private static final long serialVersionUID = 1L;
static private final transient Logger log = Logger
.getLogger(TxServlet.class);
/**
* The URL query parameter for a PREPARE message.
*/
static final transient String ATTR_PREPARE = "PREPARE";
/**
* The URL query parameter for a COMMIT message.
*/
static final transient String ATTR_COMMIT = "COMMIT";
/**
* The URL query parameter for a ABORT message.
*/
static final transient String ATTR_ABORT = "ABORT";
/**
* The URL query parameter for a STATUS message.
*/
static final transient String ATTR_STATUS = "STATUS";
/**
* The name of the URL query parameter which indicates the timestamp for a
* CREATE message.
*/
static final transient String ATTR_TIMESTAMP = QueryServlet.ATTR_TIMESTAMP;
public TxServlet() {
}
/**
* Methods for transaction management (create, prepare, isActive, commit,
* abort).
*/
@Override
protected void doPost(final HttpServletRequest req,
final HttpServletResponse resp) throws IOException {
if (!isReadable(getServletContext(), req, resp)) {
// Service must be available.
return;
}
if (req.getRequestURI().endsWith("/tx")) {
// CREATE-TX
doCreateTx(req, resp);
return;
} else if (req.getParameter(ATTR_PREPARE) != null) {
doPrepareTx(req, resp);
} else if (req.getParameter(ATTR_ABORT) != null) {
doAbortTx(req, resp);
} else if (req.getParameter(ATTR_COMMIT) != null) {
doCommitTx(req, resp);
} else if (req.getParameter(ATTR_STATUS) != null) {
doStatusTx(req, resp);
} else {
buildAndCommitResponse(resp, HttpServletResponse.SC_BAD_REQUEST,
MIME_TEXT_HTML, "Unknown transaction management request");
}
}
@Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse resp) throws IOException {
if (!isReadable(getServletContext(), req, resp)) {
// Service must be available.
return;
}
if (req.getRequestURI().endsWith("/tx")) {
// LIST-TX
doListTx(req, resp);
return;
} else if (req.getParameter(ATTR_STATUS) != null) {
doStatusTx(req, resp);
} else {
buildAndCommitResponse(resp, HttpServletResponse.SC_BAD_REQUEST,
MIME_TEXT_HTML, "Unknown transaction management request");
}
}
/** <code>CREATE-TX(?timestamp=...)</code> */
private void doCreateTx(final HttpServletRequest req,
final HttpServletResponse resp) throws IOException {
final long beginNanos = System.nanoTime();
// Note: This parameter has default values for CREATE-TX.
final long timestamp = getTimestamp(req);
if (timestamp == ITx.UNISOLATED) {
// read/write transaction.
if (!isWritable(getServletContext(), req, resp)) {
// Service must be writable.
return;
}
if (getIndexManager() instanceof IBigdataFederation) {
buildAndCommitResponse(resp, HttpServletResponse.SC_BAD_REQUEST,
MIME_TEXT_HTML,
"Scale-out does not support distributed read/write transactions");
}
} else if (timestamp == ITx.READ_COMMITTED) {
// read-only transaction reading from the lastCommitTime.
} else if (timestamp > ITx.UNISOLATED) {
/*
* Create a read-only transaction reading from the most recent
* committed state whose commit timestamp is less than or equal to
* timestamp.
*/
} else {
buildAndCommitResponse(resp, HttpServletResponse.SC_BAD_REQUEST,
MIME_TEXT_HTML, "Illegal value: timestamp=" + timestamp);
return;
}
try {
/*
* Now that we have validated the request, create the transaction and
* report it to the client.
*/
final long txId = getBigdataRDFContext().newTx(timestamp);
// TODO This URL is correct IFF we only allow CREATE-TX at the correct
// path.
final String txURL = req.getRequestURL().append('/')
.append(Long.valueOf(txId)).toString();
final long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System
.nanoTime() - beginNanos);
final StringWriter w = new StringWriter();
final XMLBuilder t = new XMLBuilder(w);
final Node root = t.root("response");
root.attr("elapsed", elapsedMillis);
addTx(root, txId, getReadsOnCommitTimeOrNull(txId));
root.close();
buildAndCommitResponse(resp, HttpServletResponse.SC_CREATED,
MIME_APPLICATION_XML, w.toString(), new NV("Location", txURL));
} catch (Throwable t) {
launderThrowable(t, resp, "CREATE-TX");
}
}
/**
* ABORT-TX(txId)
*/
private void doAbortTx(final HttpServletRequest req,
final HttpServletResponse resp) throws IOException {
final long beginNanos = System.nanoTime();
final AtomicLong txId = new AtomicLong();
if (!getTxId(req, resp, txId))
return;
try {
if (getIndexManager() instanceof IBigdataFederation) {
((IBigdataFederation<?>) getIndexManager()).getTransactionService()
.abort(txId.get());
} else {
// On the journal we can lookup the Tx.
final ITx tx = ((Journal) getIndexManager())
.getTransactionManager().getTx(txId.get());
if (tx == null) {
// No such transaction.
buildAndCommitResponse(resp, HttpServletResponse.SC_NOT_FOUND,
MIME_TEXT_PLAIN, "ABORT-TX: Transaction not found: txId="
+ txId);
return;
}
if (!tx.isEmptyWriteSet()) {
// Dirty tx. Must be leader.
if (!isWritable(getServletContext(), req, resp)) {
// Service must be writable.
return;
}
}
((Journal) getIndexManager()).abort(txId.get());
}
final long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System
.nanoTime() - beginNanos);
final StringWriter w = new StringWriter();
final XMLBuilder t = new XMLBuilder(w);
final Node root = t.root("response");
root.attr("elapsed", elapsedMillis);
addTx(root, txId.get(), getReadsOnCommitTimeOrNull(txId.get()));
root.close();
buildAndCommitResponse(resp, HttpServletResponse.SC_OK,
MIME_APPLICATION_XML, w.toString());
} catch (Throwable t) {
if (InnerCause.isInnerCause(t, IllegalStateException.class)) {
/*
* TODO This is pretty diagnostic for the Journal. For scale-out
* there could be other root causes that might throw the same
* exception. We could make this 100% diagnostic by subclassing
* IllegalStateException and throwing a typed
* TransactionNotFoundException. At which point this condition could
* be pushed down inside of launderThrowabler()
*/
buildAndCommitResponse(resp, HttpServletResponse.SC_NOT_FOUND,
MIME_TEXT_PLAIN, "ABORT-TX: Transaction not found: txId="
+ txId);
return;
}
// some other error.
launderThrowable(t, resp, "ABORT-TX:: txId=" + txId);
}
}
/**
* COMMIT-TX(txId)
*/
private void doCommitTx(final HttpServletRequest req,
final HttpServletResponse resp) throws IOException {
final long beginNanos = System.nanoTime();
final AtomicLong txId = new AtomicLong();
if (!getTxId(req, resp, txId))
return;
try {
if (getIndexManager() instanceof IBigdataFederation) {
((IBigdataFederation<?>) getIndexManager()).getTransactionService()
.commit(txId.get());
} else {
// On the journal we can lookup the Tx.
final ITx tx = ((Journal) getIndexManager())
.getTransactionManager().getTx(txId.get());
if (tx == null) {
// No such transaction.
buildAndCommitResponse(resp, HttpServletResponse.SC_NOT_FOUND,
MIME_TEXT_PLAIN, "COMMIT-TX: Transaction not found: txId="
+ txId);
return;
}
if (!tx.isEmptyWriteSet()) {
// Dirty tx. Must be leader.
if (!isWritable(getServletContext(), req, resp)) {
// Service must be writable.
return;
}
}
((Journal) getIndexManager()).commit(txId.get());
}
final long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System
.nanoTime() - beginNanos);
final StringWriter w = new StringWriter();
final XMLBuilder t = new XMLBuilder(w);
final Node root = t.root("response");
root.attr("elapsed", elapsedMillis);
addTx(root, txId.get(), getReadsOnCommitTimeOrNull(txId.get()));
root.close();
buildAndCommitResponse(resp, HttpServletResponse.SC_OK,
MIME_APPLICATION_XML, w.toString());
} catch (Throwable e) {
if (InnerCause.isInnerCause(e, ValidationError.class)) {
/*
* The transaction could not be validated. The client needs to redo
* the transaction.
*
* Note: The 409 (CONFLICT) status code does deal with cases of
* resource conflict. However, in the case of our transactions API
* the resource is the transaction and there is no ability to "redo"
* the *same* transaction (same transaction identifier, same
* transaction resource) *unless* we also discard the write set of
* the transaction when validation fails (just during a commit or
* any time PREPARE is invoked?)
*/
final long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System
.nanoTime() - beginNanos);
final StringWriter w = new StringWriter();
final XMLBuilder t = new XMLBuilder(w);
final Node root = t.root("response");
root.attr("elapsed", elapsedMillis);
addTx(root, txId.get(), getReadsOnCommitTimeOrNull(txId.get()));
root.close();
buildAndCommitResponse(resp, HttpServletResponse.SC_CONFLICT,
MIME_APPLICATION_XML, w.toString());
return;
} else if (InnerCause.isInnerCause(e, IllegalStateException.class)) {
/*
* TODO This is pretty diagnostic for the Journal. For scale-out
* there could be other root causes that might throw the same
* exception. We could make this 100% diagnostic by subclassing
* IllegalStateException and throwing a typed
* TransactionNotFoundException. At which point this condition could
* be pushed down inside of launderThrowabler()
*/
buildAndCommitResponse(resp, HttpServletResponse.SC_NOT_FOUND,
MIME_TEXT_PLAIN, "COMMIT-TX: Transaction not found: txId="
+ txId);
return;
}
// some other error.
launderThrowable(e, resp, "COMMIT-TX:: txId=" + txId);
}
}
/**
* <code>PREPARE-TX(txId)</code>
*
* FIXME Test suite for this at the Journal level. Make sure that there are
* no undesired side-effects from validation. For example, the writeSet of
* the tx is modified by validation if a conflict is resolved. Is that
* modification Ok if we do not go ahead and commit? Should it be rolled
* back? Can we have additional writes on the tx and reconcile additional
* conflicts in another PREPARE or a COMMIT?
*/
private void doPrepareTx(final HttpServletRequest req,
final HttpServletResponse resp) throws IOException {
final long beginNanos = System.nanoTime();
final AtomicLong txId = new AtomicLong();
if (!getTxId(req, resp, txId))
return;
final boolean ok;
try {
if (getIndexManager() instanceof IBigdataFederation) {
// Scale-out does not have read/write transactions. This is a NOP.
ok = true;
} else {
// On the journal we can lookup the Tx.
final ITx tx = ((Journal) getIndexManager())
.getTransactionManager().getTx(txId.get());
if (tx == null) {
// No such transaction.
buildAndCommitResponse(resp, HttpServletResponse.SC_NOT_FOUND,
MIME_TEXT_PLAIN,
"PREPARE-TX: Transaction not found: txId=" + txId);
return;
}
if (!tx.isEmptyWriteSet()) {
// Dirty tx. Must be leader.
if (!isWritable(getServletContext(), req, resp)) {
// Service must be writable.
return;
}
}
ok = ((Journal) getIndexManager()).prepare(txId.get());
}
final long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System
.nanoTime() - beginNanos);
final StringWriter w = new StringWriter();
final XMLBuilder t = new XMLBuilder(w);
final Node root = t.root("response");
root.attr("elapsed", elapsedMillis);
addTx(root, txId.get(), getReadsOnCommitTimeOrNull(txId.get()));
root.close();
// Either OK (200) or CONFLICT (409).
final int statusCode = ok ? HttpServletResponse.SC_OK
: HttpServletResponse.SC_CONFLICT;
buildAndCommitResponse(resp, statusCode, MIME_APPLICATION_XML,
w.toString());
} catch (Throwable t) {
if (InnerCause.isInnerCause(t, IllegalStateException.class)) {
/*
* TODO This is pretty diagnostic for the Journal. For scale-out
* there could be other root causes that might throw the same
* exception. We could make this 100% diagnostic by subclassing
* IllegalStateException and throwing a typed
* TransactionNotFoundException. At which point this condition could
* be pushed down inside of launderThrowabler()
*/
buildAndCommitResponse(resp, HttpServletResponse.SC_NOT_FOUND,
MIME_TEXT_PLAIN, "PREPARE-TX: Transaction not found: txId="
+ txId);
return;
}
// some other error.
launderThrowable(t, resp, "PREPARE-TX:: txId=" + txId);
}
}
/**
* <code>STATUS-TX</code>
*/
private void doStatusTx(final HttpServletRequest req,
final HttpServletResponse resp) throws IOException {
final long beginNanos = System.nanoTime();
final AtomicLong txId = new AtomicLong();
if (!getTxId(req, resp, txId))
return;
try {
if (getIndexManager() instanceof IBigdataFederation) {
/*
* Scale-out does not let us resolve the transaction status.
*
* TODO This could be exposed on the ITransactionService easily
* enough.
*/
buildAndCommitResponse(resp, HttpServletResponse.SC_NOT_FOUND,
MIME_TEXT_PLAIN, "Scale-out does not support STATUS-TX");
return;
} else {
// On the journal we can lookup the Tx.
final ITx tx = ((Journal) getIndexManager())
.getTransactionManager().getTx(txId.get());
if (tx == null) {
// 404 (GONE). No such transaction (definitive).
buildAndCommitResponse(resp, HttpServletResponse.SC_GONE,
MIME_TEXT_PLAIN, "STATUS-TX: Transaction not found: txId="
+ txId);
return;
}
final long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System
.nanoTime() - beginNanos);
final StringWriter w = new StringWriter();
final XMLBuilder t = new XMLBuilder(w);
final Node root = t.root("response");
root.attr("elapsed", elapsedMillis);
addTx(root, txId.get(), getReadsOnCommitTimeOrNull(txId.get()));
root.close();
buildAndCommitResponse(resp, HttpServletResponse.SC_OK,
MIME_APPLICATION_XML, w.toString(),//
// disable caching (if GET)
new NV("Cache-Control", "no-cache"));
return;
}
} catch (Throwable t) {
if (InnerCause.isInnerCause(t, IllegalStateException.class)) {
/*
* TODO This is pretty diagnostic for the Journal. For scale-out
* there could be other root causes that might throw the same
* exception. We could make this 100% diagnostic by subclassing
* IllegalStateException and throwing a typed
* TransactionNotFoundException. At which point this condition could
* be pushed down inside of launderThrowabler()
*/
buildAndCommitResponse(resp, HttpServletResponse.SC_NOT_FOUND,
MIME_TEXT_PLAIN, "STATUS-TX: Transaction not found: txId="
+ txId);
return;
}
// some other error.
launderThrowable(t, resp, "PREPARE-TX:: txId=" + txId);
}
}
/**
* <code>LIST-TX</code>
*/
private void doListTx(final HttpServletRequest req,
final HttpServletResponse resp) throws IOException {
final ITxState[] a;
if (getIndexManager() instanceof IBigdataFederation) {
// NOP
a = new ITxState[] {};
} else {
// The Journal will self-report the active transactions.
a = ((Journal) getIndexManager()).getTransactionManager()
.getActiveTx();
}
final StringWriter w = new StringWriter();
final XMLBuilder t = new XMLBuilder(w);
final Node root = t.root("response");
for (ITxState tx : a) {
addTx(root, tx);
}
root.close();
/*
* TODO What is an appropriate cache strategy here?
*/
buildAndCommitResponse(resp, HttpServletResponse.SC_OK,
MIME_APPLICATION_XML, w.toString(), //
// disable caching.
new NV("Cache-Control", "no-cache")
/*
* Sets the cache behavior -- the data should be good for up to 60
* seconds unless you change the query parameters. These cache
* control parameters SHOULD indicate that the response is valid for
* 60 seconds, that the client must revalidate, and that the
* response is cachable even if the client was authenticated.
*/
// new NV("Cache-Control", "max-age=60, must-revalidate, public")//
);
}
/**
* Return <code>true</code> iff a transaction identifier was parsed from the
* request and otherwise commit a {@link HttpServletResponse#SC_BAD_REQUEST}
* response.
* <p>
* COMMIT-TX, ABORT-TX, PREPARE-TX, and STATUS-TX all need to extract the
* transaction identifier from the last component of the path which should be
* <code>/tx/txId</code>.
*
* @param req
* The request.
* @param resp
* The response.
* @param ref
* The transaction identifier will be saved in this reference.
* @return <code>true</code> if a transaction identifier was extracted. if
* <code>false</code> then no transaction identifier was found and a
* {@link HttpServletResponse#SC_BAD_REQUEST} response was committed.
*
* @throws IOException
*/
private final boolean getTxId(final HttpServletRequest req,
final HttpServletResponse resp, final AtomicLong ref)
throws IOException {
/*
* The path info follows the servlet and starts with /. So for
* "/bigdata/tx/559" this will be "/559". We strip of the leading "/" and
* the rest is the transaction identifier.
*/
final String pathInfo = req.getPathInfo();
assert pathInfo != null;
if (pathInfo.length() < 2) {
buildAndCommitResponse(resp, HttpServletResponse.SC_BAD_REQUEST,
MIME_TEXT_HTML, "No transaction identifier in path: pathInfo="
+ pathInfo);
return false;
}
// This should be the transaction identifier.
final String s = pathInfo.substring(1/* beginIndex */);
/*
* Validate the transaction identifier syntactically.
*/
for (int i = 0; i < s.length(); i++) {
if (!Character.isDigit(s.charAt(i)) && s.charAt(i) != '-') {
buildAndCommitResponse(resp, HttpServletResponse.SC_BAD_REQUEST,
MIME_TEXT_HTML,
"Transaction identifier is not numeric: pathInfo=" + pathInfo);
return false;
}
}
final long txId = Long.valueOf(s);
ref.set(txId);
return true;
}
/**
* Return the readsOnCommitTime associated with a transaction -or-
* <code>null</code> if the transaction is no longer active or if the backend
* is the scale-out architecture.
*
* @param txId
* The transaction identifier.
*
* @return The readsOnCommitTime if it is available and otherwise
* <code>null</code>.
*
* TODO This information is not available in scale-out. See <a
* href="http://trac.bigdata.com/ticket/#266" > Refactor native long
* tx id to thin object. </a>
*/
private Long getReadsOnCommitTimeOrNull(final long txId) {
if (getIndexManager() instanceof IBigdataFederation) {
return null;
}
final ITxState tx = ((Journal) getIndexManager())
.getLocalTransactionManager().getTx(txId);
if (tx == null) {
// Gone.
return null;
}
return tx.getReadsOnCommitTime();
}
private static void addTx(final Node parent, final ITxState tx)
throws IOException {
addTx(parent, tx.getStartTimestamp(), tx.getReadsOnCommitTime());
}
private static void addTx(final Node parent, final long txId,
final Long readsOnCommitTime) throws IOException {
if (txId == ITx.UNISOLATED) {
// Not a transaction identifier.
throw new IllegalArgumentException();
}
if (txId == ITx.READ_COMMITTED) {
// Not a transaction identifier.
throw new IllegalArgumentException();
}
/*
* Note: Since the scale-out architecture does not allow us to "GET" the
* status of a transaction, we can not readily obtain the ITxState object
* in scale-out. Therefore we rely on the txId to decide whether the tx is
* read-only (GT ZERO) or read-write (LT -1). The cases of 0L (UNISOALTED)
* and -1L (READ_COMMITTED) are handled above as they are not valid txIds.
*/
final boolean readOnly = txId > 0;
final Node t = parent.node("tx");
t.attr("txId", txId);
if (readsOnCommitTime != null) {
// Note: Not available in scale-out.
t.attr("readsOnCommitTime", readsOnCommitTime);
}
t.attr("readOnly", readOnly);
t.close();
}
// /**
// * Report a tuple containing the transaction identifier, a boolean response,
// * and elapsed time back to the user agent. The response is an XML document
// * as follows.
// *
// * <pre>
// * <data txId="txId" result="true|false" milliseconds="elapsed"/>
// * </pre>
// *
// * where <i>txId</i> is either the transaction identifier; <br/>
// * where <i>result</i> is either "true" or "false"; <br/>
// * where <i>elapsed</i> is the elapsed time in milliseconds for the request.
// *
// * @param resp
// * The response.
// * @param statusCode
// * The HTTP status code that will be associated with the response.
// * @param txId
// * The transaction identifier.
// * @param result
// * The outcome of the request.
// * @param elapsed
// * The elapsed time (milliseconds).
// *
// * @throws IOException
// */
// static protected void buildAndCommitTxBooleanResponse(
// final HttpServletResponse resp, final int statusCode, final long txId,
// final boolean result, final long elapsed) throws IOException {
//
// final StringWriter w = new StringWriter();
//
// final XMLBuilder t = new XMLBuilder(w);
//
// t.root("data").attr("txId", txId).attr("result", result)
// .attr("milliseconds", elapsed).close();
//
// buildAndCommitResponse(resp, statusCode, MIME_APPLICATION_XML, w.toString());
//
// }
}