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 org.apache.activemq.ActiveMQMessageAudit;
020import org.apache.activemq.broker.ConnectionContext;
021import org.apache.activemq.command.ActiveMQDestination;
022import org.apache.activemq.command.Message;
023import org.apache.activemq.command.MessageAck;
024import org.apache.activemq.command.MessageId;
025import org.apache.activemq.command.XATransactionId;
026import org.apache.activemq.store.AbstractMessageStore;
027import org.apache.activemq.store.IndexListener;
028import org.apache.activemq.store.MessageRecoveryListener;
029import org.apache.activemq.util.ByteSequence;
030import org.apache.activemq.util.ByteSequenceData;
031import org.apache.activemq.util.IOExceptionSupport;
032import org.apache.activemq.wireformat.WireFormat;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import java.io.IOException;
037import java.sql.SQLException;
038import java.util.ArrayList;
039import java.util.Arrays;
040import java.util.LinkedList;
041import java.util.Map;
042import java.util.TreeMap;
043
044/**
045 *
046 */
047public class JDBCMessageStore extends AbstractMessageStore {
048
049    class Duration {
050        static final int LIMIT = 100;
051        final long start = System.currentTimeMillis();
052        final String name;
053
054        Duration(String name) {
055            this.name = name;
056        }
057        void end() {
058            end(null);
059        }
060        void end(Object o) {
061            long duration = System.currentTimeMillis() - start;
062
063            if (duration > LIMIT) {
064                System.err.println(name + " took a long time: " + duration + "ms " + o);
065            }
066        }
067    }
068    private static final Logger LOG = LoggerFactory.getLogger(JDBCMessageStore.class);
069    protected final WireFormat wireFormat;
070    protected final JDBCAdapter adapter;
071    protected final JDBCPersistenceAdapter persistenceAdapter;
072    protected ActiveMQMessageAudit audit;
073    protected final LinkedList<Long> pendingAdditions = new LinkedList<Long>();
074    protected final TreeMap<Long, Message> rolledBackAcks = new TreeMap<Long, Message>();
075    final long[] perPriorityLastRecovered = new long[10];
076
077    public JDBCMessageStore(JDBCPersistenceAdapter persistenceAdapter, JDBCAdapter adapter, WireFormat wireFormat, ActiveMQDestination destination, ActiveMQMessageAudit audit) throws IOException {
078        super(destination);
079        this.persistenceAdapter = persistenceAdapter;
080        this.adapter = adapter;
081        this.wireFormat = wireFormat;
082        this.audit = audit;
083
084        if (destination.isQueue() && persistenceAdapter.getBrokerService().shouldRecordVirtualDestination(destination)) {
085            recordDestinationCreation(destination);
086        }
087        resetBatching();
088    }
089
090    private void recordDestinationCreation(ActiveMQDestination destination) throws IOException {
091        TransactionContext c = persistenceAdapter.getTransactionContext();
092        try {
093            if (adapter.doGetLastAckedDurableSubscriberMessageId(c, destination, destination.getQualifiedName(), destination.getQualifiedName()) < 0) {
094                adapter.doRecordDestination(c, destination);
095            }
096        } catch (SQLException e) {
097            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
098            throw IOExceptionSupport.create("Failed to record destination: " + destination + ". Reason: " + e, e);
099        } finally {
100            c.close();
101        }
102    }
103
104    @Override
105    public void addMessage(final ConnectionContext context, final Message message) throws IOException {
106        MessageId messageId = message.getMessageId();
107        if (audit != null && audit.isDuplicate(message)) {
108            if (LOG.isDebugEnabled()) {
109                LOG.debug(destination.getPhysicalName()
110                    + " ignoring duplicated (add) message, already stored: "
111                    + messageId);
112            }
113            return;
114        }
115
116        // if xaXid present - this is a prepare - so we don't yet have an outcome
117        final XATransactionId xaXid =  context != null ? context.getXid() : null;
118
119        // Serialize the Message..
120        byte data[];
121        try {
122            ByteSequence packet = wireFormat.marshal(message);
123            data = ByteSequenceData.toByteArray(packet);
124        } catch (IOException e) {
125            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
126        }
127
128        // Get a connection and insert the message into the DB.
129        TransactionContext c = persistenceAdapter.getTransactionContext(context);
130        long sequenceId;
131        synchronized (pendingAdditions) {
132            sequenceId = persistenceAdapter.getNextSequenceId();
133            final long sequence = sequenceId;
134            message.getMessageId().setEntryLocator(sequence);
135
136            if (xaXid == null) {
137                pendingAdditions.add(sequence);
138
139                c.onCompletion(new Runnable() {
140                    @Override
141                    public void run() {
142                        // jdbc close or jms commit - while futureOrSequenceLong==null ordered
143                        // work will remain pending on the Queue
144                        message.getMessageId().setFutureOrSequenceLong(sequence);
145                    }
146                });
147
148                if (indexListener != null) {
149                    indexListener.onAdd(new IndexListener.MessageContext(context, message, new Runnable() {
150                        @Override
151                        public void run() {
152                            // cursor add complete
153                            synchronized (pendingAdditions) { pendingAdditions.remove(sequence); }
154                        }
155                    }));
156                } else {
157                    pendingAdditions.remove(sequence);
158                }
159            }
160        }
161        try {
162            adapter.doAddMessage(c, sequenceId, messageId, destination, data, message.getExpiration(),
163                    this.isPrioritizedMessages() ? message.getPriority() : 0, xaXid);
164        } catch (SQLException e) {
165            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
166            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
167        } finally {
168            c.close();
169        }
170        if (xaXid == null) {
171            onAdd(message, sequenceId, message.getPriority());
172        }
173    }
174
175    // jdbc commit order is random with concurrent connections - limit scan to lowest pending
176    private long minPendingSequeunceId() {
177        synchronized (pendingAdditions) {
178            if (!pendingAdditions.isEmpty()) {
179                return pendingAdditions.get(0);
180            } else {
181                // nothing pending, ensure scan is limited to current state
182                return persistenceAdapter.sequenceGenerator.getLastSequenceId() + 1;
183            }
184        }
185    }
186
187    @Override
188    public void updateMessage(Message message) throws IOException {
189        TransactionContext c = persistenceAdapter.getTransactionContext();
190        try {
191            adapter.doUpdateMessage(c, destination, message.getMessageId(), ByteSequenceData.toByteArray(wireFormat.marshal(message)));
192        } catch (SQLException e) {
193            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
194            throw IOExceptionSupport.create("Failed to update message: " + message.getMessageId() + " in container: " + e, e);
195        } finally {
196            c.close();
197        }
198    }
199
200    protected void onAdd(Message message, long sequenceId, byte priority) {}
201
202    public void addMessageReference(ConnectionContext context, MessageId messageId, long expirationTime, String messageRef) throws IOException {
203        // Get a connection and insert the message into the DB.
204        TransactionContext c = persistenceAdapter.getTransactionContext(context);
205        try {
206            adapter.doAddMessageReference(c, persistenceAdapter.getNextSequenceId(), messageId, destination, expirationTime, messageRef);
207        } catch (SQLException e) {
208            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
209            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
210        } finally {
211            c.close();
212        }
213    }
214
215    @Override
216    public Message getMessage(MessageId messageId) throws IOException {
217        // Get a connection and pull the message out of the DB
218        TransactionContext c = persistenceAdapter.getTransactionContext();
219        try {
220            byte data[] = adapter.doGetMessage(c, messageId);
221            if (data == null) {
222                return null;
223            }
224
225            Message answer = (Message)wireFormat.unmarshal(new ByteSequence(data));
226            return answer;
227        } catch (IOException e) {
228            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
229        } catch (SQLException e) {
230            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
231            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
232        } finally {
233            c.close();
234        }
235    }
236
237    public String getMessageReference(MessageId messageId) throws IOException {
238        long id = messageId.getBrokerSequenceId();
239
240        // Get a connection and pull the message out of the DB
241        TransactionContext c = persistenceAdapter.getTransactionContext();
242        try {
243            return adapter.doGetMessageReference(c, id);
244        } catch (IOException e) {
245            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
246        } catch (SQLException e) {
247            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
248            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
249        } finally {
250            c.close();
251        }
252    }
253
254    @Override
255    public void removeMessage(ConnectionContext context, MessageAck ack) throws IOException {
256
257        long seq = (ack.getLastMessageId().getFutureOrSequenceLong() != null && ((Long) ack.getLastMessageId().getFutureOrSequenceLong() != 0)) ?
258                (Long) ack.getLastMessageId().getFutureOrSequenceLong() :
259                persistenceAdapter.getStoreSequenceIdForMessageId(context, ack.getLastMessageId(), destination)[0];
260
261        // Get a connection and remove the message from the DB
262        TransactionContext c = persistenceAdapter.getTransactionContext(context);
263        try {
264            adapter.doRemoveMessage(c, seq, context != null ? context.getXid() : null);
265        } catch (SQLException e) {
266            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
267            throw IOExceptionSupport.create("Failed to broker message: " + ack.getLastMessageId() + " in container: " + e, e);
268        } finally {
269            c.close();
270        }
271    }
272
273    @Override
274    public void recover(final MessageRecoveryListener listener) throws Exception {
275
276        // Get all the Message ids out of the database.
277        TransactionContext c = persistenceAdapter.getTransactionContext();
278        try {
279            adapter.doRecover(c, destination, new JDBCMessageRecoveryListener() {
280                @Override
281                public boolean recoverMessage(long sequenceId, byte[] data) throws Exception {
282                    if (listener.hasSpace()) {
283                        Message msg = (Message) wireFormat.unmarshal(new ByteSequence(data));
284                        msg.getMessageId().setBrokerSequenceId(sequenceId);
285                        return listener.recoverMessage(msg);
286                    } else {
287                        if (LOG.isTraceEnabled()) {
288                            LOG.trace("Message recovery limit reached for MessageRecoveryListener");
289                        }
290                        return false;
291                    }
292                }
293
294                @Override
295                public boolean recoverMessageReference(String reference) throws Exception {
296                    if (listener.hasSpace()) {
297                        return listener.recoverMessageReference(new MessageId(reference));
298                    } else {
299                        if (LOG.isTraceEnabled()) {
300                            LOG.trace("Message recovery limit reached for MessageRecoveryListener");
301                        }
302                        return false;
303                    }
304                }
305            });
306        } catch (SQLException e) {
307            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
308            throw IOExceptionSupport.create("Failed to recover container. Reason: " + e, e);
309        } finally {
310            c.close();
311        }
312    }
313
314    /**
315     * @see org.apache.activemq.store.MessageStore#removeAllMessages(ConnectionContext)
316     */
317    @Override
318    public void removeAllMessages(ConnectionContext context) throws IOException {
319        // Get a connection and remove the message from the DB
320        TransactionContext c = persistenceAdapter.getTransactionContext(context);
321        try {
322            adapter.doRemoveAllMessages(c, destination);
323        } catch (SQLException e) {
324            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
325            throw IOExceptionSupport.create("Failed to broker remove all messages: " + e, e);
326        } finally {
327            c.close();
328        }
329    }
330
331    @Override
332    public int getMessageCount() throws IOException {
333        int result = 0;
334        TransactionContext c = persistenceAdapter.getTransactionContext();
335        try {
336
337            result = adapter.doGetMessageCount(c, destination);
338
339        } catch (SQLException e) {
340            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
341            throw IOExceptionSupport.create("Failed to get Message Count: " + destination + ". Reason: " + e, e);
342        } finally {
343            c.close();
344        }
345        return result;
346    }
347
348    /**
349     * @param maxReturned
350     * @param listener
351     * @throws Exception
352     * @see org.apache.activemq.store.MessageStore#recoverNextMessages(int,
353     *      org.apache.activemq.store.MessageRecoveryListener)
354     */
355    @Override
356    public void recoverNextMessages(int maxReturned, final MessageRecoveryListener listener) throws Exception {
357        TransactionContext c = persistenceAdapter.getTransactionContext();
358        try {
359            if (LOG.isTraceEnabled()) {
360                LOG.trace(this + " recoverNext lastRecovered:" + Arrays.toString(perPriorityLastRecovered) + ", minPending:" + minPendingSequeunceId());
361            }
362
363            maxReturned -= recoverRolledBackAcks(maxReturned, listener);
364
365            adapter.doRecoverNextMessages(c, destination, perPriorityLastRecovered, minPendingSequeunceId(),
366                    maxReturned, isPrioritizedMessages(), new JDBCMessageRecoveryListener() {
367
368                @Override
369                public boolean recoverMessage(long sequenceId, byte[] data) throws Exception {
370                    if (listener.canRecoveryNextMessage()) {
371                        Message msg = (Message) wireFormat.unmarshal(new ByteSequence(data));
372                        msg.getMessageId().setBrokerSequenceId(sequenceId);
373                        msg.getMessageId().setFutureOrSequenceLong(sequenceId);
374                        msg.getMessageId().setEntryLocator(sequenceId);
375                        listener.recoverMessage(msg);
376                        trackLastRecovered(sequenceId, msg.getPriority());
377                        return true;
378                    } else {
379                        return false;
380                    }
381                }
382
383                @Override
384                public boolean recoverMessageReference(String reference) throws Exception {
385                    if (listener.hasSpace()) {
386                        listener.recoverMessageReference(new MessageId(reference));
387                        return true;
388                    }
389                    return false;
390                }
391
392            });
393        } catch (SQLException e) {
394            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
395        } finally {
396            c.close();
397        }
398
399    }
400
401    public void trackRollbackAck(Message message) {
402        synchronized (rolledBackAcks) {
403            rolledBackAcks.put((Long)message.getMessageId().getEntryLocator(), message);
404        }
405    }
406
407    private int recoverRolledBackAcks(int max, MessageRecoveryListener listener) throws Exception {
408        int recovered = 0;
409        ArrayList<Long> toRemove = new ArrayList<Long>();
410        synchronized (rolledBackAcks) {
411            if (!rolledBackAcks.isEmpty()) {
412                for ( Map.Entry<Long,Message> candidate : rolledBackAcks.entrySet()) {
413                    if (candidate.getKey() <= lastRecovered(candidate.getValue().getPriority())) {
414                        listener.recoverMessage(candidate.getValue());
415                        recovered++;
416                        toRemove.add(candidate.getKey());
417                        if (recovered == max) {
418                            break;
419                        }
420                    } else {
421                        toRemove.add(candidate.getKey());
422                    }
423                }
424                for (Long key : toRemove) {
425                    rolledBackAcks.remove(key);
426                }
427            }
428        }
429        return recovered;
430    }
431
432    private long lastRecovered(int priority) {
433        return perPriorityLastRecovered[isPrioritizedMessages() ? priority : 0];
434    }
435
436    private void trackLastRecovered(long sequenceId, int priority) {
437        perPriorityLastRecovered[isPrioritizedMessages() ? priority : 0] = sequenceId;
438    }
439
440    /**
441     * @see org.apache.activemq.store.MessageStore#resetBatching()
442     */
443    @Override
444    public void resetBatching() {
445        if (LOG.isTraceEnabled()) {
446            LOG.trace(this + " resetBatching. last recovered: " + Arrays.toString(perPriorityLastRecovered));
447        }
448        setLastRecovered(-1);
449    }
450
451    private void setLastRecovered(long val) {
452        for (int i=0;i<perPriorityLastRecovered.length;i++) {
453            perPriorityLastRecovered[i] = val;
454        }
455    }
456
457
458    @Override
459    public void setBatch(MessageId messageId) {
460        if (LOG.isTraceEnabled()) {
461            LOG.trace(this + " setBatch: last recovered: " + Arrays.toString(perPriorityLastRecovered));
462        }
463        try {
464            long[] storedValues = persistenceAdapter.getStoreSequenceIdForMessageId(null, messageId, destination);
465            setLastRecovered(storedValues[0]);
466        } catch (IOException ignoredAsAlreadyLogged) {
467            resetBatching();
468        }
469        if (LOG.isTraceEnabled()) {
470            LOG.trace(this + " setBatch: new last recovered: " + Arrays.toString(perPriorityLastRecovered));
471        }
472    }
473
474
475    @Override
476    public void setPrioritizedMessages(boolean prioritizedMessages) {
477        super.setPrioritizedMessages(prioritizedMessages);
478    }
479
480    @Override
481    public String toString() {
482        return destination.getPhysicalName() + ",pendingSize:" + pendingAdditions.size();
483    }
484
485}