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.store.jdbc;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.Iterator;
023import org.apache.activemq.broker.ConnectionContext;
024import org.apache.activemq.command.ActiveMQDestination;
025import org.apache.activemq.command.Message;
026import org.apache.activemq.command.MessageAck;
027import org.apache.activemq.command.MessageId;
028import org.apache.activemq.command.TransactionId;
029import org.apache.activemq.command.XATransactionId;
030import org.apache.activemq.store.MessageStore;
031import org.apache.activemq.store.ProxyMessageStore;
032import org.apache.activemq.store.ProxyTopicMessageStore;
033import org.apache.activemq.store.TopicMessageStore;
034import org.apache.activemq.store.TransactionRecoveryListener;
035import org.apache.activemq.store.memory.MemoryTransactionStore;
036import org.apache.activemq.util.ByteSequence;
037import org.apache.activemq.util.DataByteArrayInputStream;
038
039/**
040 * respect 2pc prepare
041 * uses local transactions to maintain prepared state
042 * xid column provides transaction flag for additions and removals
043 * a commit clears that context and completes the work
044 * a rollback clears the flag and removes the additions
045 * Essentially a prepare is an insert &| update transaction
046 *  commit|rollback is an update &| remove
047 */
048public class JdbcMemoryTransactionStore extends MemoryTransactionStore {
049
050
051    private HashMap<ActiveMQDestination, MessageStore> topicStores = new HashMap<ActiveMQDestination, MessageStore>();
052    private HashMap<ActiveMQDestination, MessageStore> queueStores = new HashMap<ActiveMQDestination, MessageStore>();
053
054    public JdbcMemoryTransactionStore(JDBCPersistenceAdapter jdbcPersistenceAdapter) {
055        super(jdbcPersistenceAdapter);
056    }
057
058    @Override
059    public void prepare(TransactionId txid) throws IOException {
060        Tx tx = inflightTransactions.remove(txid);
061        if (tx == null) {
062            return;
063        }
064
065        ConnectionContext ctx = new ConnectionContext();
066        // setting the xid modifies the add/remove to be pending transaction outcome
067        ctx.setXid((XATransactionId) txid);
068        persistenceAdapter.beginTransaction(ctx);
069        try {
070
071            // Do all the message adds.
072            for (Iterator<AddMessageCommand> iter = tx.messages.iterator(); iter.hasNext();) {
073                AddMessageCommand cmd = iter.next();
074                cmd.run(ctx);
075            }
076            // And removes..
077            for (Iterator<RemoveMessageCommand> iter = tx.acks.iterator(); iter.hasNext();) {
078                RemoveMessageCommand cmd = iter.next();
079                cmd.run(ctx);
080            }
081
082        } catch ( IOException e ) {
083            persistenceAdapter.rollbackTransaction(ctx);
084            throw e;
085        }
086        persistenceAdapter.commitTransaction(ctx);
087
088        ctx.setXid(null);
089        // setup for commit outcome
090        ArrayList<AddMessageCommand> updateFromPreparedStateCommands = new ArrayList<AddMessageCommand>();
091        for (Iterator<AddMessageCommand> iter = tx.messages.iterator(); iter.hasNext();) {
092            final AddMessageCommand addMessageCommand = iter.next();
093            updateFromPreparedStateCommands.add(new AddMessageCommand() {
094                @Override
095                public Message getMessage() {
096                    return addMessageCommand.getMessage();
097                }
098
099                @Override
100                public MessageStore getMessageStore() {
101                    return addMessageCommand.getMessageStore();
102                }
103
104                @Override
105                public void run(ConnectionContext context) throws IOException {
106                    JDBCPersistenceAdapter jdbcPersistenceAdapter = (JDBCPersistenceAdapter) persistenceAdapter;
107                    Message message = addMessageCommand.getMessage();
108                    jdbcPersistenceAdapter.commitAdd(context, message.getMessageId());
109                    ((JDBCMessageStore)addMessageCommand.getMessageStore()).onAdd(
110                            message,
111                            (Long)message.getMessageId().getFutureOrSequenceLong(),
112                            message.getPriority());
113
114                }
115
116                @Override
117                public void setMessageStore(MessageStore messageStore) {
118                    throw new RuntimeException("MessageStore already known");
119                }
120            });
121        }
122        tx.messages = updateFromPreparedStateCommands;
123        preparedTransactions.put(txid, tx);
124
125    }
126
127
128    @Override
129    public void rollback(TransactionId txid) throws IOException {
130
131        Tx tx = inflightTransactions.remove(txid);
132        if (tx == null) {
133            tx = preparedTransactions.remove(txid);
134            if (tx != null) {
135                // undo prepare work
136                ConnectionContext ctx = new ConnectionContext();
137                persistenceAdapter.beginTransaction(ctx);
138                try {
139
140                    for (Iterator<AddMessageCommand> iter = tx.messages.iterator(); iter.hasNext(); ) {
141                        final Message message = iter.next().getMessage();
142                        // need to delete the row
143                        ((JDBCPersistenceAdapter) persistenceAdapter).commitRemove(ctx, new MessageAck(message, MessageAck.STANDARD_ACK_TYPE, 1));
144                    }
145
146                    for (Iterator<RemoveMessageCommand> iter = tx.acks.iterator(); iter.hasNext(); ) {
147                        RemoveMessageCommand removeMessageCommand = iter.next();
148                        if (removeMessageCommand instanceof LastAckCommand ) {
149                            ((LastAckCommand)removeMessageCommand).rollback(ctx);
150                        } else {
151                            // need to unset the txid flag on the existing row
152                            ((JDBCPersistenceAdapter) persistenceAdapter).commitAdd(ctx,
153                                    removeMessageCommand.getMessageAck().getLastMessageId());
154                        }
155                    }
156                } catch (IOException e) {
157                    persistenceAdapter.rollbackTransaction(ctx);
158                    throw e;
159                }
160                persistenceAdapter.commitTransaction(ctx);
161            }
162        }
163    }
164
165    @Override
166    public void recover(TransactionRecoveryListener listener) throws IOException {
167        ((JDBCPersistenceAdapter)persistenceAdapter).recover(this);
168        super.recover(listener);
169    }
170
171    public void recoverAdd(long id, byte[] messageBytes) throws IOException {
172        final Message message = (Message) ((JDBCPersistenceAdapter)persistenceAdapter).getWireFormat().unmarshal(new ByteSequence(messageBytes));
173        message.getMessageId().setFutureOrSequenceLong(id);
174        Tx tx = getPreparedTx(message.getTransactionId());
175        tx.add(new AddMessageCommand() {
176            MessageStore messageStore;
177            @Override
178            public Message getMessage() {
179                return message;
180            }
181
182            @Override
183            public MessageStore getMessageStore() {
184                return messageStore;
185            }
186
187            @Override
188            public void run(ConnectionContext context) throws IOException {
189                ((JDBCPersistenceAdapter)persistenceAdapter).commitAdd(null, message.getMessageId());
190                ((JDBCMessageStore)messageStore).onAdd(message, ((Long)message.getMessageId().getFutureOrSequenceLong()).longValue(), message.getPriority());
191            }
192
193            @Override
194            public void setMessageStore(MessageStore messageStore) {
195                this.messageStore = messageStore;
196            }
197
198        });
199    }
200
201    public void recoverAck(long id, byte[] xid, byte[] message) throws IOException {
202        Message msg = (Message) ((JDBCPersistenceAdapter)persistenceAdapter).getWireFormat().unmarshal(new ByteSequence(message));
203        msg.getMessageId().setFutureOrSequenceLong(id);
204        Tx tx = getPreparedTx(new XATransactionId(xid));
205        final MessageAck ack = new MessageAck(msg, MessageAck.STANDARD_ACK_TYPE, 1);
206        tx.add(new RemoveMessageCommand() {
207            @Override
208            public MessageAck getMessageAck() {
209                return ack;
210            }
211
212            @Override
213            public void run(ConnectionContext context) throws IOException {
214                ((JDBCPersistenceAdapter)persistenceAdapter).commitRemove(context, ack);
215            }
216
217            @Override
218            public MessageStore getMessageStore() {
219                return null;
220            }
221
222        });
223
224    }
225
226    interface LastAckCommand extends RemoveMessageCommand {
227        void rollback(ConnectionContext context) throws IOException;
228
229        String getClientId();
230
231        String getSubName();
232
233        long getSequence();
234
235        byte getPriority();
236
237        void setMessageStore(JDBCTopicMessageStore jdbcTopicMessageStore);
238    }
239
240    public void recoverLastAck(byte[] encodedXid, final ActiveMQDestination destination, final String subName, final String clientId) throws IOException {
241        Tx tx = getPreparedTx(new XATransactionId(encodedXid));
242        DataByteArrayInputStream inputStream = new DataByteArrayInputStream(encodedXid);
243        inputStream.skipBytes(1); // +|-
244        final long lastAck = inputStream.readLong();
245        final byte priority = inputStream.readByte();
246        final MessageAck ack = new MessageAck();
247        ack.setDestination(destination);
248        tx.add(new LastAckCommand() {
249            JDBCTopicMessageStore jdbcTopicMessageStore;
250
251            @Override
252            public MessageAck getMessageAck() {
253                return ack;
254            }
255
256            @Override
257            public MessageStore getMessageStore() {
258                return jdbcTopicMessageStore;
259            }
260
261            @Override
262            public void run(ConnectionContext context) throws IOException {
263                ((JDBCPersistenceAdapter)persistenceAdapter).commitLastAck(context, lastAck, priority, destination, subName, clientId);
264                jdbcTopicMessageStore.complete(clientId, subName);
265            }
266
267            @Override
268            public void rollback(ConnectionContext context) throws IOException {
269                ((JDBCPersistenceAdapter)persistenceAdapter).rollbackLastAck(context, priority, jdbcTopicMessageStore.getDestination(), subName, clientId);
270                jdbcTopicMessageStore.complete(clientId, subName);
271            }
272
273            @Override
274            public String getClientId() {
275                return clientId;
276            }
277
278            @Override
279            public String getSubName() {
280                return subName;
281            }
282
283            @Override
284            public long getSequence() {
285                return lastAck;
286            }
287
288            @Override
289            public byte getPriority() {
290                return priority;
291            }
292
293            @Override
294            public void setMessageStore(JDBCTopicMessageStore jdbcTopicMessageStore) {
295                this.jdbcTopicMessageStore = jdbcTopicMessageStore;
296            }
297        });
298
299    }
300
301    @Override
302    protected void onProxyTopicStore(ProxyTopicMessageStore proxyTopicMessageStore) {
303        topicStores.put(proxyTopicMessageStore.getDestination(), proxyTopicMessageStore.getDelegate());
304    }
305
306    @Override
307    protected void onProxyQueueStore(ProxyMessageStore proxyQueueMessageStore) {
308        queueStores.put(proxyQueueMessageStore.getDestination(), proxyQueueMessageStore.getDelegate());
309    }
310
311    @Override
312    protected void onRecovered(Tx tx) {
313        for (RemoveMessageCommand removeMessageCommand: tx.acks) {
314            if (removeMessageCommand instanceof LastAckCommand) {
315                LastAckCommand lastAckCommand = (LastAckCommand) removeMessageCommand;
316                JDBCTopicMessageStore jdbcTopicMessageStore = (JDBCTopicMessageStore) topicStores.get(lastAckCommand.getMessageAck().getDestination());
317                jdbcTopicMessageStore.pendingCompletion(lastAckCommand.getClientId(), lastAckCommand.getSubName(), lastAckCommand.getSequence(), lastAckCommand.getPriority());
318                lastAckCommand.setMessageStore(jdbcTopicMessageStore);
319            } else {
320                // when reading the store we ignore messages with non null XIDs but should include those with XIDS starting in - (pending acks in an xa transaction),
321                // but the sql is non portable to match BLOB with LIKE etc
322                // so we make up for it when we recover the ack
323                ((JDBCPersistenceAdapter)persistenceAdapter).getBrokerService().getRegionBroker().getDestinationMap().get(removeMessageCommand.getMessageAck().getDestination()).getDestinationStatistics().getMessages().increment();
324            }
325        }
326        for (AddMessageCommand addMessageCommand : tx.messages) {
327            ActiveMQDestination destination = addMessageCommand.getMessage().getDestination();
328            addMessageCommand.setMessageStore(destination.isQueue() ? queueStores.get(destination) : topicStores.get(destination));
329        }
330    }
331
332    @Override
333    public void acknowledge(final TopicMessageStore topicMessageStore, final String clientId, final String subscriptionName,
334                           final MessageId messageId, final MessageAck ack) throws IOException {
335
336        if (ack.isInTransaction()) {
337            Tx tx = getTx(ack.getTransactionId());
338            tx.add(new LastAckCommand() {
339                public MessageAck getMessageAck() {
340                    return ack;
341                }
342
343                public void run(ConnectionContext ctx) throws IOException {
344                    topicMessageStore.acknowledge(ctx, clientId, subscriptionName, messageId, ack);
345                }
346
347                @Override
348                public MessageStore getMessageStore() {
349                    return topicMessageStore;
350                }
351
352                @Override
353                public void rollback(ConnectionContext context) throws IOException {
354                    JDBCTopicMessageStore jdbcTopicMessageStore = (JDBCTopicMessageStore)topicMessageStore;
355                    ((JDBCPersistenceAdapter)persistenceAdapter).rollbackLastAck(context,
356                            jdbcTopicMessageStore,
357                            ack,
358                            subscriptionName, clientId);
359                    jdbcTopicMessageStore.complete(clientId, subscriptionName);
360                }
361
362
363                @Override
364                public String getClientId() {
365                    return clientId;
366                }
367
368                @Override
369                public String getSubName() {
370                    return subscriptionName;
371                }
372
373                @Override
374                public long getSequence() {
375                    throw new IllegalStateException("Sequence id must be inferred from ack");
376                }
377
378                @Override
379                public byte getPriority() {
380                    throw new IllegalStateException("Priority must be inferred from ack or row");
381                }
382
383                @Override
384                public void setMessageStore(JDBCTopicMessageStore jdbcTopicMessageStore) {
385                    throw new IllegalStateException("message store already known!");
386                }
387            });
388        } else {
389            topicMessageStore.acknowledge(null, clientId, subscriptionName, messageId, ack);
390        }
391    }
392
393}