001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.broker;
018
019
020import java.util.ArrayList;
021import java.util.Iterator;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.concurrent.ConcurrentHashMap;
026
027import javax.jms.JMSException;
028import javax.transaction.xa.XAException;
029
030import org.apache.activemq.ActiveMQMessageAudit;
031import org.apache.activemq.broker.jmx.ManagedRegionBroker;
032import org.apache.activemq.broker.region.Destination;
033import org.apache.activemq.command.ActiveMQDestination;
034import org.apache.activemq.command.BaseCommand;
035import org.apache.activemq.command.ConnectionInfo;
036import org.apache.activemq.command.LocalTransactionId;
037import org.apache.activemq.command.Message;
038import org.apache.activemq.command.MessageAck;
039import org.apache.activemq.command.ProducerInfo;
040import org.apache.activemq.command.TransactionId;
041import org.apache.activemq.command.XATransactionId;
042import org.apache.activemq.state.ProducerState;
043import org.apache.activemq.store.TransactionRecoveryListener;
044import org.apache.activemq.store.TransactionStore;
045import org.apache.activemq.transaction.LocalTransaction;
046import org.apache.activemq.transaction.Synchronization;
047import org.apache.activemq.transaction.Transaction;
048import org.apache.activemq.transaction.XATransaction;
049import org.apache.activemq.util.IOExceptionSupport;
050import org.apache.activemq.util.WrappedException;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054/**
055 * This broker filter handles the transaction related operations in the Broker
056 * interface.
057 * 
058 * 
059 */
060public class TransactionBroker extends BrokerFilter {
061
062    private static final Logger LOG = LoggerFactory.getLogger(TransactionBroker.class);
063
064    // The prepared XA transactions.
065    private TransactionStore transactionStore;
066    private Map<TransactionId, XATransaction> xaTransactions = new LinkedHashMap<TransactionId, XATransaction>();
067    private ActiveMQMessageAudit audit;
068
069    public TransactionBroker(Broker next, TransactionStore transactionStore) {
070        super(next);
071        this.transactionStore = transactionStore;
072    }
073
074    // ////////////////////////////////////////////////////////////////////////////
075    //
076    // Life cycle Methods
077    //
078    // ////////////////////////////////////////////////////////////////////////////
079
080    /**
081     * Recovers any prepared transactions.
082     */
083    public void start() throws Exception {
084        transactionStore.start();
085        try {
086            final ConnectionContext context = new ConnectionContext();
087            context.setBroker(this);
088            context.setInRecoveryMode(true);
089            context.setTransactions(new ConcurrentHashMap<TransactionId, Transaction>());
090            context.setProducerFlowControl(false);
091            final ProducerBrokerExchange producerExchange = new ProducerBrokerExchange();
092            producerExchange.setMutable(true);
093            producerExchange.setConnectionContext(context);
094            producerExchange.setProducerState(new ProducerState(new ProducerInfo()));
095            final ConsumerBrokerExchange consumerExchange = new ConsumerBrokerExchange();
096            consumerExchange.setConnectionContext(context);
097            transactionStore.recover(new TransactionRecoveryListener() {
098                public void recover(XATransactionId xid, Message[] addedMessages, MessageAck[] aks) {
099                    try {
100                        beginTransaction(context, xid);
101                        XATransaction transaction = (XATransaction) getTransaction(context, xid, false);
102                        for (int i = 0; i < addedMessages.length; i++) {
103                            forceDestinationWakeupOnCompletion(context, transaction, addedMessages[i].getDestination(), addedMessages[i]);
104                        }
105                        for (int i = 0; i < aks.length; i++) {
106                            forceDestinationWakeupOnCompletion(context, transaction, aks[i].getDestination(), aks[i]);
107                        }
108                        transaction.setState(Transaction.PREPARED_STATE);
109                        registerMBean(transaction);
110                        LOG.debug("recovered prepared transaction: {}", transaction.getTransactionId());
111                    } catch (Throwable e) {
112                        throw new WrappedException(e);
113                    }
114                }
115            });
116        } catch (WrappedException e) {
117            Throwable cause = e.getCause();
118            throw IOExceptionSupport.create("Recovery Failed: " + cause.getMessage(), cause);
119        }
120        next.start();
121    }
122
123    private void registerMBean(XATransaction transaction) {
124        if (getBrokerService().getRegionBroker() instanceof ManagedRegionBroker ) {
125            ManagedRegionBroker managedRegionBroker = (ManagedRegionBroker) getBrokerService().getRegionBroker();
126            managedRegionBroker.registerRecoveredTransactionMBean(transaction);
127        }
128    }
129
130    private void forceDestinationWakeupOnCompletion(ConnectionContext context, Transaction transaction,
131                                                    ActiveMQDestination amqDestination, BaseCommand ack) throws Exception {
132        Destination destination =  addDestination(context, amqDestination, false);
133        registerSync(destination, transaction, ack);
134    }
135
136    private void registerSync(Destination destination, Transaction transaction, BaseCommand command) {
137        Synchronization sync = new PreparedDestinationCompletion(destination, command.isMessage());
138        // ensure one per destination in the list
139        Synchronization existing = transaction.findMatching(sync);
140        if (existing != null) {
141           ((PreparedDestinationCompletion)existing).incrementOpCount();
142        } else {
143            transaction.addSynchronization(sync);
144        }
145    }
146
147    static class PreparedDestinationCompletion extends Synchronization {
148        final Destination destination;
149        final boolean messageSend;
150        int opCount = 1;
151        public PreparedDestinationCompletion(final Destination destination, boolean messageSend) {
152            this.destination = destination;
153            // rollback relevant to acks, commit to sends
154            this.messageSend = messageSend;
155        }
156
157        public void incrementOpCount() {
158            opCount++;
159        }
160
161        @Override
162        public int hashCode() {
163            return System.identityHashCode(destination) +
164                    System.identityHashCode(Boolean.valueOf(messageSend));
165        }
166
167        @Override
168        public boolean equals(Object other) {
169            return other instanceof PreparedDestinationCompletion &&
170                    destination.equals(((PreparedDestinationCompletion) other).destination) &&
171                    messageSend == ((PreparedDestinationCompletion) other).messageSend;
172        }
173
174        @Override
175        public void afterRollback() throws Exception {
176            if (!messageSend) {
177                destination.clearPendingMessages();
178                LOG.debug("cleared pending from afterRollback: {}", destination);
179            }
180        }
181
182        @Override
183        public void afterCommit() throws Exception {
184            if (messageSend) {
185                destination.clearPendingMessages();
186                destination.getDestinationStatistics().getEnqueues().add(opCount);
187                destination.getDestinationStatistics().getMessages().add(opCount);
188                LOG.debug("cleared pending from afterCommit: {}", destination);
189            } else {
190                destination.getDestinationStatistics().getDequeues().add(opCount);
191                destination.getDestinationStatistics().getMessages().subtract(opCount);
192            }
193        }
194    }
195
196    public void stop() throws Exception {
197        transactionStore.stop();
198        next.stop();
199    }
200
201    // ////////////////////////////////////////////////////////////////////////////
202    //
203    // BrokerFilter overrides
204    //
205    // ////////////////////////////////////////////////////////////////////////////
206    public TransactionId[] getPreparedTransactions(ConnectionContext context) throws Exception {
207        List<TransactionId> txs = new ArrayList<TransactionId>();
208        synchronized (xaTransactions) {
209            for (Iterator<XATransaction> iter = xaTransactions.values().iterator(); iter.hasNext();) {
210                Transaction tx = iter.next();
211                if (tx.isPrepared()) {
212                    LOG.debug("prepared transaction: {}", tx.getTransactionId());
213                    txs.add(tx.getTransactionId());
214                }
215            }
216        }
217        XATransactionId rc[] = new XATransactionId[txs.size()];
218        txs.toArray(rc);
219        LOG.debug("prepared transaction list size: {}", rc.length);
220        return rc;
221    }
222
223    public void beginTransaction(ConnectionContext context, TransactionId xid) throws Exception {
224        // the transaction may have already been started.
225        if (xid.isXATransaction()) {
226            XATransaction transaction = null;
227            synchronized (xaTransactions) {
228                transaction = xaTransactions.get(xid);
229                if (transaction != null) {
230                    return;
231                }
232                transaction = new XATransaction(transactionStore, (XATransactionId)xid, this, context.getConnectionId());
233                xaTransactions.put(xid, transaction);
234            }
235        } else {
236            Map<TransactionId, Transaction> transactionMap = context.getTransactions();
237            Transaction transaction = transactionMap.get(xid);
238            if (transaction != null) {
239                throw new JMSException("Transaction '" + xid + "' has already been started.");
240            }
241            transaction = new LocalTransaction(transactionStore, (LocalTransactionId)xid, context);
242            transactionMap.put(xid, transaction);
243        }
244    }
245
246    public int prepareTransaction(ConnectionContext context, TransactionId xid) throws Exception {
247        Transaction transaction = getTransaction(context, xid, false);
248        return transaction.prepare();
249    }
250
251    public void commitTransaction(ConnectionContext context, TransactionId xid, boolean onePhase) throws Exception {
252        Transaction transaction = getTransaction(context, xid, true);
253        transaction.commit(onePhase);
254    }
255
256    public void rollbackTransaction(ConnectionContext context, TransactionId xid) throws Exception {
257        Transaction transaction = getTransaction(context, xid, true);
258        transaction.rollback();
259    }
260
261    public void forgetTransaction(ConnectionContext context, TransactionId xid) throws Exception {
262        Transaction transaction = getTransaction(context, xid, true);
263        transaction.rollback();
264    }
265
266    public void acknowledge(ConsumerBrokerExchange consumerExchange, MessageAck ack) throws Exception {
267        // This method may be invoked recursively.
268        // Track original tx so that it can be restored.
269        final ConnectionContext context = consumerExchange.getConnectionContext();
270        Transaction originalTx = context.getTransaction();
271        Transaction transaction = null;
272        if (ack.isInTransaction()) {
273            transaction = getTransaction(context, ack.getTransactionId(), false);
274        }
275        context.setTransaction(transaction);
276        try {
277            next.acknowledge(consumerExchange, ack);
278        } finally {
279            context.setTransaction(originalTx);
280        }
281    }
282
283    public void send(ProducerBrokerExchange producerExchange, final Message message) throws Exception {
284        // This method may be invoked recursively.
285        // Track original tx so that it can be restored.
286        final ConnectionContext context = producerExchange.getConnectionContext();
287        Transaction originalTx = context.getTransaction();
288        Transaction transaction = null;
289        Synchronization sync = null;
290        if (message.getTransactionId() != null) {
291            transaction = getTransaction(context, message.getTransactionId(), false);
292            if (transaction != null) {
293                sync = new Synchronization() {
294
295                    public void afterRollback() {
296                        if (audit != null) {
297                            audit.rollback(message);
298                        }
299                    }
300                };
301                transaction.addSynchronization(sync);
302            }
303        }
304        if (audit == null || !audit.isDuplicate(message)) {
305            context.setTransaction(transaction);
306            try {
307                next.send(producerExchange, message);
308            } finally {
309                context.setTransaction(originalTx);
310            }
311        } else {
312            if (sync != null && transaction != null) {
313                transaction.removeSynchronization(sync);
314            }
315            LOG.debug("IGNORING duplicate message {}", message);
316        }
317    }
318
319    public void removeConnection(ConnectionContext context, ConnectionInfo info, Throwable error) throws Exception {
320        for (Iterator<Transaction> iter = context.getTransactions().values().iterator(); iter.hasNext();) {
321            try {
322                Transaction transaction = iter.next();
323                transaction.rollback();
324            } catch (Exception e) {
325                LOG.warn("ERROR Rolling back disconnected client's transactions: ", e);
326            }
327            iter.remove();
328        }
329
330        synchronized (xaTransactions) {
331            // first find all txs that belongs to the connection
332            ArrayList<XATransaction> txs = new ArrayList<XATransaction>();
333            for (XATransaction tx : xaTransactions.values()) {
334                if (tx.getConnectionId() != null && tx.getConnectionId().equals(info.getConnectionId()) && !tx.isPrepared()) {
335                    txs.add(tx);
336                }
337            }
338
339            // then remove them
340            // two steps needed to avoid ConcurrentModificationException, from removeTransaction()
341            for (XATransaction tx : txs) {
342                try {
343                    tx.rollback();
344                } catch (Exception e) {
345                    LOG.warn("ERROR Rolling back disconnected client's xa transactions: ", e);
346                }
347            }
348
349        }
350        next.removeConnection(context, info, error);
351    }
352
353    // ////////////////////////////////////////////////////////////////////////////
354    //
355    // Implementation help methods.
356    //
357    // ////////////////////////////////////////////////////////////////////////////
358    public Transaction getTransaction(ConnectionContext context, TransactionId xid, boolean mightBePrepared) throws JMSException, XAException {
359        Map transactionMap = null;
360        synchronized (xaTransactions) {
361            transactionMap = xid.isXATransaction() ? xaTransactions : context.getTransactions();
362        }
363        Transaction transaction = (Transaction)transactionMap.get(xid);
364        if (transaction != null) {
365            return transaction;
366        }
367        if (xid.isXATransaction()) {
368            XAException e = XATransaction.newXAException("Transaction '" + xid + "' has not been started.", XAException.XAER_NOTA);
369            throw e;
370        } else {
371            throw new JMSException("Transaction '" + xid + "' has not been started.");
372        }
373    }
374
375    public void removeTransaction(XATransactionId xid) {
376        synchronized (xaTransactions) {
377            xaTransactions.remove(xid);
378        }
379    }
380
381    public synchronized void brokerServiceStarted() {
382        super.brokerServiceStarted();
383        if (getBrokerService().isSupportFailOver() && audit == null) {
384            audit = new ActiveMQMessageAudit();
385        }
386    }
387
388}