com.bigdata.rdf.sail.webapp.client.RemoteTransactionManager Maven / Gradle / Ivy
/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2014. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
[email protected]
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.client;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.ext.DefaultHandler2;
/**
* Remote client for the Transaction Management API.
*
* @author bryan
* @since 1.5.2
*
* @see Support read/write
* transactions in the REST API
*/
public class RemoteTransactionManager {
/**
* The constant that SHOULD used to request a read/write transaction. The
* transaction will read on the current commit point on the database at the
* time that the request is processed by the server.
*/
public static final long UNISOLATED = 0L;
/**
* The constant that should be used to request a read-only transaction
* against the current commit point on the database at the time that the
* request is processed by the server.
*/
public static final long READ_COMMITTED = -1L;
/**
* Return true
iff the transaction identifier would be
* associated with a read/write transaction. This is a purely syntactic check
* of the numerical value of the transaction identifier. Negative transaction
* identifiers are read/write transactions.
*
* @param txId
* The transaction identifier.
*
* @return true
iff it is a read/write transaction.
*/
public static boolean isReadWriteTx(final long txId) {
return txId < READ_COMMITTED;
}
final private RemoteRepositoryManager mgr;
/**
* Flyweight constructor for stateless transaction manager client.
*
* @param remoteRepositoryManager
*/
public RemoteTransactionManager(
final RemoteRepositoryManager remoteRepositoryManager) {
if (remoteRepositoryManager == null)
throw new IllegalArgumentException();
this.mgr = remoteRepositoryManager;
}
private class RemoteTxState0 implements IRemoteTxState0 {
/**
* The transaction identifier.
*/
private final long txId;
/**
* The commit time on which the transaction will read.
*/
private final long readsOnCommitTime;
private RemoteTxState0(final long txId, final long readsOnCommitTime) {
if (txId == -1L) {
// This is the symbolic constant for a READ_COMMITTED operation. It
// is not a transaction identifier.
throw new IllegalArgumentException();
}
if (txId == 0L) {
// This is the symbolic constant for an UNISOLATED operation. It
// is not a transaction identifier.
throw new IllegalArgumentException();
}
this.txId = txId;
this.readsOnCommitTime = readsOnCommitTime;
}
@Override
public long getTxId() {
return txId;
}
@Override
public long getReadsOnCommitTime() {
return readsOnCommitTime;
}
@Override
public boolean isReadOnly() {
return false;
}
}
/**
* Note: It is not possible to establish a canonical factory pattern for a
* {@link RemoteTx} because we can not be certain that we are not accessing
* the same REST API through different URls and because we can not be certain
* that different URLs are not in fact the same REST API. Therefore there is
* nothing that will allow us to ensure that the returned {@link RemoteTx}
* objects are truely 1:1 with the transaction on the database. This means
* that {@link RemoteTx#lock} that serializes operations on the
* {@link RemoteTx} may be defeated by this factory if it is used for
* transactions discovered by LIST-TX
rather than just those
* known to be created for the client by CREATE-TX
*
* FIXME The [readsOnCommitTime] is not available for scale-out. Either allow
* [null] or use -1L if it is not available. Make this consistent in the
* server, client, and documentation.
*/
private class RemoteTx implements IRemoteTx {
/**
* The transaction identifier.
*/
private final long txId;
/**
* The commit time on which the transaction will read.
*/
private final long readsOnCommitTime;
/**
* Flag indicates whether the client believes the transaction to be
* active. Note that the transaction could have been aborted by the
* server, so the client's belief could be incorrect. The client uses this
* information to refuse to attempt operations if it believes that the
* transaction is not active.
*/
private final AtomicBoolean active = new AtomicBoolean(true);
/**
* Note: This object is used both to provide synchronization.
*/
private Object lock = this;
private RemoteTx(final IRemoteTxState0 tmp) {
final long txId = tmp.getTxId();
if (txId == -1L) {
// This is the symbolic constant for a READ_COMMITTED operation. It
// is not a transaction identifier.
throw new IllegalArgumentException();
}
if (txId == 0L) {
// This is the symbolic constant for an UNISOLATED operation. It
// is not a transaction identifier.
throw new IllegalArgumentException();
}
this.txId = txId;
this.readsOnCommitTime = tmp.getReadsOnCommitTime();
}
@Override
public long getTxId() {
return txId;
}
@Override
public long getReadsOnCommitTime() {
return readsOnCommitTime;
}
@Override
public boolean isReadOnly() {
/*
* Note: read/write transaction identifiers are negative numbers. -1L
* is a READ_COMMITTED operation, but is not allowed but our ctor. 0L
* is an UNISOLATED operation, but is not allowed by our ctor. Thus if
* the value is positive, we assume it is a read-only transaction
* identifier (as assigned by the remote transaction service and not
* just a timestamp) and if it is negative we assume it is a read/write
* transaction identifier (as assigned by the remote transaction
* service).
*/
return txId > 0;
}
/**
* Look at the {@link #active} flag and throw an exception if the client
* already believes that the transaction is not active.
*/
private void assertClientThinksTxActive() {
if (!active.get())
throw new RemoteTransactionNotFoundException(txId,
mgr.getBaseServiceURL());
}
@Override
public boolean isActive() {
return active.get();
}
@Override
public boolean prepare() throws RemoteTransactionNotFoundException {
synchronized (lock) {
assertClientThinksTxActive();
// tell the server to prepare the transaction.
return prepareTx(txId);
}
}
@Override
public void abort() throws RemoteTransactionNotFoundException {
synchronized (lock) {
assertClientThinksTxActive();
abortTx(txId);
active.set(false);
}
}
@Override
public void commit() throws RemoteTransactionValidationException,
RemoteTransactionNotFoundException {
synchronized (lock) {
assertClientThinksTxActive();
commitTx(txId);
active.set(false);
}
}
}
/**
* CREATE-TX: Create a transaction on the server.
*
* - {@link #UNISOLATED}
* - his requests a new read/write transaction. The transaction will read
* on the last commit point on the database at the time that the transaction
* was created. This is the default behavior if the timestamp parameter is
* not specified. Note: The federation architecture (aka scale-out) does NOT
* support distributed read/write transactions - all mutations in scale-out
* are shard-wise ACID.
* - {@link #READ_COMMITTED}
* - This requests a new read-only transaction. The transaction will read
* on the last commit point on the database at the time that the transaction
* was created.
* - A timestamp
* - This requests a new read-only transaction. The operation will be
* executed again the most recent committed state whose commit timestamp is
* less than or equal to timestamp.
*
*
* @param timestamp
* The timestamp used to indicate the type of transaction
* requested.
*
* @return The transaction object.
*/
public IRemoteTx createTx(final long timestamp) {
final ConnectOptions opts = new ConnectOptions(mgr.getBaseServiceURL()
+ "/tx");
opts.method = "POST";
opts.addRequestParam("timestamp", Long.toString(timestamp));
JettyResponseListener response = null;
try {
final JettyResponseListener listener = RemoteRepository
.checkResponseCode(response = mgr.doConnect(opts));
switch (listener.getStatus()) {
case 201:
return new RemoteTx(singleTxResponse(response));
default:
throw new HttpException(listener.getStatus(), "status="
+ listener.getStatus() + ", reason" + listener.getReason());
}
} catch (Exception t) {
throw new RuntimeException(t);
} finally {
if (response != null)
response.abort();
}
}
/**
* LIST-TX
: Return the set of active transactions.
*/
public Iterator listTx() {
final ConnectOptions opts = new ConnectOptions(mgr.getBaseServiceURL()
+ "/tx");
opts.method = "GET";
JettyResponseListener response = null;
try {
RemoteRepository.checkResponseCode(response = mgr.doConnect(opts));
// Note: iterator return supports streaming results (not implemented).
return multiTxResponse(response).iterator();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (response != null)
response.abort();
}
}
/**
* STATUS-TX: Return information about a transaction, including whether or
* not it is active.
*
* @param txId
* The transaction identifier.
*
* @return The {@link IRemoteTx} for that transaction.
*
* @throws RemoteTransactionNotFoundException
* if the transaction was not found on the server.
*
* @throws RemoteTransactionNotFoundException
*/
public IRemoteTxState0 statusTx(final long txId)
throws RemoteTransactionNotFoundException {
final ConnectOptions opts = new ConnectOptions(mgr.getBaseServiceURL()
+ "/tx/" + Long.toString(txId));
opts.method = "POST";
opts.addRequestParam("STATUS");
JettyResponseListener response = null;
try {
RemoteRepository.checkResponseCode(response = mgr.doConnect(opts));
return singleTxResponse(response);
} catch (HttpException ex) {
switch (ex.getStatusCode()) {
case 404: // GONE
throw new RemoteTransactionNotFoundException(txId,
mgr.getBaseServiceURL());
default: // Unexpected status code.
throw new RuntimeException(ex);
}
} catch(Exception t) {
throw new RuntimeException(t);
} finally {
if (response != null)
response.abort();
}
}
/**
* PREPARE-TX
: Validate a transaction on the server.
*
* @param txId
* The transaction identifier.
*
* @return true
if the transaction is read-only or if write set
* of the transaction was validated and false
iff the
* server was unable to validate a read-write transaction known to
* the server.
*
* @throws RemoteTransactionNotFoundException
* if there is no such transaction on the server.
*/
private boolean prepareTx(final long txId)
throws RemoteTransactionNotFoundException {
if (!isReadWriteTx(txId)) {
// NOP unless read/write transaction.
return true;
}
final ConnectOptions opts = new ConnectOptions(mgr.getBaseServiceURL()
+ "/tx/" + Long.toString(txId));
opts.method = "POST";
opts.addRequestParam("PREPARE");
JettyResponseListener response = null;
try {
final JettyResponseListener listener = RemoteRepository
.checkResponseCode(response = mgr.doConnect(opts));
switch (listener.getStatus()) {
case 200:
return true;
default:
throw new HttpException(listener.getStatus(), "status="
+ listener.getStatus() + ", reason" + listener.getReason());
}
} catch (HttpException ex) {
switch (ex.getStatusCode()) {
case 404: // GONE
throw new RemoteTransactionNotFoundException(txId,
mgr.getBaseServiceURL());
case 409: // CONFLICT
// validation failed.
return false;
default: // Unexpected status code.
throw new RuntimeException(ex);
}
} catch(Exception t) {
throw new RuntimeException(t);
} finally {
if (response != null)
response.abort();
}
}
/**
* ABORT-TX
*
* @param txId
* The transaction identifier.
*/
private void abortTx(final long txId)
throws RemoteTransactionNotFoundException {
final ConnectOptions opts = new ConnectOptions(mgr.getBaseServiceURL()
+ "/tx/" + Long.toString(txId));
opts.method = "POST";
opts.addRequestParam("ABORT");
JettyResponseListener response = null;
try {
final JettyResponseListener listener = RemoteRepository
.checkResponseCode(response = mgr.doConnect(opts));
switch (listener.getStatus()) {
case 200:
return;
default:
throw new HttpException(listener.getStatus(), "status="
+ listener.getStatus() + ", reason" + listener.getReason());
}
} catch (HttpException ex) {
switch (ex.getStatusCode()) {
case 404: // GONE
throw new RemoteTransactionNotFoundException(txId,
mgr.getBaseServiceURL());
default: // Unexpected status code.
throw new RuntimeException(ex);
}
} catch(Exception t) {
throw new RuntimeException(t);
} finally {
if (response != null)
response.abort();
}
}
/**
* COMMIT-TX:
*
* @param txId
* The transaction identifier.
*
* @throws RemoteTransactionNotFoundException
* if there is no such transaction on the server.
* @throws RemoteTransactionValidationException
* if the transaction exists but could not be validated.
*/
private void commitTx(final long txId)
throws RemoteTransactionNotFoundException,
RemoteTransactionValidationException {
final ConnectOptions opts = new ConnectOptions(mgr.getBaseServiceURL()
+ "/tx/" + Long.toString(txId));
opts.method = "POST";
opts.addRequestParam("COMMIT");
JettyResponseListener response = null;
try {
final JettyResponseListener listener = RemoteRepository
.checkResponseCode(response = mgr.doConnect(opts));
switch (listener.getStatus()) {
case 200:
return;
default:
throw new HttpException(listener.getStatus(), "status="
+ listener.getStatus() + ", reason" + listener.getReason());
}
} catch(HttpException ex) {
switch (ex.getStatusCode()) {
case 404: // GONE
throw new RemoteTransactionNotFoundException(txId,
mgr.getBaseServiceURL());
case 409: // CONFLICT
// Validation failed.
throw new RemoteTransactionValidationException(txId,
mgr.getBaseServiceURL());
default: // Unexpected status code.
throw new RuntimeException(ex);
}
} catch(Exception t) {
throw new RuntimeException(t);
} finally {
if (response != null)
response.abort();
}
}
private IRemoteTxState0 singleTxResponse(final JettyResponseListener response)
throws Exception {
try {
final String contentType = response.getContentType();
if (!contentType.startsWith(IMimeTypes.MIME_APPLICATION_XML)) {
throw new RuntimeException("Expecting Content-Type of "
+ IMimeTypes.MIME_APPLICATION_XML + ", not " + contentType);
}
final SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
final AtomicLong txId = new AtomicLong();
final AtomicLong readsOnCommitTime = new AtomicLong();
/*
* For example:
*/
parser.parse(response.getInputStream(), new DefaultHandler2() {
@Override
public void startElement(final String uri, final String localName,
final String qName, final Attributes attributes) {
if ("response".equals(qName)) {
// This is the outer element.
return;
}
if (!"tx".equals(qName))
throw new RuntimeException("Expecting: 'tx', but have: uri="
+ uri + ", localName=" + localName + ", qName=" + qName);
txId.set(Long.valueOf(attributes.getValue("txId")));
readsOnCommitTime.set(Long.valueOf(attributes
.getValue("readsOnCommitTime")));
}
});
// done.
return new RemoteTxState0(txId.get(), readsOnCommitTime.get());
} finally {
if (response != null) {
response.abort();
}
}
}
private List multiTxResponse(
final JettyResponseListener response) throws Exception {
try {
final String contentType = response.getContentType();
if (!contentType.startsWith(IMimeTypes.MIME_APPLICATION_XML)) {
throw new RuntimeException("Expecting Content-Type of "
+ IMimeTypes.MIME_APPLICATION_XML + ", not " + contentType);
}
final SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
final List list = new LinkedList();
/*
* For example:
*/
parser.parse(response.getInputStream(), new DefaultHandler2() {
@Override
public void startElement(final String uri, final String localName,
final String qName, final Attributes attributes) {
if ("response".equals(qName)) {
// This is the outer element.
return;
}
if (!"tx".equals(qName))
throw new RuntimeException("Expecting: 'tx', but have: uri="
+ uri + ", localName=" + localName + ", qName=" + qName);
final long txId = Long.valueOf(attributes.getValue("txId"));
final long readsOnCommitTime = Long.valueOf(attributes
.getValue("readsOnCommitTime"));
list.add(new RemoteTxState0(txId, readsOnCommitTime));
}
});
// done.
return list;
} finally {
if (response != null) {
response.abort();
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy