package org.jboss.narayana.rest.integration; import com.arjuna.ats.arjuna.common.Uid; import com.arjuna.ats.arjuna.exceptions.ObjectStoreException; 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.arjuna.state.OutputObjectState; import com.arjuna.ats.internal.arjuna.common.UidHelper; import org.jboss.jbossts.star.util.TxLinkNames; import org.jboss.jbossts.star.util.TxMediaType; import org.jboss.logging.Logger; import org.jboss.narayana.rest.integration.api.HeuristicException; import org.jboss.narayana.rest.integration.api.Participant; import org.jboss.narayana.rest.integration.api.ParticipantDeserializer; import org.jboss.narayana.rest.integration.api.ParticipantException; import org.jboss.narayana.rest.integration.api.ParticipantsManagerFactory; import org.jboss.narayana.rest.integration.api.PersistableParticipant; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.core.Link; import javax.ws.rs.core.Response; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author <a href="mailto:gytis@redhat.com">Gytis Trikleris</a> */ public final class RecoveryManager { private static final String PARTICIPANT_INFORMATION_RECORD_TYPE = "/REST-AT/Integration/ParticipantInformationRecoveryRecord"; private static final Logger LOG = Logger.getLogger(RecoveryManager.class); private static final RecoveryManager INSTANCE = new RecoveryManager(); private final Map<String, ParticipantDeserializer> deserializers = new ConcurrentHashMap<String, ParticipantDeserializer>(); private RecoveryManager() { } public static RecoveryManager getInstance() { return INSTANCE; } public void registerDeserializer(final String applicationId, final ParticipantDeserializer deserializer) { if (LOG.isTraceEnabled()) { LOG.trace("RecoveryManager.registerDeserializer: applicationId=" + applicationId + ", deserializer=" + deserializer); } if (!deserializers.containsKey(applicationId)) { deserializers.put(applicationId, deserializer); recoverParticipants(); } } public void persistParticipantInformation(final ParticipantInformation participantInformation) { if (LOG.isTraceEnabled()) { LOG.trace("RecoveryManager.persistParticipantInformation: participantInformation=" + participantInformation); } if (!isRecoverableParticipant(participantInformation.getParticipant())) { if (LOG.isTraceEnabled()) { LOG.trace("RecoveryManager.persistParticipantInformation: participant is not recoverable"); } return; } try { final RecoveryStore recoveryStore = StoreManager.getRecoveryStore(); final OutputObjectState state = getParticipantInformationOutputState(participantInformation); final Uid uid = new Uid(participantInformation.getId()); recoveryStore.write_committed(uid, PARTICIPANT_INFORMATION_RECORD_TYPE, state); } catch (Exception e) { LOG.warn("Failure while persisting participant information.", e); } } public void removeParticipantInformation(final ParticipantInformation participantInformation) { if (LOG.isTraceEnabled()) { LOG.trace("RecoveryManager.removeParticipantInformation: participantInformation=" + participantInformation); } final RecoveryStore recoveryStore = StoreManager.getRecoveryStore(); final Uid uid = new Uid(participantInformation.getId()); try { recoveryStore.remove_committed(uid, PARTICIPANT_INFORMATION_RECORD_TYPE); } catch (ObjectStoreException e) { LOG.warn("Failure while removing participant information from the object store.", e); } } private OutputObjectState getParticipantInformationOutputState(final ParticipantInformation participantInformation) throws IOException { final Uid uid = new Uid(participantInformation.getId()); final OutputObjectState state = new OutputObjectState(uid, PARTICIPANT_INFORMATION_RECORD_TYPE); state.packString(participantInformation.getId()); state.packString(participantInformation.getApplicationId()); state.packString(participantInformation.getStatus()); state.packString(participantInformation.getRecoveryURL()); state.packBytes(getParticipantBytes(participantInformation.getParticipant())); return state; } private byte[] getParticipantBytes(final Participant participant) throws IOException { if (participant instanceof Serializable) { return serializeParticipant((Serializable) participant); } else if (participant instanceof PersistableParticipant) { return ((PersistableParticipant) participant).getRecoveryState(); } // Shouldn't happen return new byte[] {}; } private byte[] serializeParticipant(final Serializable participant) throws IOException { final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); final ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(participant); return byteArrayOutputStream.toByteArray(); } private boolean isRecoverableParticipant(final Participant participant) { return (participant instanceof Serializable) || (participant instanceof PersistableParticipant); } private void recoverParticipants() { if (ParticipantsManagerFactory.getInstance().getBaseUrl() != null) { final RecoveryStore recoveryStore = StoreManager.getRecoveryStore(); final InputObjectState states = new InputObjectState(); try { if (recoveryStore.allObjUids(PARTICIPANT_INFORMATION_RECORD_TYPE, states)) { Uid uid; while ((uid = UidHelper.unpackFrom(states)).notEquals(Uid.nullUid())) { final ParticipantInformation participantInformation = recreateParticipantInformation( recoveryStore, uid); if (participantInformation != null) { ParticipantsContainer.getInstance().addParticipantInformation( participantInformation.getId(), participantInformation); } } } } catch (ObjectStoreException e) { LOG.warn(e.getMessage(), e); } catch (IOException e) { LOG.warn(e.getMessage(), e); } } else { LOG.warn("Participants cannot be loaded from the object store, because base URL was not set."); } } private ParticipantInformation recreateParticipantInformation(final RecoveryStore recoveryStore, final Uid uid) throws ObjectStoreException, IOException { final InputObjectState inputObjectState = recoveryStore.read_committed(uid, PARTICIPANT_INFORMATION_RECORD_TYPE); final String id = inputObjectState.unpackString(); if (ParticipantsContainer.getInstance().getParticipantInformation(id) != null) { // Participant is already loaded. return null; } final String applicationId = inputObjectState.unpackString(); if (!deserializers.containsKey(applicationId)) { // There is no appropriate deserializer. return null; } final String status = inputObjectState.unpackString(); final String recoveryUrl = inputObjectState.unpackString(); final Participant participant = recreateParticipant(inputObjectState, applicationId); if (participant == null) { // Deserializer failed to recreate participant. return null; } final ParticipantInformation participantInformation = new ParticipantInformation(id, applicationId, recoveryUrl, participant, status); if (!synchronizeParticipantUrlWithCoordinator(participantInformation)) { try { participant.rollback(); removeParticipantInformation(participantInformation); // TODO is it OK to leave participant not rolled back in case of Exception? } catch (HeuristicException e) { LOG.warn(e.getMessage(), e); } catch (ParticipantException e) { LOG.warn(e.getMessage(), e); } return null; } return participantInformation; } private Participant recreateParticipant(final InputObjectState inputObjectState, final String applicationId) throws IOException { final ParticipantDeserializer deserializer = deserializers.get(applicationId); final byte[] participantBytes = inputObjectState.unpackBytes(); Participant participant = deserializer.recreate(participantBytes); if (participant == null) { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(participantBytes); final ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); participant = deserializer.deserialize(objectInputStream); } return participant; } private boolean synchronizeParticipantUrlWithCoordinator(final ParticipantInformation participantInformation) { final String participantUrl = getParticipantUrl(participantInformation.getId()); Client client = ClientBuilder.newClient(); Link participantLink = Link.fromUri(participantUrl).title(TxLinkNames.PARTICIPANT_RESOURCE).rel(TxLinkNames.PARTICIPANT_RESOURCE).type(TxMediaType.PLAIN_MEDIA_TYPE).build(); Link terminatorLink = Link.fromUri(participantUrl).title(TxLinkNames.PARTICIPANT_TERMINATOR).rel(TxLinkNames.PARTICIPANT_TERMINATOR).type(TxMediaType.PLAIN_MEDIA_TYPE).build(); Response response = client.target(participantInformation.getRecoveryURL()).request() .header("Link", participantLink).header("Link", terminatorLink).put(null); try { if (response.getStatus() == 404) { return false; } } catch (Exception e) { LOG.warn(e.getMessage(), e); return false; } return true; } private String getParticipantUrl(final String participantId) { String baseUrl = ParticipantsManagerFactory.getInstance().getBaseUrl(); if (!baseUrl.substring(baseUrl.length() - 1).equals("/")) { baseUrl += "/"; } return baseUrl + ParticipantResource.BASE_PATH_SEGMENT + "/" + participantId; } }