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.sql.SQLException;
021import java.util.Iterator;
022import java.util.LinkedHashSet;
023import java.util.LinkedList;
024import java.util.Set;
025import java.util.concurrent.atomic.AtomicLong;
026
027import org.apache.activemq.ActiveMQMessageAudit;
028import org.apache.activemq.broker.ConnectionContext;
029import org.apache.activemq.command.ActiveMQDestination;
030import org.apache.activemq.command.Message;
031import org.apache.activemq.command.MessageAck;
032import org.apache.activemq.command.MessageId;
033import org.apache.activemq.store.AbstractMessageStore;
034import org.apache.activemq.store.IndexListener;
035import org.apache.activemq.store.MessageRecoveryListener;
036import org.apache.activemq.util.ByteSequence;
037import org.apache.activemq.util.ByteSequenceData;
038import org.apache.activemq.util.IOExceptionSupport;
039import org.apache.activemq.wireformat.WireFormat;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * 
045 */
046public class JDBCMessageStore extends AbstractMessageStore {
047
048    class Duration {
049        static final int LIMIT = 100;
050        final long start = System.currentTimeMillis();
051        final String name;
052
053        Duration(String name) {
054            this.name = name;
055        }
056        void end() {
057            end(null);
058        }
059        void end(Object o) {
060            long duration = System.currentTimeMillis() - start;
061
062            if (duration > LIMIT) {
063                System.err.println(name + " took a long time: " + duration + "ms " + o);
064            }
065        }
066    }
067    private static final Logger LOG = LoggerFactory.getLogger(JDBCMessageStore.class);
068    protected final WireFormat wireFormat;
069    protected final JDBCAdapter adapter;
070    protected final JDBCPersistenceAdapter persistenceAdapter;
071    protected AtomicLong lastRecoveredSequenceId = new AtomicLong(-1);
072    protected AtomicLong lastRecoveredPriority = new AtomicLong(Byte.MAX_VALUE -1);
073    final Set<Long> recoveredAdditions = new LinkedHashSet<Long>();
074    protected ActiveMQMessageAudit audit;
075    protected final LinkedList<Long> pendingAdditions = new LinkedList<Long>();
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    }
088
089    private void recordDestinationCreation(ActiveMQDestination destination) throws IOException {
090        TransactionContext c = persistenceAdapter.getTransactionContext();
091        try {
092            c = persistenceAdapter.getTransactionContext();
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    public void addMessage(final ConnectionContext context, final Message message) throws IOException {
105        MessageId messageId = message.getMessageId();
106        if (audit != null && audit.isDuplicate(message)) {
107            if (LOG.isDebugEnabled()) {
108                LOG.debug(destination.getPhysicalName()
109                    + " ignoring duplicated (add) message, already stored: "
110                    + messageId);
111            }
112            return;
113        }
114
115        // Serialize the Message..
116        byte data[];
117        try {
118            ByteSequence packet = wireFormat.marshal(message);
119            data = ByteSequenceData.toByteArray(packet);
120        } catch (IOException e) {
121            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
122        }
123
124        // Get a connection and insert the message into the DB.
125        TransactionContext c = persistenceAdapter.getTransactionContext(context);
126        long sequenceId;
127        synchronized (pendingAdditions) {
128            sequenceId = persistenceAdapter.getNextSequenceId();
129            final long sequence = sequenceId;
130            pendingAdditions.add(sequence);
131            c.onCompletion(new Runnable() {
132                public void run() {
133                    // jdbc close or jms commit - while futureOrSequenceLong==null ordered
134                    // work will remain pending on the Queue
135                    message.getMessageId().setFutureOrSequenceLong(sequence);
136                    message.getMessageId().setEntryLocator(sequence);
137                }
138            });
139
140            if (indexListener != null) {
141                indexListener.onAdd(new IndexListener.MessageContext(context, message, new Runnable() {
142                    @Override
143                    public void run() {
144                        // cursor add complete
145                        synchronized (pendingAdditions) { pendingAdditions.remove(sequence); }
146                    }
147                }));
148            } else {
149                pendingAdditions.remove(sequence);
150            }
151        }
152        try {
153            adapter.doAddMessage(c, sequenceId, messageId, destination, data, message.getExpiration(),
154                    this.isPrioritizedMessages() ? message.getPriority() : 0, context != null ? context.getXid() : null);
155        } catch (SQLException e) {
156            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
157            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
158        } finally {
159            c.close();
160        }
161        onAdd(message, sequenceId, message.getPriority());
162    }
163
164    // jdbc commit order is random with concurrent connections - limit scan to lowest pending
165    private long minPendingSequeunceId() {
166        synchronized (pendingAdditions) {
167            if (!pendingAdditions.isEmpty()) {
168                return pendingAdditions.get(0);
169            } else {
170                // nothing pending, ensure scan is limited to current state
171                return persistenceAdapter.sequenceGenerator.getLastSequenceId() + 1;
172            }
173        }
174    }
175
176    @Override
177    public void updateMessage(Message message) throws IOException {
178        TransactionContext c = persistenceAdapter.getTransactionContext();
179        try {
180            adapter.doUpdateMessage(c, destination, message.getMessageId(), ByteSequenceData.toByteArray(wireFormat.marshal(message)));
181        } catch (SQLException e) {
182            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
183            throw IOExceptionSupport.create("Failed to update message: " + message.getMessageId() + " in container: " + e, e);
184        } finally {
185            c.close();
186        }
187    }
188
189    protected void onAdd(Message message, long sequenceId, byte priority) {
190        if (message.getTransactionId() != null && message.getTransactionId().isXATransaction()
191                && lastRecoveredSequenceId.get() > 0 && sequenceId < lastRecoveredSequenceId.get()) {
192            recoveredAdditions.add(sequenceId);
193        }
194    }
195
196    public void addMessageReference(ConnectionContext context, MessageId messageId, long expirationTime, String messageRef) throws IOException {
197        // Get a connection and insert the message into the DB.
198        TransactionContext c = persistenceAdapter.getTransactionContext(context);
199        try {
200            adapter.doAddMessageReference(c, persistenceAdapter.getNextSequenceId(), messageId, destination, expirationTime, messageRef);
201        } catch (SQLException e) {
202            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
203            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
204        } finally {
205            c.close();
206        }
207    }
208
209    public Message getMessage(MessageId messageId) throws IOException {
210        // Get a connection and pull the message out of the DB
211        TransactionContext c = persistenceAdapter.getTransactionContext();
212        try {
213            byte data[] = adapter.doGetMessage(c, messageId);
214            if (data == null) {
215                return null;
216            }
217
218            Message answer = (Message)wireFormat.unmarshal(new ByteSequence(data));
219            return answer;
220        } catch (IOException e) {
221            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
222        } catch (SQLException e) {
223            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
224            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
225        } finally {
226            c.close();
227        }
228    }
229
230    public String getMessageReference(MessageId messageId) throws IOException {
231        long id = messageId.getBrokerSequenceId();
232
233        // Get a connection and pull the message out of the DB
234        TransactionContext c = persistenceAdapter.getTransactionContext();
235        try {
236            return adapter.doGetMessageReference(c, id);
237        } catch (IOException e) {
238            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
239        } catch (SQLException e) {
240            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
241            throw IOExceptionSupport.create("Failed to broker message: " + messageId + " in container: " + e, e);
242        } finally {
243            c.close();
244        }
245    }
246
247    public void removeMessage(ConnectionContext context, MessageAck ack) throws IOException {
248
249        long seq = ack.getLastMessageId().getFutureOrSequenceLong() != null ?
250                (Long) ack.getLastMessageId().getFutureOrSequenceLong() :
251                persistenceAdapter.getStoreSequenceIdForMessageId(context, ack.getLastMessageId(), destination)[0];
252
253        // Get a connection and remove the message from the DB
254        TransactionContext c = persistenceAdapter.getTransactionContext(context);
255        try {
256            adapter.doRemoveMessage(c, seq, context != null ? context.getXid() : null);
257        } catch (SQLException e) {
258            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
259            throw IOExceptionSupport.create("Failed to broker message: " + ack.getLastMessageId() + " in container: " + e, e);
260        } finally {
261            c.close();
262        }
263    }
264
265    public void recover(final MessageRecoveryListener listener) throws Exception {
266
267        // Get all the Message ids out of the database.
268        TransactionContext c = persistenceAdapter.getTransactionContext();
269        try {
270            c = persistenceAdapter.getTransactionContext();
271            adapter.doRecover(c, destination, new JDBCMessageRecoveryListener() {
272                public boolean recoverMessage(long sequenceId, byte[] data) throws Exception {
273                    Message msg = (Message) wireFormat.unmarshal(new ByteSequence(data));
274                    msg.getMessageId().setBrokerSequenceId(sequenceId);
275                    return listener.recoverMessage(msg);
276                }
277
278                public boolean recoverMessageReference(String reference) throws Exception {
279                    return listener.recoverMessageReference(new MessageId(reference));
280                }
281            });
282        } catch (SQLException e) {
283            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
284            throw IOExceptionSupport.create("Failed to recover container. Reason: " + e, e);
285        } finally {
286            c.close();
287        }
288    }
289
290    /**
291     * @see org.apache.activemq.store.MessageStore#removeAllMessages(ConnectionContext)
292     */
293    public void removeAllMessages(ConnectionContext context) throws IOException {
294        // Get a connection and remove the message from the DB
295        TransactionContext c = persistenceAdapter.getTransactionContext(context);
296        try {
297            adapter.doRemoveAllMessages(c, destination);
298        } catch (SQLException e) {
299            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
300            throw IOExceptionSupport.create("Failed to broker remove all messages: " + e, e);
301        } finally {
302            c.close();
303        }
304    }
305
306    public int getMessageCount() throws IOException {
307        int result = 0;
308        TransactionContext c = persistenceAdapter.getTransactionContext();
309        try {
310
311            result = adapter.doGetMessageCount(c, destination);
312
313        } catch (SQLException e) {
314            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
315            throw IOExceptionSupport.create("Failed to get Message Count: " + destination + ". Reason: " + e, e);
316        } finally {
317            c.close();
318        }
319        return result;
320    }
321
322    /**
323     * @param maxReturned
324     * @param listener
325     * @throws Exception
326     * @see org.apache.activemq.store.MessageStore#recoverNextMessages(int,
327     *      org.apache.activemq.store.MessageRecoveryListener)
328     */
329    public void recoverNextMessages(int maxReturned, final MessageRecoveryListener listener) throws Exception {
330        TransactionContext c = persistenceAdapter.getTransactionContext();
331        try {
332            if (!recoveredAdditions.isEmpty()) {
333                for (Iterator<Long> iterator = recoveredAdditions.iterator(); iterator.hasNext(); )  {
334                    Long sequenceId = iterator.next();
335                    iterator.remove();
336                    maxReturned--;
337                    if (sequenceId <= lastRecoveredSequenceId.get()) {
338                        Message msg = (Message)wireFormat.unmarshal(new ByteSequence(adapter.doGetMessageById(c, sequenceId)));
339                        LOG.trace("recovered add {} {}", this, msg.getMessageId());
340                        listener.recoverMessage(msg);
341                    }
342                }
343            }
344            if (LOG.isTraceEnabled()) {
345                LOG.trace(this + " recoverNext lastRecovered:" + lastRecoveredSequenceId.get() + ", minPending:" + minPendingSequeunceId());
346            }
347            adapter.doRecoverNextMessages(c, destination, minPendingSequeunceId(), lastRecoveredSequenceId.get(), lastRecoveredPriority.get(),
348                    maxReturned, isPrioritizedMessages(), new JDBCMessageRecoveryListener() {
349
350                public boolean recoverMessage(long sequenceId, byte[] data) throws Exception {
351                        Message msg = (Message)wireFormat.unmarshal(new ByteSequence(data));
352                        msg.getMessageId().setBrokerSequenceId(sequenceId);
353                        msg.getMessageId().setFutureOrSequenceLong(sequenceId);
354                        listener.recoverMessage(msg);
355                        lastRecoveredSequenceId.set(sequenceId);
356                        lastRecoveredPriority.set(msg.getPriority());
357                        return true;
358                }
359
360                public boolean recoverMessageReference(String reference) throws Exception {
361                    if (listener.hasSpace()) {
362                        listener.recoverMessageReference(new MessageId(reference));
363                        return true;
364                    }
365                    return false;
366                }
367
368            });
369        } catch (SQLException e) {
370            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
371        } finally {
372            c.close();
373        }
374
375    }
376
377    /**
378     * @see org.apache.activemq.store.MessageStore#resetBatching()
379     */
380    public void resetBatching() {
381        if (LOG.isTraceEnabled()) {
382            LOG.trace(this + " resetBatching, existing last recovered seqId: " + lastRecoveredSequenceId.get());
383        }
384        lastRecoveredSequenceId.set(-1);
385        lastRecoveredPriority.set(Byte.MAX_VALUE - 1);
386
387    }
388
389    @Override
390    public void setBatch(MessageId messageId) {
391        try {
392            long[] storedValues = persistenceAdapter.getStoreSequenceIdForMessageId(null, messageId, destination);
393            lastRecoveredSequenceId.set(storedValues[0]);
394            lastRecoveredPriority.set(storedValues[1]);
395        } catch (IOException ignoredAsAlreadyLogged) {
396            lastRecoveredSequenceId.set(-1);
397            lastRecoveredPriority.set(Byte.MAX_VALUE -1);
398        }
399        if (LOG.isTraceEnabled()) {
400            LOG.trace(this + " setBatch: new sequenceId: " + lastRecoveredSequenceId.get()
401                    + ", priority: " + lastRecoveredPriority.get());
402        }
403    }
404
405
406    public void setPrioritizedMessages(boolean prioritizedMessages) {
407        super.setPrioritizedMessages(prioritizedMessages);
408    }
409
410    @Override
411    public String toString() {
412        return destination.getPhysicalName() + ",pendingSize:" + pendingAdditions.size();
413    }
414}