/*
* JBoss, Home of Professional Open Source
* Copyright 2010, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* (C) 2010
* @author JBoss Inc.
*/
package org.jboss.jbossts.star.service;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.concurrent.atomic.AtomicInteger;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import com.arjuna.ats.arjuna.common.Uid;
import com.arjuna.ats.arjuna.objectstore.RecoveryStore;
import com.arjuna.ats.arjuna.objectstore.StoreManager;
import com.arjuna.ats.arjuna.state.InputObjectState;
import com.arjuna.ats.internal.arjuna.common.UidHelper;
import org.jboss.jbossts.star.provider.ResourceNotFoundException;
import org.jboss.jbossts.star.provider.TransactionStatusException;
import org.jboss.jbossts.star.resource.RESTRecord;
import org.jboss.jbossts.star.resource.RecoveringTransaction;
import org.jboss.jbossts.star.resource.Transaction;
import org.jboss.jbossts.star.util.*;
import org.jboss.jbossts.star.util.media.txstatusext.*;
import org.jboss.logging.Logger;
import com.arjuna.ats.arjuna.AtomicAction;
import com.arjuna.ats.arjuna.coordinator.ActionStatus;
@Path(TxSupport.TX_PATH)
public class Coordinator
{
static final String RC_SEGMENT = "recovery-coordinator";
protected final static Logger log = Logger.getLogger(Coordinator.class);
private final static String REST_TXN_TYPE = new AtomicAction().type();
private static Map<String, Transaction> transactions = new ConcurrentHashMap<String, Transaction>();
// each participant may only be enlisted in one transaction - map each registration id to a map of
// link name to link uri
private static Map<String, HashMap<String, String>> participants = new ConcurrentHashMap<String, HashMap<String, String>>();
private static Map<String, RecoveringTransaction> recoveringTransactions = getRecoveringTransactions(transactions);
private static final AtomicInteger active = new AtomicInteger(0);
private static final AtomicInteger prepared = new AtomicInteger(0);
private static final AtomicInteger committed = new AtomicInteger(0);
private static final AtomicInteger aborted = new AtomicInteger(0);
private static long age = System.currentTimeMillis();
/**
* Performing a GET on the transaction-manager returns a list of all transaction URIs
* known to the coordinator (active and in recovery) separated by
* the @see TxSupport.URI_SEPARATOR character
* @param info http context of the request
* @return JSON representation of active transactions and HTTP status code
*/
@GET
@Path(TxSupport.TX_SEGMENT)
@Produces(TxMediaType.TX_LIST_MEDIA_TYPE)
public Response getAllTransactions(@Context UriInfo info)//, String content)
{
log.trace("coordinator: list: transaction-coordinator");
StringBuilder txns = new StringBuilder();
Iterator<String> i;
String statisticsUri = TxSupport.extractUri(info, TxLinkNames.STATISTICS);
updateTransactions();
i = transactions.keySet().iterator();
while (i.hasNext()) {
URI uri = TxSupport.getUri(info, info.getPathSegments().size(), i.next());
txns.append(uri.toASCIIString());
if (i.hasNext())
txns.append(TxSupport.URI_SEPARATOR);
}
/*
* Note, to return only recovering or only active txns use:
* getTransactions(recoveringFilter); or
* getTransactions(activeTxFilter);
*/
Response.ResponseBuilder builder = Response.ok(txns.toString());
builder.header("Content-Length", txns.length());
TxSupport.setLinkHeader(builder, "Transaction Statistics", TxLinkNames.STATISTICS,
statisticsUri, TxMediaType.TX_STATUS_EXT_MEDIA_TYPE);
return builder.build();
}
/**
* Performing a GET on the transaction-manager URI with media type application/txstatusext+xml
* returns extended information about the transaction-manager resource such as how long it has
* been up and all transaction-coordinator URIs.
*
* @param info Request context
* @return TransactionManagerElement
*/
@GET
@Path(TxSupport.TX_SEGMENT)
@Produces(TxMediaType.TX_STATUS_EXT_MEDIA_TYPE)
public TransactionManagerElement getTransactionManagerInfo(@Context UriInfo info) {
TransactionManagerElement tm = new TransactionManagerElement();
updateTransactions();
for (String s : transactions.keySet()) {
URI uri = TxSupport.getUri(info, info.getPathSegments().size(), s);
tm.addCoordinator(uri.toASCIIString());
}
tm.setCreated(new Date(age));
tm.setStatistics(new TransactionStatisticsElement(
active.get(), prepared.get(), committed.get(), aborted.get()));
return tm;
}
/**
* Performing a GET on the transaction-manager URI sufficed with /statistics
* returns statistics of the transaction manager.
* Numbers of active, prepared, committed, and aborted transactions are returned.
*
* @return TransactionStatisticsElement
*/
@GET
@Path(TxSupport.TX_SEGMENT + TxLinkNames.STATISTICS)
@Produces(TxMediaType.TX_STATUS_EXT_MEDIA_TYPE)
public TransactionStatisticsElement getTransactionStatistics() {
// for prepared we could go through every transaction and get its status
return new TransactionStatisticsElement(
active.get(), prepared.get(), committed.get(), aborted.get());
}
/**
* Obtain the transaction terminator and participant enlistment URIs for the
* specified transaction id. These are returned in link headers in the same
* way they were returned when the transaction was started @see Coordinator#beginTransaction
*
* @param info request context
* @param id URL template parameter for the transaction id
* @return http response
*/
@HEAD
@Path(TxSupport.TX_SEGMENT + "{id}")
@Produces(TxMediaType.TX_LIST_MEDIA_TYPE)
public Response getTransactionURIs(@Context UriInfo info, @PathParam("id") String id)
{
log.tracef("coordinator txn head request for txn %s", id);
getTransaction(id); // throws an exception if the transaction does not exist
String terminator = TxLinkNames.TERMINATOR;
Response.ResponseBuilder builder = Response.ok();
TxSupport.addLinkHeader(builder, info, terminator, terminator, terminator);
TxSupport.addLinkHeader(builder, info, TxLinkNames.PARTICIPANT, TxLinkNames.PARTICIPANT);
return builder.build();
}
/**
* Performing a GET on the transaction url returns its status
* @see org.jboss.jbossts.star.util.TxStatusMediaType#TX_ACTIVE
* etc for the format of the returned content
* @param info request context
* @param id URL template parameter for the id of the transaction
* @return content representing the status of the transaction
*/
@GET
@Path(TxSupport.TX_SEGMENT + "{id}")
@Produces(TxMediaType.TX_STATUS_MEDIA_TYPE)
public Response getTransactionStatus(@Context UriInfo info, @PathParam("id") String id)
{
log.tracef("coordinator: status: transaction-coordinator/%s", id);
Transaction txn = getTransaction(id);
Response.ResponseBuilder builder = Response.ok(TxSupport.toStatusContent(txn.getStatus()));
return addTransactionHeaders(builder, info, txn, false).build();
}
/**
* Performing a GET on the transaction URL with media type application/txstatusext+xml
* returns extended information about the transaction, such as its status,
* number of participants, and their individual URIs.
*
* @param info Request context
* @param id URL template parameter for the id of the transaction
* @return HTTP response representing extended transaction status information
*/
@GET
@Path(TxSupport.TX_SEGMENT + "{id}")
@Produces(TxMediaType.TX_STATUS_EXT_MEDIA_TYPE)
public Response getTransactionExtStatus(@Context UriInfo info, @PathParam("id") String id)
{
log.tracef("coordinator: status: transaction-coordinator/%s", id);
Transaction txn = getTransaction(id);
String terminator = TxLinkNames.TERMINATOR;
Collection<String> enlistmentIds = new ArrayList<String>();
CoordinatorElement coordinatorElement = txn.toXML();
String txnURI = TxSupport.getUri(info, info.getPathSegments().size()).toASCIIString();
URI terminateURI = TxSupport.getUri(info, info.getPathSegments().size(),terminator);
URI volatileURI = TxSupport.getUri(info, info.getPathSegments().size(), TxLinkNames.VOLATILE_PARTICIPANT);
coordinatorElement.setTxnURI(txnURI);
coordinatorElement.setTerminateURI(terminateURI.toASCIIString());
coordinatorElement.setDurableParticipantEnlistmentURI(txnURI);
coordinatorElement.setVolatileParticipantEnlistmentURI(volatileURI.toASCIIString());
txn.getParticipants(enlistmentIds);
for (String enlistmentId : enlistmentIds) {
Map<String, String> participantInfo = participants.get(enlistmentId);
String terminatorURI = participantInfo.get(TxLinkNames.PARTICIPANT_TERMINATOR);
String participantURI = participantInfo.get(TxLinkNames.PARTICIPANT_RESOURCE);
String recoveryURI = participantInfo.get(TxLinkNames.PARTICIPANT_RECOVERY);
TwoPhaseAwareParticipantElement participantElement = new TwoPhaseAwareParticipantElement();
participantElement.setTerminatorURI(terminatorURI);
participantElement.setRecoveryURI(recoveryURI);
participantElement.setResourceURI(participantURI);
txn.getStatus(participantElement, participantURI);
coordinatorElement.addTwoPhaseAware(participantElement);
}
return addTransactionHeaders(Response.ok(coordinatorElement), info, txn, false).build();
}
/**
* Performing a DELETE on the transaction-coordinator URL will return a 403.
* @param id transaction id
* @return 403
*/
@SuppressWarnings({"UnusedDeclaration"})
@DELETE
@Path(TxSupport.TX_SEGMENT + "{id}")
public Response deleteTransaction(@PathParam("id") String id)
{
return Response.status(HttpURLConnection.HTTP_FORBIDDEN).build();
}
// Performing HEAD, GET, POST, DELETE and OPTIONS on the transaction
// url generates a 400 status code
@SuppressWarnings({"UnusedDeclaration"})
@HEAD @Path(TxSupport.TX_SEGMENT + "{TxId}/" + TxLinkNames.TERMINATOR)
public Response tt1(@PathParam("TxId")String txId) {
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).build();
}
@SuppressWarnings({"UnusedDeclaration"})
@GET @Path(TxSupport.TX_SEGMENT + "{TxId}/" + TxLinkNames.TERMINATOR)
public Response tt2(@PathParam("TxId")String txId) {
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).build();
}
@SuppressWarnings({"UnusedDeclaration"})
@POST @Path(TxSupport.TX_SEGMENT + "{TxId}/" + TxLinkNames.TERMINATOR)
public Response tt3(@PathParam("TxId")String txId) {
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).build();
}
@SuppressWarnings({"UnusedDeclaration"})
@DELETE @Path(TxSupport.TX_SEGMENT + "{TxId}/" + TxLinkNames.TERMINATOR)
public Response tt4(@PathParam("TxId")String txId) {
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).build();
}
@SuppressWarnings({"UnusedDeclaration"})
@OPTIONS @Path(TxSupport.TX_SEGMENT + "{TxId}/" + TxLinkNames.TERMINATOR)
public Response tt5(@PathParam("TxId")String txId) {
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).build();
}
private Response.ResponseBuilder addTransactionHeaders(Response.ResponseBuilder builder, UriInfo info,
Transaction tx, boolean includeTxId) {
String uid = tx.get_uid().fileStringForm();
String terminator = TxLinkNames.TERMINATOR;
String participant = TxLinkNames.PARTICIPANT;
String vparticipant = TxLinkNames.VOLATILE_PARTICIPANT;
if (includeTxId) {
TxSupport.addLinkHeader(builder, info, terminator, terminator, uid, terminator);
TxSupport.addLinkHeader(builder, info, participant, participant, uid);
TxSupport.addLinkHeader(builder, info, vparticipant, vparticipant, uid, vparticipant);
} else {
TxSupport.addLinkHeader(builder, info, terminator, terminator, terminator);
TxSupport.addLinkHeader(builder, info, participant, participant);
TxSupport.addLinkHeader(builder, info, vparticipant, vparticipant, vparticipant);
}
return builder;
}
/**
*
* Performing a POST on Transaction Manager URL @see TxSupport.TX_SEGMENT with no content
* as shown below will start a new transaction with a default timeout.
* A successful invocation will return 201 and the Location header MUST contain the URI
* of the newly created transaction resource, which we will refer to as the transaction-coordinator
* in the rest of this specification. Two related URLs MUST also be returned,
* one for use by the terminator of the transaction (typically referred to as the client)
* and one used for registering durable participation in the transaction (typically referred
* to as the server). These linked URLs can be of arbitrary format.
* The rel names for the links are:
* @see org.jboss.jbossts.star.util.TxLinkNames#TERMINATOR and @see TxLinkNames#PARTICIPANT
*
* @param info uri context
* @param headers http headers
* @param content empty if no transaction timeout is required otherwise the number of milliseconds
* after which the transaction is eligible for being timed out. The content should have the format
* TxSupport#TIMEOUT_PROPERTY milliseconds
* @return http status code
*/
@SuppressWarnings({"UnusedDeclaration"})
@POST
@Path(TxSupport.TX_SEGMENT)
@Consumes(TxMediaType.POST_MEDIA_TYPE)
// @Produces("application/vnd.rht.txstatus+text;version=0.1")
public Response beginTransaction(@Context UriInfo info, @Context HttpHeaders headers, @DefaultValue("") String content)
{
log.tracef("coordinator: POST /transaction-manager content: %s", content);
Transaction tx = new Transaction(this, "coordinator");
int timeout = TxSupport.getIntValue(content, TxMediaType.TIMEOUT_PROPERTY, 0); // default is 0 - never timeout
String uid = tx.get_uid().fileStringForm();
log.tracef("coordinator: timeout=%d", timeout);
transactions.put(uid, tx);
// round up the timeout from milliseconds to seconds
if (timeout != 0) {
if (timeout < 0)
timeout = 0;
else
timeout /= 1000;
}
int status = tx.begin(timeout);
try {
if (status == ActionStatus.RUNNING) {
active.incrementAndGet();
URI uri1 = TxSupport.getUri(info, info.getPathSegments().size(), uid);
Response.ResponseBuilder builder = Response.created(uri1);
return addTransactionHeaders(builder, info, tx, true).build();
}
throw new TransactionStatusException("Transaction failed to start: " + status);
} catch (Exception e) {
log.debugf(e, "begin");
throw new TransactionStatusException("Transaction failed to start: " + e);
}
finally
{
AtomicAction.suspend();
}
}
/**
* The client can control the outcome of the transaction by by PUTing to the terminator
* URL returned as a response to the original transaction create request.
* Upon termination, the resource and all associated resources are implicitly deleted.
* For any subsequent invocation then an implementation MAY return 410 if the implementation
* records information about transactions that have completed, otherwise it should return 404
* (not necessary for presumed rollback semantics) but at a minimum MUST return 401.
* The invoker can assume this was a rollback. In order for an interested party to know for
* sure the outcome of a transaction then it MUST be registered as a participant with the
* transaction coordinator.
*
* @param txId URL template component containing the transaction identifier
* @param fault mechanism for injecting faults TODO use byteman instead
* @param content body of the request indicating a commit or abort request
* @see TxStatus#TransactionCommitted etc
* @return http response code
*/
@PUT
@Path(TxSupport.TX_SEGMENT + "{TxId}/" + TxLinkNames.TERMINATOR)
public Response terminateTransaction(@PathParam("TxId")String txId, @QueryParam("fault") @DefaultValue("")String fault, String content)
{
log.tracef("coordinator: commit: transaction-manager/%s/terminator : content: %s", txId, content);
Transaction tx = getTransaction(txId);
String how = TxSupport.getStringValue(content, TxStatusMediaType.STATUS_PROPERTY);
TxStatus currentStatus = tx.getTxStatus();
String status;
int scRes;
/*
* 275If the transaction no longer exists then an implementation MAY return 410 if the implementation
* 276records information about transactions that have rolled back, (not necessary for presumed
* 277rollback semantics) but at a minimum MUST return 404.
*/
if (!currentStatus.isRunning() && !currentStatus.isRollbackOnly())
return Response.status(HttpURLConnection.HTTP_PRECON_FAILED).build();
/*
ABORT_ONLY is not in the spec for the same reasons as it's not in the WS-TX and WS-CAF where
it is assumed that only the txn originator can end the tx:
- simply register a synchronization in the transaction that prevented a commit from happening;
and I haven't implemented synchronisations yet.
It is unclear why allowing similar functionality via synchronisations doesn't open up a similar
security hole.
*/
TxStatus txStatus = TxStatus.fromStatus(how);
tx.setFault(fault);
AtomicAction.resume(tx);
switch (txStatus) {
case TransactionCommitted:
prepared.incrementAndGet();
status = tx.getStatus(tx.commit(true));
break;
case TransactionRolledBack:
prepared.incrementAndGet();
status = tx.getStatus(tx.abort());
break;
case TransactionRollbackOnly:
tx.preventCommit();
status = tx.getStatus();
break;
default:
AtomicAction.suspend();
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).build();
}
AtomicAction.suspend();
log.tracef("terminate result: %s", status);
/* if (tx.isRunning())
throw new TransactionStatusException("Transaction failed to terminate");*/
// Cleanup is done as part of the org.jboss.jbossts.star.resource.Transaction.afterCompletion()
scRes = status.length() == 0 ? HttpURLConnection.HTTP_INTERNAL_ERROR : HttpURLConnection.HTTP_OK;
return Response.status(scRes).entity(TxSupport.toStatusContent(status)).build();
}
public void removeTxState(int status, Transaction tx, final Collection<String> enlistmentIds) {
String txId = tx.get_uid().fileStringForm();
transactions.remove(txId);
prepared.decrementAndGet();
active.decrementAndGet();
if (status == ActionStatus.COMMITTED)
committed.incrementAndGet();
else if (status == ActionStatus.ABORTED)
aborted.incrementAndGet();
if(enlistmentIds == null) {
// Cleanup synchronization could not pass in the participants (tx timed out)
// locate the enlistment ids
Iterator<Entry<String, HashMap<String, String>>> j = participants.entrySet().iterator();
while (j.hasNext()) {
Map.Entry<java.lang.String, HashMap<String, String>> entry = j.next();
HashMap<String, String> linkHolder = entry.getValue();
String participantTxId = linkHolder.get(TxLinkNames.TRANSACTION);
if (txId.equals(participantTxId)) {
j.remove();
}
}
}
else {
for (String enlistmentId : enlistmentIds) {
participants.remove(enlistmentId);
}
}
}
/**
* Register a participant in a tx
* @param linkHeader link header
* @param info URI info
* @param txId id of transaction
* @param content body of request containing URI for driving the participant through completion
* (the URI should be unique within the scope of txId)
* @return unique resource ref for the participant
*/
@POST
@Path(TxSupport.TX_SEGMENT + "{TxId}")
public Response enlistParticipant(@HeaderParam("Link") String linkHeader, @Context UriInfo info,
@PathParam("TxId")String txId, String content)
{
log.tracef("enlistParticipant request uri %s txid: %s Link: %s content: %s",
info.getRequestUri(), txId,
linkHeader != null ? linkHeader : "null",
content != null ? content : "null");
Transaction tx = getTransaction(txId);
/*
* If the transaction is not TransactionActive then the implementation MUST return a 412 status
* code
*/
if (!tx.isRunning())
return Response.status(HttpURLConnection.HTTP_PRECON_FAILED).build();
Map<String, String> links = TxSupport.decodeLinkHeader(linkHeader);
// Map<String, String> links = new HashMap<String, String>();
// for (Map.Entry<String, Link> link : linkHeader.getLinksByRelationship().entrySet())
// links.put(link.getKey(), link.getValue().getHref());
if (links.containsKey(TxLinkNames.VOLATILE_PARTICIPANT))
tx.addVolatileParticipant(links.get(TxLinkNames.VOLATILE_PARTICIPANT));
String participantURI = links.get(TxLinkNames.PARTICIPANT_RESOURCE);
String terminatorURI = links.get(TxLinkNames.PARTICIPANT_TERMINATOR);
if (participantURI == null)
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST)
.entity("Missing Enlistment Link Header").build();
String txURI = TxSupport.buildURI(info.getBaseUriBuilder(), info.getPathSegments().get(0).getPath(),
info.getPathSegments().get(1).getPath());
UriBuilder builder = info.getBaseUriBuilder();
String recoveryUrlBase = builder.path(info.getPathSegments().get(0).getPath()).path(RC_SEGMENT)
.build().toString() + '/';
String coordinatorId;
if (tx.isEnlisted(participantURI))
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST)
.entity("participant is already enlisted").build();
if (terminatorURI == null) {
String commitURI = links.get(TxLinkNames.PARTICIPANT_COMMIT);
String prepareURI = links.get(TxLinkNames.PARTICIPANT_PREPARE);
String rollbackURI = links.get(TxLinkNames.PARTICIPANT_ROLLBACK);
String commitOnePhaseURI = links.get(TxLinkNames.PARTICIPANT_COMMIT_ONE_PHASE);
if (commitURI == null || prepareURI == null || rollbackURI == null)
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST)
.entity("Missing TwoPhaseUnawareLink Header").build();
coordinatorId= tx.enlistParticipant(txURI, participantURI, recoveryUrlBase,
commitURI, prepareURI, rollbackURI, commitOnePhaseURI);
} else {
coordinatorId = tx.enlistParticipant(txURI, participantURI, recoveryUrlBase, terminatorURI);
}
if (coordinatorId == null) // the request was rejected (2PC processing must have started)
return Response.status(HttpURLConnection.HTTP_FORBIDDEN).entity("2PC has started").build();
links.put(TxLinkNames.PARTICIPANT_RECOVERY, tx.getRecoveryUrl());
links.put(TxLinkNames.TRANSACTION, txId);
participants.put(coordinatorId, new HashMap<String, String>(links));
log.debug("enlisted participant: content=" + content + " in tx " + txId + " Coordinator url base: " + recoveryUrlBase);
return Response.created(URI.create(tx.getRecoveryUrl())).build();
}
/**
* Register a volatile participant in a tx
*
* @param linkHeader link header
* @param info URI info
* @param txId id of transaction
* @return HTTP status code
*/
@PUT
@Path(TxSupport.TX_SEGMENT + "{TxId}/" + TxLinkNames.VOLATILE_PARTICIPANT)
public Response enlistVolatileParticipant(@HeaderParam("Link") String linkHeader, @Context UriInfo info,
@PathParam("TxId")String txId) {
log.tracef("enlistParticipant request uri %s txid: %s", info.getRequestUri(), txId);
Transaction tx = getTransaction(txId);
if (tx.isFinishing())
return Response.status(HttpURLConnection.HTTP_PRECON_FAILED).build();
Map<String, String> links = TxSupport.decodeLinkHeader(linkHeader);
String vparticipantURI = links.get(TxLinkNames.VOLATILE_PARTICIPANT);
if (vparticipantURI == null)
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST)
.entity("Missing Enlistment Link Header").build();
tx.addVolatileParticipant(vparticipantURI);
return Response.ok().build();
}
/**
* Get the participant url (registered during enlistParticipant) corresponding to a resource reference
* if the coordinator crashes - the participant list will be empty but this is ok if commit hasn't been
* called since the TM uses presumed abort semantics.
*
* @param txId transaction id that this recovery url belongs to
* @param enlistmentId the resource reference
* @return the participant url
*/
@GET
@Path(RC_SEGMENT + "/{TxId}/{RecCoordId}")
public Response lookupParticipant(@PathParam("TxId")String txId, @PathParam("RecCoordId")String enlistmentId)
{
log.tracef("coordinator: lookup: transaction-coordinator: %s/%s", txId, enlistmentId);
HashMap<String, String> p = participants.get(enlistmentId);
if (p == null)
return Response.status(HttpURLConnection.HTTP_NOT_FOUND).build();
String linkHeader = new TxSupport().makeTwoPhaseParticipantLinkHeader(p);
if (linkHeader == null)
return Response.status(HttpURLConnection.HTTP_NOT_FOUND).build();
return Response.ok().header("Link", linkHeader).build();
}
/**
* PUT /recovery-coordinator/_RecCoordId_/_new participant URL_ -
* overwrite the old participant URL with new participant URL
* (as with JTS, this will also trigger off a recovery attempt on the associated transaction)
* A participant may use this url to notifiy the coordinator that he has moved to a new location.
*
* @param linkHeader link header containing participant links
* @param txId transaction id that this recovery url belongs to
* @param enlistmentId id by the participant is known
* @return http status code
*/
@PUT
@Path(RC_SEGMENT + "/{TxId}/{RecCoordId}")
public Response replaceParticipant(@HeaderParam("Link") String linkHeader, @PathParam("TxId")String txId,
@PathParam("RecCoordId")String enlistmentId)
{
Map<String, String> links = TxSupport.decodeLinkHeader(linkHeader);
String terminatorUrl = links.get(TxLinkNames.PARTICIPANT_TERMINATOR);
String participantUrl = links.get(TxLinkNames.PARTICIPANT_RESOURCE);
if (participantUrl == null)
return Response.status(HttpURLConnection.HTTP_BAD_REQUEST).entity("Missing Link Header").build();
log.tracef("coordinator: replace: recovery-coordinator/%s?URL=%s", enlistmentId, terminatorUrl);
// check whether the transaction or log still exists
Transaction tx = getTransaction(txId); // throws not found exception if the txn has finished
links.put(TxLinkNames.TRANSACTION, txId);
links.put(TxLinkNames.PARTICIPANT_RECOVERY, tx.getRecoveryUrl());
participants.put(enlistmentId, new HashMap<String, String>(links));
return Response.status(HttpURLConnection.HTTP_OK).build();
}
@POST
@Path(RC_SEGMENT + "/{RecCoordId}")
public Response postParticipant(@PathParam("RecCoordId")String enlistmentId)
{
log.tracef("coordinator: replace via Post: recovery-coordinator/%s", enlistmentId);
return Response.status(HttpURLConnection.HTTP_UNAUTHORIZED).build();
}
/**
* Performing DELETE on participant's recovery URL removes participant from the transaction.
*
* @param enlistmentId The resource reference
* @return HTTP status code
*/
@DELETE
@Path(RC_SEGMENT + "/{RecCoordId}")
public Response deleteParticipant(@PathParam("RecCoordId")String enlistmentId)
{
log.tracef("coordinator: participant leaving via Delete: recovery-coordinator/%s", enlistmentId);
HashMap<String, String> p = participants.get(enlistmentId);
Transaction txn;
if (p == null || (txn = transactions.get(p.get(TxLinkNames.TRANSACTION))) == null)
return Response.status(HttpURLConnection.HTTP_NOT_FOUND).build();
if (txn.forgetParticipant(p.get(TxLinkNames.PARTICIPANT_RESOURCE)))
return Response.status(HttpURLConnection.HTTP_OK).build();
return Response.status(HttpURLConnection.HTTP_CONFLICT).build();
}
private Transaction getTransaction(String txId)
{
Transaction tx = transactions.get(txId);
if (tx == null) {
updateTransactions();
if ((tx = transactions.get(txId)) == null)
throw new ResourceNotFoundException("Transaction id not found");
}
return tx;
}
// Look up all log records for a given type
private static Set<Uid> getUids(Set<Uid> uids, String type) {
try {
RecoveryStore recoveryStore = StoreManager.getRecoveryStore();
InputObjectState states = new InputObjectState();
if (recoveryStore.allObjUids(type, states) && states.notempty()) {
boolean finished = false;
do {
Uid uid = UidHelper.unpackFrom(states);
if (uid.notEquals(Uid.nullUid())) {
uids.add(uid);
} else {
finished = true;
}
} while (!finished);
}
} catch (Exception e) {
e.printStackTrace();
}
return uids;
}
private void updateTransactions() {
Map<String, RecoveringTransaction> txns = new HashMap<String, RecoveringTransaction>(recoveringTransactions);
// remove all those uids that are still recovering
for (Uid uid : getUids(new HashSet<Uid>(), REST_TXN_TYPE)) {
txns.remove(uid.fileStringForm());
}
// the remaining entries must have been recovered
for (String txId : txns.keySet()) {
recoveringTransactions.remove(txId);
transactions.remove(txId);
}
}
private static Map<String, RecoveringTransaction> getRecoveringTransactions(Map<String, Transaction> transactions) {
Map<String, RecoveringTransaction> recoveringTransactions = new ConcurrentHashMap<String, RecoveringTransaction>();
for (Uid uid : getUids(new HashSet<Uid>(), REST_TXN_TYPE)) {
String key = uid.fileStringForm();
RecoveringTransaction txn = new RecoveringTransaction(uid);
try {
// the recoverable transaction contains the recovery urls of each of its participants
// so it needs activate in order to make it available to anyone that wants to obtain it:
if (txn.activate()) {
for (RESTRecord r : txn.getParticipants(new ArrayList<RESTRecord>())) {
Map<String, String> links = new HashMap<String, String>();
links.put(TxLinkNames.PARTICIPANT_RECOVERY, r.getRecoveryURI());
links.put(TxLinkNames.TRANSACTION, r.getTxId());
participants.put(r.getCoordinatorURI(), new HashMap<>(links));
}
}
} catch (Throwable e) {
log.warnf("Could not reactivate pending transaction %s (reason: %s)", txn.get_uid(), e.getMessage());
}
recoveringTransactions.put(key, txn);
transactions.put(key, txn);
}
return recoveringTransactions;
}
}