/* * 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.journal.impl; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Collection; import java.util.List; import java.util.concurrent.BlockingDeque; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.apache.activemq.artemis.api.core.ActiveMQIOErrorException; import org.apache.activemq.artemis.core.io.SequentialFile; import org.apache.activemq.artemis.core.io.SequentialFileFactory; import org.apache.activemq.artemis.journal.ActiveMQJournalBundle; import org.apache.activemq.artemis.journal.ActiveMQJournalLogger; import org.jboss.logging.Logger; /** * This is a helper class for the Journal, which will control access to dataFiles, openedFiles and freeFiles * Guaranteeing that they will be delivered in order to the Journal */ public class JournalFilesRepository { private static final Logger logger = Logger.getLogger(JournalFilesRepository.class); /** * Used to debug the consistency of the journal ordering. * <br> * This is meant to be false as these extra checks would cause performance issues */ private static final boolean CHECK_CONSISTENCE = false; private final SequentialFileFactory fileFactory; private final JournalImpl journal; private final BlockingDeque<JournalFile> dataFiles = new LinkedBlockingDeque<>(); private final ConcurrentLinkedQueue<JournalFile> freeFiles = new ConcurrentLinkedQueue<>(); private final BlockingQueue<JournalFile> openedFiles = new LinkedBlockingQueue<>(); private final AtomicLong nextFileID = new AtomicLong(0); private final int maxAIO; private final int minFiles; private final int poolSize; private final int fileSize; private final String filePrefix; private final String fileExtension; private final int userVersion; private final AtomicInteger freeFilesCount = new AtomicInteger(0); private Executor openFilesExecutor; private final Runnable pushOpenRunnable = new Runnable() { @Override public void run() { try { pushOpenedFile(); } catch (Exception e) { ActiveMQJournalLogger.LOGGER.errorPushingFile(e); fileFactory.onIOError(e, "unable to open ", null); } } }; public JournalFilesRepository(final SequentialFileFactory fileFactory, final JournalImpl journal, final String filePrefix, final String fileExtension, final int userVersion, final int maxAIO, final int fileSize, final int minFiles, final int poolSize) { if (filePrefix == null) { throw new IllegalArgumentException("filePrefix cannot be null"); } if (fileExtension == null) { throw new IllegalArgumentException("fileExtension cannot be null"); } if (maxAIO <= 0) { throw new IllegalArgumentException("maxAIO must be a positive number"); } this.fileFactory = fileFactory; this.maxAIO = maxAIO; this.filePrefix = filePrefix; this.fileExtension = fileExtension; this.minFiles = minFiles; this.fileSize = fileSize; this.poolSize = poolSize; this.userVersion = userVersion; this.journal = journal; } // Public -------------------------------------------------------- public void setExecutor(final Executor fileExecutor) { this.openFilesExecutor = fileExecutor; } public void clear() throws Exception { dataFiles.clear(); freeFiles.clear(); freeFilesCount.set(0); for (JournalFile file : openedFiles) { try { file.getFile().close(); } catch (Exception e) { ActiveMQJournalLogger.LOGGER.errorClosingFile(e); } } openedFiles.clear(); } public int getMaxAIO() { return maxAIO; } public String getFileExtension() { return fileExtension; } public String getFilePrefix() { return filePrefix; } public void calculateNextfileID(final List<JournalFile> files) { for (JournalFile file : files) { final long fileIdFromFile = file.getFileID(); final long fileIdFromName = getFileNameID(file.getFile().getFileName()); // The compactor could create a fileName but use a previously assigned ID. // Because of that we need to take both parts into account setNextFileID(Math.max(fileIdFromName, fileIdFromFile)); } } /** * Set the {link #nextFileID} value to {@code targetUpdate} if the current value is less than * {@code targetUpdate}. * * Notice that {@code nextFileID} is incremented before being used, see * {@link JournalFilesRepository#generateFileID()}. * * @param targetUpdate */ public void setNextFileID(final long targetUpdate) { while (true) { final long current = nextFileID.get(); if (current >= targetUpdate) return; if (nextFileID.compareAndSet(current, targetUpdate)) return; } } public void ensureMinFiles() throws Exception { int filesToCreate = minFiles - (dataFiles.size() + freeFilesCount.get()); if (filesToCreate > 0) { for (int i = 0; i < filesToCreate; i++) { // Keeping all files opened can be very costly (mainly on AIO) freeFiles.add(createFile(false, false, true, false, -1)); freeFilesCount.getAndIncrement(); } } } public void openFile(final JournalFile file, final boolean multiAIO) throws Exception { if (multiAIO) { file.getFile().open(); } else { file.getFile().open(1, false); } file.getFile().position(file.getFile().calculateBlockStart(JournalImpl.SIZE_HEADER)); } // Data File Operations ========================================== public JournalFile[] getDataFilesArray() { return dataFiles.toArray(new JournalFile[dataFiles.size()]); } public JournalFile pollLastDataFile() { return dataFiles.pollLast(); } public void removeDataFile(final JournalFile file) { if (!dataFiles.remove(file)) { ActiveMQJournalLogger.LOGGER.couldNotRemoveFile(file); } } public int getDataFilesCount() { return dataFiles.size(); } public Collection<JournalFile> getDataFiles() { return dataFiles; } public void clearDataFiles() { dataFiles.clear(); } public void addDataFileOnTop(final JournalFile file) { dataFiles.addFirst(file); if (CHECK_CONSISTENCE) { checkDataFiles(); } } public String debugFiles() { StringBuilder buffer = new StringBuilder(); buffer.append("**********\nCurrent File = " + journal.getCurrentFile() + "\n"); buffer.append("**********\nDataFiles:\n"); for (JournalFile file : dataFiles) { buffer.append(file.toString() + "\n"); } buffer.append("*********\nFreeFiles:\n"); for (JournalFile file : freeFiles) { buffer.append(file.toString() + "\n"); } return buffer.toString(); } public synchronized void checkDataFiles() { long seq = -1; for (JournalFile file : dataFiles) { if (file.getFileID() <= seq) { ActiveMQJournalLogger.LOGGER.checkFiles(); ActiveMQJournalLogger.LOGGER.info(debugFiles()); ActiveMQJournalLogger.LOGGER.seqOutOfOrder(); throw new IllegalStateException("Sequence out of order"); } if (journal.getCurrentFile() != null && journal.getCurrentFile().getFileID() <= file.getFileID()) { ActiveMQJournalLogger.LOGGER.checkFiles(); ActiveMQJournalLogger.LOGGER.info(debugFiles()); ActiveMQJournalLogger.LOGGER.currentFile(file.getFileID(), journal.getCurrentFile().getFileID(), file.getFileID(), (journal.getCurrentFile() == file)); // throw new RuntimeException ("Check failure!"); } if (journal.getCurrentFile() == file) { throw new RuntimeException("Check failure! Current file listed as data file!"); } seq = file.getFileID(); } long lastFreeId = -1; for (JournalFile file : freeFiles) { if (file.getFileID() <= lastFreeId) { ActiveMQJournalLogger.LOGGER.checkFiles(); ActiveMQJournalLogger.LOGGER.info(debugFiles()); ActiveMQJournalLogger.LOGGER.fileIdOutOfOrder(); throw new RuntimeException("Check failure!"); } lastFreeId = file.getFileID(); if (file.getFileID() < seq) { ActiveMQJournalLogger.LOGGER.checkFiles(); ActiveMQJournalLogger.LOGGER.info(debugFiles()); ActiveMQJournalLogger.LOGGER.fileTooSmall(); // throw new RuntimeException ("Check failure!"); } } } public void addDataFileOnBottom(final JournalFile file) { dataFiles.add(file); if (CHECK_CONSISTENCE) { checkDataFiles(); } } // Free File Operations ========================================== public int getFreeFilesCount() { return freeFilesCount.get(); } /** * @param file * @throws Exception */ public synchronized void addFreeFile(final JournalFile file, final boolean renameTmp) throws Exception { addFreeFile(file, renameTmp, true); } /** * @param file * @param renameTmp - should rename the file as it's being added to free files * @param checkDelete - should delete the file if max condition has been met * @throws Exception */ public synchronized void addFreeFile(final JournalFile file, final boolean renameTmp, final boolean checkDelete) throws Exception { long calculatedSize = 0; try { calculatedSize = file.getFile().size(); } catch (Exception e) { throw new IllegalStateException(e.getMessage() + " file: " + file); } if (calculatedSize != fileSize) { ActiveMQJournalLogger.LOGGER.deletingFile(file); file.getFile().delete(); } else if (!checkDelete || (freeFilesCount.get() + dataFiles.size() + 1 + openedFiles.size() < poolSize) || (poolSize < 0)) { // Re-initialise it if (logger.isTraceEnabled()) { logger.trace("Adding free file " + file); } JournalFile jf = reinitializeFile(file); if (renameTmp) { jf.getFile().renameTo(JournalImpl.renameExtensionFile(jf.getFile().getFileName(), ".tmp")); } freeFiles.add(jf); freeFilesCount.getAndIncrement(); } else { if (logger.isTraceEnabled()) { logger.trace("DataFiles.size() = " + dataFiles.size()); logger.trace("openedFiles.size() = " + openedFiles.size()); logger.trace("minfiles = " + minFiles + ", poolSize = " + poolSize); logger.trace("Free Files = " + freeFilesCount.get()); logger.trace("File " + file + " being deleted as freeFiles.size() + dataFiles.size() + 1 + openedFiles.size() (" + (freeFilesCount.get() + dataFiles.size() + 1 + openedFiles.size()) + ") < minFiles (" + minFiles + ")"); } file.getFile().delete(); } if (CHECK_CONSISTENCE) { checkDataFiles(); } } public Collection<JournalFile> getFreeFiles() { return freeFiles; } public JournalFile getFreeFile() { JournalFile file = freeFiles.remove(); freeFilesCount.getAndDecrement(); return file; } // Opened files operations ======================================= public int getOpenedFilesCount() { return openedFiles.size(); } /** * <p>This method will instantly return the opened file, and schedule opening and reclaiming.</p> * <p>In case there are no cached opened files, this method will block until the file was opened, * what would happen only if the system is under heavy load by another system (like a backup system, or a DB sharing the same box as ActiveMQ).</p> * * @throws ActiveMQIOErrorException In case the file could not be opened */ public JournalFile openFile() throws InterruptedException, ActiveMQIOErrorException { if (logger.isTraceEnabled()) { logger.trace("enqueueOpenFile with openedFiles.size=" + openedFiles.size()); } // First try to get an open file, that's prepared and already open JournalFile nextFile = openedFiles.poll(); if (nextFile == null) { // if there's none, push to open pushOpen(); nextFile = openedFiles.poll(5, TimeUnit.SECONDS); } if (openedFiles.isEmpty()) { // if empty, push to open one. pushOpen(); } if (nextFile == null) { logger.debug("Could not get a file in 5 seconds, it will retry directly, without an executor"); try { nextFile = takeFile(true, true, true, false); } catch (Exception e) { fileFactory.onIOError(e, "unable to open ", null); // We need to reconnect the current file with the timed buffer as we were not able to roll the file forward // If you don't do this you will get a NPE in TimedBuffer::checkSize where it uses the bufferobserver fileFactory.activateBuffer(journal.getCurrentFile().getFile()); throw ActiveMQJournalBundle.BUNDLE.fileNotOpened(); } } if (logger.isTraceEnabled()) { logger.trace("Returning file " + nextFile); } return nextFile; } private void pushOpen() { if (openFilesExecutor == null) { pushOpenRunnable.run(); } else { openFilesExecutor.execute(pushOpenRunnable); } } /** * Open a file and place it into the openedFiles queue */ public void pushOpenedFile() throws Exception { JournalFile nextOpenedFile = takeFile(true, true, true, false); if (logger.isTraceEnabled()) { logger.trace("pushing openFile " + nextOpenedFile); } if (!openedFiles.offer(nextOpenedFile)) { ActiveMQJournalLogger.LOGGER.failedToAddFile(nextOpenedFile); } } public void closeFile(final JournalFile file) throws Exception { fileFactory.deactivateBuffer(); file.getFile().close(); if (!dataFiles.contains(file)) { // This is not a retry from openFile // If you don't check this then retries keep adding the same file into // dataFiles list and the compactor then re-adds multiple copies of the // same file into freeFiles. // The consequence of that is that you can end up with the same file // twice in a row in the list of openedFiles // The consequence of that is that JournalImpl::switchFileIfNecessary // will throw throw new IllegalStateException("Invalid logic on buffer allocation") // because the file will be checked effectively twice and the buffer will // not fit in it dataFiles.add(file); } } /** * This will get a File from freeFile without initializing it * * @return uninitialized JournalFile * @throws Exception * @see JournalImpl#initFileHeader(SequentialFileFactory, SequentialFile, int, long) */ public JournalFile takeFile(final boolean keepOpened, final boolean multiAIO, final boolean initFile, final boolean tmpCompactExtension) throws Exception { JournalFile nextFile = null; nextFile = freeFiles.poll(); if (nextFile != null) { freeFilesCount.getAndDecrement(); } if (nextFile == null) { nextFile = createFile(keepOpened, multiAIO, initFile, tmpCompactExtension, -1); } else { if (tmpCompactExtension) { SequentialFile sequentialFile = nextFile.getFile(); sequentialFile.renameTo(sequentialFile.getFileName() + ".cmp"); } if (keepOpened) { openFile(nextFile, multiAIO); } } return nextFile; } /** * Creates files for journal synchronization of a replicated backup. * * In order to simplify synchronization, the file IDs in the backup match those in the live * server. * * @param fileID the fileID to use when creating the file. */ public JournalFile createRemoteBackupSyncFile(long fileID) throws Exception { return createFile(false, false, true, false, fileID); } /** * This method will create a new file on the file system, pre-fill it with FILL_CHARACTER * * @param keepOpened * @return an initialized journal file * @throws Exception */ private JournalFile createFile(final boolean keepOpened, final boolean multiAIO, final boolean init, final boolean tmpCompact, final long fileIdPreSet) throws Exception { if (System.getSecurityManager() == null) { return createFile0(keepOpened, multiAIO, init, tmpCompact, fileIdPreSet); } else { try { return AccessController.doPrivileged(new PrivilegedExceptionAction<JournalFile>() { @Override public JournalFile run() throws Exception { return createFile0(keepOpened, multiAIO, init, tmpCompact, fileIdPreSet); } }); } catch (PrivilegedActionException e) { throw unwrapException(e); } } } private RuntimeException unwrapException(PrivilegedActionException e) throws Exception { Throwable c = e.getCause(); if (c instanceof RuntimeException) { throw (RuntimeException) c; } else if (c instanceof Error) { throw (Error) c; } else { throw new RuntimeException(c); } } private JournalFile createFile0(final boolean keepOpened, final boolean multiAIO, final boolean init, final boolean tmpCompact, final long fileIdPreSet) throws Exception { long fileID = fileIdPreSet != -1 ? fileIdPreSet : generateFileID(); final String fileName = createFileName(tmpCompact, fileID); if (logger.isTraceEnabled()) { logger.trace("Creating file " + fileName); } String tmpFileName = fileName + ".tmp"; SequentialFile sequentialFile = fileFactory.createSequentialFile(tmpFileName); sequentialFile.open(1, false); if (init) { sequentialFile.fill(fileSize); JournalImpl.initFileHeader(fileFactory, sequentialFile, userVersion, fileID); } long position = sequentialFile.position(); sequentialFile.close(); if (logger.isTraceEnabled()) { logger.trace("Renaming file " + tmpFileName + " as " + fileName); } sequentialFile.renameTo(fileName); if (keepOpened) { if (multiAIO) { sequentialFile.open(); } else { sequentialFile.open(1, false); } sequentialFile.position(position); } return new JournalFileImpl(sequentialFile, fileID, JournalImpl.FORMAT_VERSION); } /** * @param tmpCompact * @param fileID * @return */ private String createFileName(final boolean tmpCompact, final long fileID) { String fileName; if (tmpCompact) { fileName = filePrefix + "-" + fileID + "." + fileExtension + ".cmp"; } else { fileName = filePrefix + "-" + fileID + "." + fileExtension; } return fileName; } private long generateFileID() { return nextFileID.incrementAndGet(); } /** * Get the ID part of the name */ private long getFileNameID(final String fileName) { try { return Long.parseLong(fileName.substring(filePrefix.length() + 1, fileName.indexOf('.'))); } catch (Throwable e) { ActiveMQJournalLogger.LOGGER.errorRetrievingID(e, fileName); return 0; } } // Discard the old JournalFile and set it with a new ID private JournalFile reinitializeFile(final JournalFile file) throws Exception { long newFileID = generateFileID(); SequentialFile sf = file.getFile(); sf.open(1, false); int position = JournalImpl.initFileHeader(fileFactory, sf, userVersion, newFileID); JournalFile jf = new JournalFileImpl(sf, newFileID, JournalImpl.FORMAT_VERSION); sf.position(position); sf.close(); return jf; } @Override public String toString() { return "JournalFilesRepository(dataFiles=" + dataFiles + ", freeFiles=" + freeFiles + ", openedFiles=" + openedFiles + ")"; } }