/*
* 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.resource;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Map;
import org.jboss.jbossts.star.provider.HttpResponseException;
import org.jboss.jbossts.star.util.*;
import org.jboss.logging.Logger;
import com.arjuna.ats.arjuna.common.Uid;
import com.arjuna.ats.arjuna.coordinator.AbstractRecord;
import com.arjuna.ats.arjuna.coordinator.RecordType;
import com.arjuna.ats.arjuna.coordinator.TwoPhaseOutcome;
import com.arjuna.ats.arjuna.state.InputObjectState;
import com.arjuna.ats.arjuna.state.OutputObjectState;
/**
* Log record for driving participants through 2PC and recovery
*/
public class RESTRecord extends AbstractRecord
{
protected final static Logger log = Logger.getLogger(RESTRecord.class);
private String participantURI;
// two phase aware participant completion URI
private String terminateURI;
// two phase unaware participant completion URIs
String commitURI;
String prepareURI;
String rollbackURI;
String commitOnePhaseURI;
private String coordinatorURI;
private String coordinatorID;
private TxStatus status;
private String txId;
private boolean prepared;
private String recoveryURI;
private long age = System.currentTimeMillis();
public RESTRecord() {
status = TxStatus.TransactionStatusUnknown;
}
public RESTRecord(String txId, String coordinatorURI, String participantURI, String terminateURI)
{
super(new Uid());
if (log.isTraceEnabled())
log.tracef("RESTRecord(%s, %s, %s, %s)", coordinatorURI, participantURI, terminateURI, txId);
this.participantURI = participantURI;
this.terminateURI = this.prepareURI = this.commitURI = this.rollbackURI = this.commitOnePhaseURI = terminateURI;
this.coordinatorURI = coordinatorURI;
this.txId = txId;
coordinatorID = get_uid().fileStringForm();
status = TxStatus.TransactionActive;
recoveryURI = "";
}
public RESTRecord(String txId, String coordinatorURI, String participantURI,
String commitURI, String prepareURI, String rollbackURI, String commitOnePhaseURI) {
this(txId, coordinatorURI, participantURI, null);
if (log.isTraceEnabled())
log.tracef("RESTRecord(%s, %s, %s, %s)", commitURI, prepareURI, rollbackURI, commitOnePhaseURI);
this.commitURI = commitURI;
this.prepareURI = prepareURI;
this.rollbackURI = rollbackURI;
this.commitOnePhaseURI = commitOnePhaseURI;
}
public String getCoordinatorURI() {
return coordinatorURI;
}
public String getTxId() {
return txId;
}
public String getRecoveryURI() {
return recoveryURI;
}
String getParticipantURI()
{
return participantURI;
}
public int typeIs()
{
return RecordType.RESTAT_RECORD;
}
public Object value()
{
return status.name();
}
public String getStatus() {
return status.name();
}
public long getAge() {
return age;
}
public void setValue(Object o)
{
}
public int nestedAbort()
{
return TwoPhaseOutcome.FINISH_OK;
}
public int nestedCommit()
{
return TwoPhaseOutcome.FINISH_OK;
}
/*
* Not sub-transaction aware.
*/
public int nestedPrepare()
{
return TwoPhaseOutcome.PREPARE_OK; // do nothing
}
private void check_suspend(Fault f)
{
if (fault.equals(f))
{
try
{
log.infof("%s: for 10 seconds", f);
Thread.sleep(10000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
private void check_halt(Fault f)
{
if (fault.equals(f))
{
log.infof("%s: halt VM", f);
Runtime.getRuntime().halt(1);
}
}
private int statusToOutcome()
{
return statusToOutcome(status);
}
private int statusToOutcome(TxStatus status)
{
try {
if (!status.equals(TxStatus.TransactionStatusUnknown))
return status.twoPhaseOutcome();
} catch (IllegalArgumentException e) {
if (log.isTraceEnabled())
log.trace("Participant returned unknown status");
}
return TwoPhaseOutcome.FINISH_ERROR;
}
public boolean forgetHeuristic() {
if (log.isTraceEnabled())
log.tracef("forgetting heuristic for %s", participantURI);
try {
new TxSupport().httpRequest(new int[] {HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_NO_CONTENT},
this.participantURI, "DELETE", null);
status = TxStatus.TransactionStatusUnknown;
} catch (HttpResponseException e) {
return false;
}
return super.forgetHeuristic();
}
public int topLevelPrepare()
{
if (log.isTraceEnabled())
log.tracef("prepare %s", prepareURI);
check_halt(Fault.prepare_halt);
check_suspend(Fault.prepare_suspend);
if (fault.equals(Fault.h_hazard))
return TwoPhaseOutcome.HEURISTIC_HAZARD;
if (prepareURI == null || txId == null)
return TwoPhaseOutcome.PREPARE_READONLY;
try
{
String body = new TxSupport().httpRequest(new int[] {HttpURLConnection.HTTP_OK}, this.prepareURI, "PUT",
TxMediaType.TX_STATUS_MEDIA_TYPE, TxSupport.toStatusContent(TxStatus.TransactionPrepared.name()));
if (body.isEmpty()) {
status = TxStatus.TransactionPrepared;
} else {
status = TxStatus.fromStatus(TxSupport.getStatus(body));
}
prepared = true;
int outcome = statusToOutcome();
if (outcome != TwoPhaseOutcome.FINISH_ERROR)
return outcome;
}
catch (HttpResponseException e)
{
if (checkFinishError(e.getActualResponse(), TxStatus.TransactionPrepared)) {
status = TxStatus.TransactionPrepared;
return TwoPhaseOutcome.PREPARE_OK;
} else {
status = TxStatus.TransactionRolledBack;
}
}
return TwoPhaseOutcome.PREPARE_NOTOK;
}
public int topLevelAbort()
{
if (log.isTraceEnabled())
log.debugf("trace %s", rollbackURI);
check_halt(Fault.abort_halt);
check_suspend(Fault.abort_suspend);
if (rollbackURI == null || txId == null)
return TwoPhaseOutcome.FINISH_ERROR;
try {
String body = new TxSupport().httpRequest(new int[] {HttpURLConnection.HTTP_OK}, this.rollbackURI, "PUT",
TxMediaType.TX_STATUS_MEDIA_TYPE, TxSupport.toStatusContent(TxStatus.TransactionRolledBack.name()));
if (body.isEmpty()) {
status = TxStatus.TransactionRolledBack;
} else {
status = TxStatus.fromStatus(TxSupport.getStatus(body));
}
} catch (HttpResponseException e) {
if (checkFinishError(e.getActualResponse(), TxStatus.TransactionRolledBack))
return TwoPhaseOutcome.FINISH_OK;
}
return statusToOutcome();
}
public int topLevelCommit()
{
if (log.isTraceEnabled())
log.tracef("commit %s", commitURI);
if (commitURI == null || txId == null)
return TwoPhaseOutcome.PREPARE_READONLY;
if (!prepared)
return TwoPhaseOutcome.NOT_PREPARED;
return doCommit(TxStatus.TransactionCommitted);
}
public int nestedOnePhaseCommit()
{
return TwoPhaseOutcome.FINISH_ERROR;
}
/**
* For commit_one_phase we can do whatever we want since the transaction
* outcome is whatever we want. Therefore, we do not need to save any
* additional recoverable state, such as a reference to the transaction
* coordinator, since it will not have an intentions list anyway.
*/
public int topLevelOnePhaseCommit()
{
return doCommit(TxStatus.TransactionCommittedOnePhase);
}
private int doCommit(TxStatus nextState)
{
TxSupport txs = new TxSupport();
check_halt(Fault.commit_halt);
check_suspend(Fault.commit_suspend);
if (txId == null)
return TwoPhaseOutcome.FINISH_ERROR;
try
{
if (log.isTraceEnabled())
log.tracef("committing %s", this.commitURI);
if (!TxStatus.TransactionReadOnly.equals(status)) {
txs = new TxSupport();
String body = txs.httpRequest(new int[] {HttpURLConnection.HTTP_OK}, this.commitURI, "PUT",
TxMediaType.TX_STATUS_MEDIA_TYPE, TxSupport.toStatusContent(nextState.name()));
if (body.isEmpty()) {
status = TxStatus.TransactionCommitted;
} else {
status = TxStatus.fromStatus(TxSupport.getStatus(body));
}
if (log.isTraceEnabled())
log.tracef("commit http status: %s RTS status: %s", txs.getStatus(), status);
} else {
status = TxStatus.TransactionCommitted;
}
if (log.isTraceEnabled())
log.tracef("COMMIT OK at commitURI: %s", this.commitURI);
}
catch (HttpResponseException e)
{
if (log.isDebugEnabled())
log.debugf(e, "commit exception: HTTP code: %s body: %s", e.getActualResponse(), txs.getBody());
// should result in the recovery system taking over
if (e.getActualResponse() == HttpURLConnection.HTTP_UNAVAILABLE) {
log.trace("Finishing with TwoPhaseOutcome.FINISH_ERROR");
return TwoPhaseOutcome.FINISH_ERROR;
} else {
checkFinishError(e.getActualResponse(), nextState);
status = TxStatus.fromStatus(txs.getBody());
}
}
return statusToOutcome(status);
}
private boolean checkFinishError(int expected, TxStatus nextState) throws HttpResponseException
{
if (expected == HttpURLConnection.HTTP_NOT_FOUND)
{
// the participant may have moved so check the coordinator participantURI
if (hasParticipantMoved())
{
if (log.isDebugEnabled())
log.debugf("participant has moved commit to new participantURI %s", this.participantURI);
String uri;
if (nextState.isCommit()) {
uri = commitURI;
} else if (nextState.isAbort()) {
uri = rollbackURI;
} else if (nextState.isPrepare()) {
uri = prepareURI;
} else if (nextState.isCommitOnePhase()) {
uri = commitOnePhaseURI;
} else {
status = TxStatus.TransactionActive;
return false;
}
try
{
TxSupport.getStatus(new TxSupport().httpRequest(new int[] {HttpURLConnection.HTTP_OK},
uri, "PUT", TxMediaType.TX_STATUS_MEDIA_TYPE,
TxSupport.toStatusContent(nextState.name())));
if (log.isDebugEnabled())
log.debug("Finish OK at new participantURI: %s" + this.participantURI);
status = nextState;
return true;
}
catch (HttpResponseException e1)
{
if (log.isTraceEnabled())
log.tracef(e1, "Finish still failing at new URI: ");
if (log.isInfoEnabled())
log.debugf("participant %s commit error: %s", this.participantURI, e1.getMessage());
}
}
}
status = TxStatus.TransactionActive;
return false;
}
/**
* A participant tells the coordinator if it changes its URL.
* To see if this has happened perform a GET on the recovery participantURI which returns the
* last known location of the participant.
* @return true if the participant did move
*/
private boolean hasParticipantMoved()
{
try
{
if (log.isTraceEnabled())
log.tracef("seeing if participant has moved: %s recoveryURI: %s", coordinatorID, recoveryURI);
if (recoveryURI.length() == 0)
return false;
// get the latest participant terminateURI (or URIs in the case of a Two Phase Unaware participant)
// by probing the recovery URI:
Map<String, String> links = new HashMap<String, String>();
new TxSupport().httpRequest(new int[] {HttpURLConnection.HTTP_OK}, recoveryURI, "GET",
TxMediaType.PLAIN_MEDIA_TYPE, null, links);
String terminateURI = links.get(TxLinkNames.PARTICIPANT_TERMINATOR);
if (links.containsKey(TxLinkNames.PARTICIPANT_TERMINATOR)) {
// participant has moved so remember the new location
this.participantURI = links.get(TxLinkNames.PARTICIPANT_RESOURCE);
}
if (terminateURI == null) {
// see if it is two phase unaware
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)
this.commitURI = commitURI;
if (prepareURI != null)
this.prepareURI = prepareURI;
if (rollbackURI != null)
this.rollbackURI = rollbackURI;
if (commitOnePhaseURI != null)
this.commitOnePhaseURI = commitOnePhaseURI;
if (log.isTraceEnabled())
log.tracef("... yes it has - new terminate URIs (commit, prepare, rollback and commit one phase)" +
" are %s %s %s %s",
commitURI != null ? commitURI : "",
prepareURI != null ? prepareURI : "",
rollbackURI != null ? rollbackURI : "",
commitOnePhaseURI != null ? commitOnePhaseURI : "");
if (this.commitURI != null && this.prepareURI != null && this.rollbackURI != null)
return true;
} else {
// terminator has moved so remember the new location
this.terminateURI = this.prepareURI = this.commitURI = this.rollbackURI = this.commitOnePhaseURI = terminateURI;
if (log.isTraceEnabled())
log.tracef("... yes it has - new terminateURI is %s", terminateURI);
return true;
}
}
catch (HttpResponseException e)
{
if (log.isTraceEnabled())
log.tracef(e, "participant has not moved: %s", e.getMessage());
}
return false;
}
public boolean save_state(OutputObjectState os, int t)
{
try
{
os.packString(txId);
os.packBoolean(prepared);
os.packString(participantURI);
os.packString(coordinatorURI);
os.packString(recoveryURI);
os.packString(coordinatorID);
os.packString(status.name());
os.packString(terminateURI);
os.packString(commitURI);
os.packString(prepareURI);
os.packString(rollbackURI);
os.packString(commitOnePhaseURI);
return super.save_state(os, t);
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
}
public boolean restore_state(InputObjectState os, int t)
{
try
{
txId = os.unpackString();
prepared = os.unpackBoolean();
participantURI = os.unpackString();
coordinatorURI = os.unpackString();
recoveryURI = os.unpackString();
coordinatorID = os.unpackString();
status = TxStatus.fromStatus(os.unpackString());
terminateURI = os.unpackString();
commitURI = os.unpackString();
prepareURI = os.unpackString();
rollbackURI = os.unpackString();
commitOnePhaseURI = os.unpackString();
if (commitURI == null) {
prepareURI = commitURI = rollbackURI = commitOnePhaseURI = terminateURI;
}
if (log.isInfoEnabled())
log.infof("restore_state %s", terminateURI);
return super.restore_state(os, t);
}
catch (Exception e)
{
return false;
}
}
public String type()
{
return RESTRecord.typeName();
}
public static String typeName()
{
return "/StateManager/AbstractRecord/RESTRecord";
}
public boolean doSave()
{
return true;
}
public void merge(AbstractRecord a)
{
}
public void alter(AbstractRecord a)
{
}
public boolean shouldAdd(AbstractRecord a)
{
return (a.typeIs() == typeIs());
}
public boolean shouldAlter(AbstractRecord a)
{
return false;
}
public boolean shouldMerge(AbstractRecord a)
{
return false;
}
public boolean shouldReplace(AbstractRecord a)
{
return false;
}
public void setRecoveryURI(String recoveryURI) {
this.recoveryURI = recoveryURI;
}
// TODO remove fault injection code - use byteman instead
enum Fault {
abort_halt, abort_suspend, prepare_halt,
prepare_suspend, commit_halt, commit_suspend,
h_commit, h_rollback, h_hazard, h_mixed, none
}
Fault fault = Fault.none;
public void setFault(String name)
{
for (Fault f : Fault.values())
{
if (f.name().equals(name))
{
log.tracef("setFault: %s participantURI: %s", f, participantURI);
fault = f;
return;
}
}
fault = Fault.none;
}
}