/* * 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 org.apache.activemq.artemis.core.persistence.impl.journal; import java.io.File; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.ActiveMQIllegalStateException; import org.apache.activemq.artemis.api.core.ActiveMQInternalErrorException; import org.apache.activemq.artemis.api.core.Message; import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.core.config.Configuration; import org.apache.activemq.artemis.core.io.IOCriticalErrorListener; import org.apache.activemq.artemis.core.io.SequentialFile; import org.apache.activemq.artemis.core.io.SequentialFileFactory; import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory; import org.apache.activemq.artemis.core.io.mapped.MappedSequentialFileFactory; import org.apache.activemq.artemis.core.io.nio.NIOSequentialFileFactory; import org.apache.activemq.artemis.core.journal.Journal; import org.apache.activemq.artemis.core.journal.impl.JournalFile; import org.apache.activemq.artemis.core.journal.impl.JournalImpl; import org.apache.activemq.artemis.core.paging.PagedMessage; import org.apache.activemq.artemis.core.paging.PagingManager; import org.apache.activemq.artemis.core.paging.PagingStore; import org.apache.activemq.artemis.core.persistence.OperationContext; import org.apache.activemq.artemis.core.persistence.impl.journal.codec.LargeMessagePersister; import org.apache.activemq.artemis.core.persistence.impl.journal.codec.PendingLargeMessageEncoding; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ReplicationLiveIsStoppingMessage; import org.apache.activemq.artemis.core.replication.ReplicatedJournal; import org.apache.activemq.artemis.core.replication.ReplicationManager; import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.server.JournalType; import org.apache.activemq.artemis.core.server.LargeServerMessage; import org.apache.activemq.artemis.core.server.files.FileStoreMonitor; import org.apache.activemq.artemis.utils.ExecutorFactory; import org.jboss.logging.Logger; public class JournalStorageManager extends AbstractJournalStorageManager { private static final Logger logger = Logger.getLogger(JournalStorageManager.class); private SequentialFileFactory journalFF; private SequentialFileFactory bindingsFF; SequentialFileFactory largeMessagesFactory; private Journal originalMessageJournal; private Journal originalBindingsJournal; protected String largeMessagesDirectory; private ReplicationManager replicator; public JournalStorageManager(final Configuration config, final ExecutorFactory executorFactory, final ScheduledExecutorService scheduledExecutorService, final ExecutorFactory ioExecutors) { this(config, executorFactory, scheduledExecutorService, ioExecutors, null); } public JournalStorageManager(final Configuration config, final ExecutorFactory executorFactory, final ExecutorFactory ioExecutors) { this(config, executorFactory, null, ioExecutors, null); } public JournalStorageManager(final Configuration config, final ExecutorFactory executorFactory, final ScheduledExecutorService scheduledExecutorService, final ExecutorFactory ioExecutors, final IOCriticalErrorListener criticalErrorListener) { super(config, executorFactory, scheduledExecutorService, ioExecutors, criticalErrorListener); } public JournalStorageManager(final Configuration config, final ExecutorFactory executorFactory, final ExecutorFactory ioExecutors, final IOCriticalErrorListener criticalErrorListener) { super(config, executorFactory, null, ioExecutors, criticalErrorListener); } @Override protected void init(Configuration config, IOCriticalErrorListener criticalErrorListener) { if (!EnumSet.allOf(JournalType.class).contains(config.getJournalType())) { throw ActiveMQMessageBundle.BUNDLE.invalidJournal(); } bindingsFF = new NIOSequentialFileFactory(config.getBindingsLocation(), criticalErrorListener, config.getJournalMaxIO_NIO()); bindingsFF.setDatasync(config.isJournalDatasync()); Journal localBindings = new JournalImpl(ioExecutors, 1024 * 1024, 2, config.getJournalCompactMinFiles(), config.getJournalPoolFiles(), config.getJournalCompactPercentage(), bindingsFF, "activemq-bindings", "bindings", 1, 0); bindingsJournal = localBindings; originalBindingsJournal = localBindings; switch (config.getJournalType()) { case NIO: ActiveMQServerLogger.LOGGER.journalUseNIO(); journalFF = new NIOSequentialFileFactory(config.getJournalLocation(), true, config.getJournalBufferSize_NIO(), config.getJournalBufferTimeout_NIO(), config.getJournalMaxIO_NIO(), config.isLogJournalWriteRate(), criticalErrorListener); break; case ASYNCIO: ActiveMQServerLogger.LOGGER.journalUseAIO(); journalFF = new AIOSequentialFileFactory(config.getJournalLocation(), config.getJournalBufferSize_AIO(), config.getJournalBufferTimeout_AIO(), config.getJournalMaxIO_AIO(), config.isLogJournalWriteRate(), criticalErrorListener); break; case MAPPED: ActiveMQServerLogger.LOGGER.journalUseMAPPED(); //the mapped version do not need buffering by default journalFF = new MappedSequentialFileFactory(config.getJournalLocation(), criticalErrorListener, true).chunkBytes(config.getJournalFileSize()).overlapBytes(0); break; default: throw ActiveMQMessageBundle.BUNDLE.invalidJournalType2(config.getJournalType()); } journalFF.setDatasync(config.isJournalDatasync()); Journal localMessage = new JournalImpl(ioExecutors, config.getJournalFileSize(), config.getJournalMinFiles(), config.getJournalPoolFiles(), config.getJournalCompactMinFiles(), config.getJournalCompactPercentage(), journalFF, "activemq-data", "amq", journalFF.getMaxIO(), 0); messageJournal = localMessage; originalMessageJournal = localMessage; largeMessagesDirectory = config.getLargeMessagesDirectory(); largeMessagesFactory = new NIOSequentialFileFactory(config.getLargeMessagesLocation(), false, criticalErrorListener, 1); if (config.getPageMaxConcurrentIO() != 1) { pageMaxConcurrentIO = new Semaphore(config.getPageMaxConcurrentIO()); } else { pageMaxConcurrentIO = null; } } // Life Cycle Handlers @Override protected void beforeStart() throws Exception { checkAndCreateDir(config.getBindingsLocation(), config.isCreateBindingsDir()); checkAndCreateDir(config.getJournalLocation(), config.isCreateJournalDir()); checkAndCreateDir(config.getLargeMessagesLocation(), config.isCreateJournalDir()); cleanupIncompleteFiles(); } @Override protected void beforeStop() throws Exception { if (replicator != null) { replicator.stop(); } } @Override public void stop() throws Exception { stop(false, true); } public boolean isReplicated() { return replicator != null; } private void cleanupIncompleteFiles() throws Exception { if (largeMessagesFactory != null) { List<String> tmpFiles = largeMessagesFactory.listFiles("tmp"); for (String tmpFile : tmpFiles) { SequentialFile file = largeMessagesFactory.createSequentialFile(tmpFile); file.delete(); } } } @Override public synchronized void stop(boolean ioCriticalError, boolean sendFailover) throws Exception { if (!started) { return; } if (!ioCriticalError) { performCachedLargeMessageDeletes(); // Must call close to make sure last id is persisted if (journalLoaded && idGenerator != null) idGenerator.persistCurrentID(); } final CountDownLatch latch = new CountDownLatch(1); try { executor.execute(new Runnable() { @Override public void run() { latch.countDown(); } }); latch.await(30, TimeUnit.SECONDS); } catch (RejectedExecutionException ignored) { // that's ok } // We cache the variable as the replicator could be changed between here and the time we call stop // since sendLiveIsStopping may issue a close back from the channel // and we want to ensure a stop here just in case ReplicationManager replicatorInUse = replicator; if (replicatorInUse != null) { if (sendFailover) { final OperationContext token = replicator.sendLiveIsStopping(ReplicationLiveIsStoppingMessage.LiveStopping.FAIL_OVER); if (token != null) { try { token.waitCompletion(5000); } catch (Exception e) { // ignore it } } } replicatorInUse.stop(); } bindingsJournal.stop(); messageJournal.stop(); journalLoaded = false; started = false; } /** * Assumption is that this is only called with a writeLock on the StorageManager. */ @Override protected void performCachedLargeMessageDeletes() { for (Long largeMsgId : largeMessagesToDelete) { SequentialFile msg = createFileForLargeMessage(largeMsgId, LargeMessageExtension.DURABLE); try { msg.delete(); } catch (Exception e) { ActiveMQServerLogger.LOGGER.journalErrorDeletingMessage(e, largeMsgId); } if (replicator != null) { replicator.largeMessageDelete(largeMsgId); } } largeMessagesToDelete.clear(); } protected SequentialFile createFileForLargeMessage(final long messageID, final boolean durable) { if (durable) { return createFileForLargeMessage(messageID, LargeMessageExtension.DURABLE); } else { return createFileForLargeMessage(messageID, LargeMessageExtension.TEMPORARY); } } @Override /** * @param messages * @param buff * @return * @throws Exception */ protected LargeServerMessage parseLargeMessage(final Map<Long, Message> messages, final ActiveMQBuffer buff) throws Exception { LargeServerMessage largeMessage = createLargeMessage(); LargeMessagePersister.getInstance().decode(buff, largeMessage); if (largeMessage.containsProperty(Message.HDR_ORIG_MESSAGE_ID)) { // for compatibility: couple with old behaviour, copying the old file to avoid message loss long originalMessageID = largeMessage.getLongProperty(Message.HDR_ORIG_MESSAGE_ID); SequentialFile currentFile = createFileForLargeMessage(largeMessage.getMessageID(), true); if (!currentFile.exists()) { SequentialFile linkedFile = createFileForLargeMessage(originalMessageID, true); if (linkedFile.exists()) { linkedFile.copyTo(currentFile); linkedFile.close(); } } currentFile.close(); } return largeMessage; } @Override public void pageClosed(final SimpleString storeName, final int pageNumber) { if (isReplicated()) { readLock(); try { if (isReplicated()) replicator.pageClosed(storeName, pageNumber); } finally { readUnLock(); } } } @Override public void pageDeleted(final SimpleString storeName, final int pageNumber) { if (isReplicated()) { readLock(); try { if (isReplicated()) replicator.pageDeleted(storeName, pageNumber); } finally { readUnLock(); } } } @Override public void pageWrite(final PagedMessage message, final int pageNumber) { if (isReplicated()) { // Note: (https://issues.jboss.org/browse/HORNETQ-1059) // We have to replicate durable and non-durable messages on paging // since acknowledgments are written using the page-position. // Say you are sending durable and non-durable messages to a page // The ACKs would be done to wrong positions, and the backup would be a mess readLock(); try { if (isReplicated()) replicator.pageWrite(message, pageNumber); } finally { readUnLock(); } } } @Override public ByteBuffer allocateDirectBuffer(int size) { return journalFF.allocateDirectBuffer(size); } @Override public void freeDirectBuffer(ByteBuffer buffer) { journalFF.releaseBuffer(buffer); } public long storePendingLargeMessage(final long messageID) throws Exception { readLock(); try { long recordID = generateID(); messageJournal.appendAddRecord(recordID, JournalRecordIds.ADD_LARGE_MESSAGE_PENDING, new PendingLargeMessageEncoding(messageID), true, getContext(true)); return recordID; } finally { readUnLock(); } } // This should be accessed from this package only void deleteLargeMessageFile(final LargeServerMessage largeServerMessage) throws ActiveMQException { if (largeServerMessage.getPendingRecordID() < 0) { try { // The delete file happens asynchronously // And the client won't be waiting for the actual file to be deleted. // We set a temporary record (short lived) on the journal // to avoid a situation where the server is restarted and pending large message stays on forever largeServerMessage.setPendingRecordID(storePendingLargeMessage(largeServerMessage.getMessageID())); } catch (Exception e) { throw new ActiveMQInternalErrorException(e.getMessage(), e); } } final SequentialFile file = largeServerMessage.getFile(); if (file == null) { return; } if (largeServerMessage.isDurable() && isReplicated()) { readLock(); try { if (isReplicated() && replicator.isSynchronizing()) { synchronized (largeMessagesToDelete) { largeMessagesToDelete.add(Long.valueOf(largeServerMessage.getMessageID())); confirmLargeMessage(largeServerMessage); } return; } } finally { readUnLock(); } } Runnable deleteAction = new Runnable() { @Override public void run() { try { readLock(); try { if (replicator != null) { replicator.largeMessageDelete(largeServerMessage.getMessageID()); } file.delete(); // The confirm could only be done after the actual delete is done confirmLargeMessage(largeServerMessage); } finally { readUnLock(); } } catch (Exception e) { ActiveMQServerLogger.LOGGER.journalErrorDeletingMessage(e, largeServerMessage.getMessageID()); } } }; if (executor == null) { deleteAction.run(); } else { executor.execute(deleteAction); } } @Override public LargeServerMessage createLargeMessage() { return new LargeServerMessageImpl(this); } @Override public LargeServerMessage createLargeMessage(final long id, final Message message) throws Exception { readLock(); try { if (isReplicated()) { replicator.largeMessageBegin(id); } LargeServerMessageImpl largeMessage = (LargeServerMessageImpl) createLargeMessage(); largeMessage.copyHeadersAndProperties(message); largeMessage.setMessageID(id); // We do this here to avoid a case where the replication gets a list without this file // to avoid a race largeMessage.validateFile(); if (largeMessage.isDurable()) { // We store a marker on the journal that the large file is pending long pendingRecordID = storePendingLargeMessage(id); largeMessage.setPendingRecordID(pendingRecordID); } return largeMessage; } finally { readUnLock(); } } @Override public SequentialFile createFileForLargeMessage(final long messageID, LargeMessageExtension extension) { return largeMessagesFactory.createSequentialFile(messageID + extension.getExtension()); } /** * Send an entire journal file to a replicating backup server. */ private void sendJournalFile(JournalFile[] journalFiles, JournalContent type) throws Exception { for (JournalFile jf : journalFiles) { if (!started) return; replicator.syncJournalFile(jf, type); } } private JournalFile[] prepareJournalForCopy(Journal journal, JournalContent contentType, String nodeID, boolean autoFailBack) throws Exception { journal.forceMoveNextFile(); JournalFile[] datafiles = journal.getDataFiles(); replicator.sendStartSyncMessage(datafiles, contentType, nodeID, autoFailBack); return datafiles; } @Override public void startReplication(ReplicationManager replicationManager, PagingManager pagingManager, String nodeID, final boolean autoFailBack, long initialReplicationSyncTimeout) throws Exception { if (!started) { throw new IllegalStateException("JournalStorageManager must be started..."); } assert replicationManager != null; if (!(messageJournal instanceof JournalImpl) || !(bindingsJournal instanceof JournalImpl)) { throw ActiveMQMessageBundle.BUNDLE.notJournalImpl(); } // We first do a compact without any locks, to avoid copying unnecessary data over the network. // We do this without holding the storageManager lock, so the journal stays open while compact is being done originalMessageJournal.scheduleCompactAndBlock(-1); originalBindingsJournal.scheduleCompactAndBlock(-1); JournalFile[] messageFiles = null; JournalFile[] bindingsFiles = null; // We get a picture of the current sitaution on the large messages // and we send the current messages while more state is coming Map<Long, Pair<String, Long>> pendingLargeMessages = null; try { Map<SimpleString, Collection<Integer>> pageFilesToSync; storageManagerLock.writeLock().lock(); try { if (isReplicated()) throw new ActiveMQIllegalStateException("already replicating"); replicator = replicationManager; // Establishes lock originalMessageJournal.synchronizationLock(); originalBindingsJournal.synchronizationLock(); try { originalBindingsJournal.replicationSyncPreserveOldFiles(); originalMessageJournal.replicationSyncPreserveOldFiles(); pagingManager.lock(); try { pagingManager.disableCleanup(); messageFiles = prepareJournalForCopy(originalMessageJournal, JournalContent.MESSAGES, nodeID, autoFailBack); bindingsFiles = prepareJournalForCopy(originalBindingsJournal, JournalContent.BINDINGS, nodeID, autoFailBack); pageFilesToSync = getPageInformationForSync(pagingManager); pendingLargeMessages = recoverPendingLargeMessages(); } finally { pagingManager.unlock(); } } finally { originalMessageJournal.synchronizationUnlock(); originalBindingsJournal.synchronizationUnlock(); } bindingsJournal = new ReplicatedJournal(((byte) 0), originalBindingsJournal, replicator); messageJournal = new ReplicatedJournal((byte) 1, originalMessageJournal, replicator); // We need to send the list while locking otherwise part of the body might get sent too soon // it will send a list of IDs that we are allocating replicator.sendLargeMessageIdListMessage(pendingLargeMessages); } finally { storageManagerLock.writeLock().unlock(); } sendJournalFile(messageFiles, JournalContent.MESSAGES); sendJournalFile(bindingsFiles, JournalContent.BINDINGS); sendLargeMessageFiles(pendingLargeMessages); sendPagesToBackup(pageFilesToSync, pagingManager); storageManagerLock.writeLock().lock(); try { if (replicator != null) { replicator.sendSynchronizationDone(nodeID, initialReplicationSyncTimeout); performCachedLargeMessageDeletes(); } } finally { storageManagerLock.writeLock().unlock(); } } catch (Exception e) { logger.warn(e.getMessage(), e); stopReplication(); throw e; } finally { // Re-enable compact and reclaim of journal files originalBindingsJournal.replicationSyncFinished(); originalMessageJournal.replicationSyncFinished(); pagingManager.resumeCleanup(); } } private void sendLargeMessageFiles(final Map<Long, Pair<String, Long>> pendingLargeMessages) throws Exception { Iterator<Map.Entry<Long, Pair<String, Long>>> iter = pendingLargeMessages.entrySet().iterator(); while (started && iter.hasNext()) { Map.Entry<Long, Pair<String, Long>> entry = iter.next(); String fileName = entry.getValue().getA(); final long id = entry.getKey(); long size = entry.getValue().getB(); SequentialFile seqFile = largeMessagesFactory.createSequentialFile(fileName); if (!seqFile.exists()) continue; if (replicator != null) { replicator.syncLargeMessageFile(seqFile, size, id); } else { throw ActiveMQMessageBundle.BUNDLE.replicatorIsNull(); } } } /** * @param pagingManager * @return * @throws Exception */ private Map<SimpleString, Collection<Integer>> getPageInformationForSync(PagingManager pagingManager) throws Exception { Map<SimpleString, Collection<Integer>> info = new HashMap<>(); for (SimpleString storeName : pagingManager.getStoreNames()) { PagingStore store = pagingManager.getPageStore(storeName); info.put(storeName, store.getCurrentIds()); store.forceAnotherPage(); } return info; } private void checkAndCreateDir(final File dir, final boolean create) { if (!dir.exists()) { if (create) { if (!dir.mkdirs()) { throw new IllegalStateException("Failed to create directory " + dir); } } else { throw ActiveMQMessageBundle.BUNDLE.cannotCreateDir(dir.getAbsolutePath()); } } } /** * Sets a list of large message files into the replicationManager for synchronization. * <p> * Collects a list of existing large messages and their current size, passing re. * <p> * So we know how much of a given message to sync with the backup. Further data appends to the * messages will be replicated normally. * * @throws Exception */ private Map<Long, Pair<String, Long>> recoverPendingLargeMessages() throws Exception { Map<Long, Pair<String, Long>> largeMessages = new HashMap<>(); // only send durable messages... // listFiles append a "." to anything... List<String> filenames = largeMessagesFactory.listFiles("msg"); List<Long> idList = new ArrayList<>(); for (String filename : filenames) { Long id = getLargeMessageIdFromFilename(filename); if (!largeMessagesToDelete.contains(id)) { idList.add(id); SequentialFile seqFile = largeMessagesFactory.createSequentialFile(filename); long size = seqFile.size(); largeMessages.put(id, new Pair<>(filename, size)); } } return largeMessages; } /** * @param pageFilesToSync * @throws Exception */ private void sendPagesToBackup(Map<SimpleString, Collection<Integer>> pageFilesToSync, PagingManager manager) throws Exception { for (Map.Entry<SimpleString, Collection<Integer>> entry : pageFilesToSync.entrySet()) { if (!started) return; PagingStore store = manager.getPageStore(entry.getKey()); store.sendPages(replicator, entry.getValue()); } } private long getLargeMessageIdFromFilename(String filename) { return Long.parseLong(filename.split("\\.")[0]); } /** * Stops replication by resetting replication-related fields to their 'unreplicated' state. */ @Override public void stopReplication() { logger.trace("stopReplication()"); storageManagerLock.writeLock().lock(); try { if (replicator == null) return; bindingsJournal = originalBindingsJournal; messageJournal = originalMessageJournal; try { replicator.stop(); } catch (Exception e) { ActiveMQServerLogger.LOGGER.errorStoppingReplicationManager(e); } replicator = null; // delete inside the writeLock. Avoids a lot of state checking and races with // startReplication. // This method should not be called under normal circumstances performCachedLargeMessageDeletes(); } finally { storageManagerLock.writeLock().unlock(); } } @Override public final void addBytesToLargeMessage(final SequentialFile file, final long messageId, final byte[] bytes) throws Exception { readLock(); try { file.position(file.size()); file.writeDirect(ByteBuffer.wrap(bytes), false); if (isReplicated()) { replicator.largeMessageWrite(messageId, bytes); } } finally { readUnLock(); } } @Override public void injectMonitor(FileStoreMonitor monitor) throws Exception { if (journalFF != null) { monitor.addStore(journalFF.getDirectory()); } if (largeMessagesFactory != null) { monitor.addStore(largeMessagesFactory.getDirectory()); } if (bindingsFF != null) { monitor.addStore(bindingsFF.getDirectory()); } } }