/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.hawtjms.provider.amqp; import io.hawtjms.jms.meta.JmsSessionInfo; import io.hawtjms.jms.meta.JmsTransactionId; import io.hawtjms.provider.AsyncResult; import java.io.IOException; import java.nio.BufferOverflowException; import java.util.LinkedHashSet; import java.util.Set; import javax.jms.IllegalStateException; import javax.jms.TransactionRolledBackException; import org.apache.qpid.proton.amqp.Binary; import org.apache.qpid.proton.amqp.messaging.AmqpValue; import org.apache.qpid.proton.amqp.messaging.Rejected; import org.apache.qpid.proton.amqp.messaging.Source; import org.apache.qpid.proton.amqp.transaction.Coordinator; import org.apache.qpid.proton.amqp.transaction.Declare; import org.apache.qpid.proton.amqp.transaction.Declared; import org.apache.qpid.proton.amqp.transaction.Discharge; import org.apache.qpid.proton.amqp.transaction.TxnCapability; import org.apache.qpid.proton.amqp.transport.DeliveryState; import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode; import org.apache.qpid.proton.amqp.transport.SenderSettleMode; import org.apache.qpid.proton.engine.Delivery; import org.apache.qpid.proton.engine.Sender; import org.apache.qpid.proton.message.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Handles the operations surrounding AMQP transaction control. * * The Transaction will carry a JmsTransactionId while the Transaction is open, once a * transaction has been committed or rolled back the Transaction Id is cleared. */ public class AmqpTransactionContext extends AbstractAmqpResource<JmsSessionInfo, Sender> { private static final Logger LOG = LoggerFactory.getLogger(AmqpTransactionContext.class); private static final Boolean ROLLBACK_MARKER = Boolean.FALSE; private static final Boolean COMMIT_MARKER = Boolean.TRUE; private final AmqpSession session; private JmsTransactionId current; private final AmqpTransferTagGenerator tagGenerator = new AmqpTransferTagGenerator(); private final Set<AmqpConsumer> txConsumers = new LinkedHashSet<AmqpConsumer>(); private Delivery pendingDelivery; private AsyncResult<Void> pendingRequest; /** * Creates a new AmqpTransaction instance. * * @param session * The session that owns this transaction * @param info * The JmsTransactionInfo that defines this Transaction. */ public AmqpTransactionContext(AmqpSession session) { super(session.getJmsResource()); this.session = session; } @Override public void processDeliveryUpdates() { if (pendingDelivery != null && pendingDelivery.remotelySettled()) { DeliveryState state = pendingDelivery.getRemoteState(); if (state instanceof Declared) { Declared declared = (Declared) state; current.setProviderHint(declared.getTxnId()); pendingDelivery.settle(); LOG.info("New TX started: {}", current.getProviderHint()); AsyncResult<Void> request = this.pendingRequest; this.pendingRequest = null; this.pendingDelivery = null; request.onSuccess(); } else if (state instanceof Rejected) { LOG.info("Last TX request failed: {}", current.getProviderHint()); pendingDelivery.settle(); Rejected rejected = (Rejected) state; TransactionRolledBackException ex = new TransactionRolledBackException(rejected.getError().getDescription()); AsyncResult<Void> request = this.pendingRequest; this.current = null; this.pendingRequest = null; this.pendingDelivery = null; postRollback(); request.onFailure(ex); } else { LOG.info("Last TX request succeeded: {}", current.getProviderHint()); pendingDelivery.settle(); AsyncResult<Void> request = this.pendingRequest; if (pendingDelivery.getContext() != null) { if (pendingDelivery.getContext().equals(COMMIT_MARKER)) { postCommit(); } else { postRollback(); } } this.current = null; this.pendingRequest = null; this.pendingDelivery = null; request.onSuccess(); } } } @Override protected void doOpen() { Coordinator coordinator = new Coordinator(); coordinator.setCapabilities(TxnCapability.LOCAL_TXN, TxnCapability.MULTI_TXNS_PER_SSN); Source source = new Source(); String coordinatorName = info.getSessionId().toString(); endpoint = session.getProtonSession().sender(coordinatorName); endpoint.setSource(source); endpoint.setTarget(coordinator); endpoint.setSenderSettleMode(SenderSettleMode.UNSETTLED); endpoint.setReceiverSettleMode(ReceiverSettleMode.FIRST); } @Override protected void doClose() { } public void begin(JmsTransactionId txId, AsyncResult<Void> request) throws Exception { if (current != null) { throw new IOException("Begin called while a TX is still Active."); } Message message = session.getMessageFactory().createMessage(); Declare declare = new Declare(); message.setBody(new AmqpValue(declare)); pendingDelivery = endpoint.delivery(tagGenerator.getNextTag()); pendingRequest = request; current = txId; sendTxCommand(message); } public void commit(AsyncResult<Void> request) throws Exception { if (current == null) { throw new IllegalStateException("Rollback called with no active Transaction."); } preCommit(); Message message = session.getMessageFactory().createMessage(); Discharge discharge = new Discharge(); discharge.setFail(false); discharge.setTxnId((Binary) current.getProviderHint()); message.setBody(new AmqpValue(discharge)); pendingDelivery = endpoint.delivery(tagGenerator.getNextTag()); pendingDelivery.setContext(COMMIT_MARKER); pendingRequest = request; sendTxCommand(message); } public void rollback(AsyncResult<Void> request) throws Exception { if (current == null) { throw new IllegalStateException("Rollback called with no active Transaction."); } preRollback(); Message message = session.getMessageFactory().createMessage(); Discharge discharge = new Discharge(); discharge.setFail(true); discharge.setTxnId((Binary) current.getProviderHint()); message.setBody(new AmqpValue(discharge)); pendingDelivery = endpoint.delivery(tagGenerator.getNextTag()); pendingDelivery.setContext(ROLLBACK_MARKER); pendingRequest = request; sendTxCommand(message); } public void registerTxConsumer(AmqpConsumer consumer) { this.txConsumers.add(consumer); } public AmqpSession getSession() { return this.session; } public JmsTransactionId getTransactionId() { return this.current; } public Binary getAmqpTransactionId() { Binary result = null; if (current != null) { result = (Binary) current.getProviderHint(); } return result; } @Override public String toString() { return this.session.getSessionId() + ": txContext"; } private void preCommit() { for (AmqpConsumer consumer : txConsumers) { consumer.preCommit(); } } private void preRollback() { for (AmqpConsumer consumer : txConsumers) { consumer.preRollback(); } } private void postCommit() { for (AmqpConsumer consumer : txConsumers) { consumer.postCommit(); } } private void postRollback() { for (AmqpConsumer consumer : txConsumers) { consumer.postRollback(); } } private void sendTxCommand(Message message) throws IOException { int encodedSize = 0; byte[] buffer = new byte[4 * 1024]; while (true) { try { encodedSize = message.encode(buffer, 0, buffer.length); break; } catch (BufferOverflowException e) { buffer = new byte[buffer.length * 2]; } } this.endpoint.send(buffer, 0, encodedSize); this.endpoint.advance(); } }