/* * 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.paging.cursor.impl; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import org.apache.activemq.artemis.core.filter.Filter; import org.apache.activemq.artemis.core.paging.PagedMessage; import org.apache.activemq.artemis.core.paging.PagingStore; import org.apache.activemq.artemis.core.paging.cursor.NonExistentPage; import org.apache.activemq.artemis.core.paging.cursor.PageCache; import org.apache.activemq.artemis.core.paging.cursor.PageCursorProvider; import org.apache.activemq.artemis.core.paging.cursor.PagePosition; import org.apache.activemq.artemis.core.paging.cursor.PageSubscription; import org.apache.activemq.artemis.core.paging.cursor.PagedReference; import org.apache.activemq.artemis.core.paging.cursor.PagedReferenceImpl; import org.apache.activemq.artemis.core.paging.impl.Page; import org.apache.activemq.artemis.core.persistence.StorageManager; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.transaction.Transaction; import org.apache.activemq.artemis.core.transaction.impl.TransactionImpl; import org.apache.activemq.artemis.utils.FutureLatch; import org.apache.activemq.artemis.utils.SoftValueHashMap; import org.jboss.logging.Logger; /** * A PageProviderIMpl * * TODO: this may be moved entirely into PagingStore as there's an one-to-one relationship here * However I want to keep this isolated as much as possible during development */ public class PageCursorProviderImpl implements PageCursorProvider { // Constants ----------------------------------------------------- private static final Logger logger = Logger.getLogger(PageCursorProviderImpl.class); // Attributes ---------------------------------------------------- /** * As an optimization, avoid subsequent schedules as they are unnecessary */ protected final AtomicInteger scheduledCleanup = new AtomicInteger(0); protected volatile boolean cleanupEnabled = true; protected final PagingStore pagingStore; protected final StorageManager storageManager; // This is the same executor used at the PageStoreImpl. One Executor per pageStore private final Executor executor; private final SoftValueHashMap<Long, PageCache> softCache; private final ConcurrentMap<Long, PageSubscription> activeCursors = new ConcurrentHashMap<>(); // Static -------------------------------------------------------- // Constructors -------------------------------------------------- public PageCursorProviderImpl(final PagingStore pagingStore, final StorageManager storageManager, final Executor executor, final int maxCacheSize) { this.pagingStore = pagingStore; this.storageManager = storageManager; this.executor = executor; this.softCache = new SoftValueHashMap<>(maxCacheSize); } // Public -------------------------------------------------------- @Override public synchronized PageSubscription createSubscription(long cursorID, Filter filter, boolean persistent) { if (logger.isTraceEnabled()) { logger.trace(this.pagingStore.getAddress() + " creating subscription " + cursorID + " with filter " + filter, new Exception("trace")); } if (activeCursors.containsKey(cursorID)) { throw new IllegalStateException("Cursor " + cursorID + " had already been created"); } PageSubscription activeCursor = new PageSubscriptionImpl(this, pagingStore, storageManager, executor, filter, cursorID, persistent); activeCursors.put(cursorID, activeCursor); return activeCursor; } @Override public synchronized PageSubscription getSubscription(long cursorID) { return activeCursors.get(cursorID); } @Override public PagedMessage getMessage(final PagePosition pos) { PageCache cache = getPageCache(pos.getPageNr()); if (cache == null || pos.getMessageNr() >= cache.getNumberOfMessages()) { // sanity check, this should never happen unless there's a bug throw new NonExistentPage("Invalid messageNumber passed = " + pos + " on " + cache); } return cache.getMessage(pos.getMessageNr()); } @Override public PagedReference newReference(final PagePosition pos, final PagedMessage msg, final PageSubscription subscription) { return new PagedReferenceImpl(pos, msg, subscription); } @Override public PageCache getPageCache(final long pageId) { try { PageCache cache; synchronized (softCache) { if (pageId > pagingStore.getCurrentWritingPage()) { return null; } cache = softCache.get(pageId); if (cache == null) { if (!pagingStore.checkPageFileExists((int) pageId)) { return null; } cache = createPageCache(pageId); // anyone reading from this cache will have to wait reading to finish first // we also want only one thread reading this cache logger.tracef("adding pageCache pageNr=%d into cursor = %s", pageId, this.pagingStore.getAddress()); readPage((int) pageId, cache); softCache.put(pageId, cache); } } return cache; } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } private void readPage(int pageId, PageCache cache) throws Exception { Page page = null; try { page = pagingStore.createPage(pageId); storageManager.beforePageRead(); page.open(); List<PagedMessage> pgdMessages = page.read(storageManager); cache.setMessages(pgdMessages.toArray(new PagedMessage[pgdMessages.size()])); } finally { try { if (page != null) { page.close(false); } } catch (Throwable ignored) { } storageManager.afterPageRead(); } } @Override public void addPageCache(PageCache cache) { logger.tracef("Add page cache %s", cache); synchronized (softCache) { softCache.put(cache.getPageId(), cache); } } @Override public void setCacheMaxSize(final int size) { softCache.setMaxElements(size); } @Override public int getCacheSize() { synchronized (softCache) { return softCache.size(); } } @Override public void clearCache() { synchronized (softCache) { softCache.clear(); } } @Override public void processReload() throws Exception { Collection<PageSubscription> cursorList = this.activeCursors.values(); for (PageSubscription cursor : cursorList) { cursor.processReload(); } if (!cursorList.isEmpty()) { // https://issues.jboss.org/browse/JBPAPP-10338 if you ack out of order, // the min page could be beyond the first page. // we have to reload any previously acked message long cursorsMinPage = checkMinPage(cursorList); // checkMinPage will return MaxValue if there aren't any pages or any cursors if (cursorsMinPage != Long.MAX_VALUE) { for (long startPage = pagingStore.getFirstPage(); startPage < cursorsMinPage; startPage++) { for (PageSubscription cursor : cursorList) { cursor.reloadPageInfo(startPage); } } } } cleanup(); } @Override public void stop() { for (PageSubscription cursor : activeCursors.values()) { cursor.stop(); } waitForFuture(); } private void waitForFuture() { FutureLatch future = new FutureLatch(); executor.execute(future); while (!future.await(10000)) { ActiveMQServerLogger.LOGGER.timedOutStoppingPagingCursor(future, executor); } } @Override public void flushExecutors() { for (PageSubscription cursor : activeCursors.values()) { cursor.flushExecutors(); } waitForFuture(); } @Override public void close(PageSubscription cursor) { activeCursors.remove(cursor.getId()); scheduleCleanup(); } @Override public void scheduleCleanup() { if (logger.isTraceEnabled()) { logger.trace("scheduling cleanup", new Exception("trace")); } if (!cleanupEnabled || scheduledCleanup.intValue() > 2) { // Scheduled cleanup was already scheduled before.. never mind! // or we have cleanup disabled return; } scheduledCleanup.incrementAndGet(); executor.execute(new Runnable() { @Override public void run() { storageManager.setContext(storageManager.newSingleThreadContext()); try { if (cleanupEnabled) { cleanup(); } } finally { storageManager.clearContext(); scheduledCleanup.decrementAndGet(); } } }); } /** * Delete everything associated with any queue on this address. * This is to be called when the address is about to be released from paging. * Hence the PagingStore will be holding a write lock, meaning no messages are going to be paged at this time. * So, we shouldn't lock anything after this method, to avoid dead locks between the writeLock and any synchronization with the CursorProvider. */ @Override public void onPageModeCleared() { ArrayList<PageSubscription> subscriptions = cloneSubscriptions(); Transaction tx = new TransactionImpl(storageManager); for (PageSubscription sub : subscriptions) { try { sub.onPageModeCleared(tx); } catch (Exception e) { ActiveMQServerLogger.LOGGER.warn("Error while cleaning paging on queue " + sub.getQueue().getName(), e); } } try { tx.commit(); } catch (Exception e) { ActiveMQServerLogger.LOGGER.warn("Error while cleaning page, during the commit", e); } } @Override public void disableCleanup() { this.cleanupEnabled = false; } @Override public void resumeCleanup() { this.cleanupEnabled = true; scheduleCleanup(); } @Override public void cleanup() { logger.tracef("performing page cleanup %s", this); ArrayList<Page> depagedPages = new ArrayList<>(); while (true) { if (pagingStore.lock(100)) { break; } if (!pagingStore.isStarted()) return; } logger.tracef("%s locked", this); synchronized (this) { try { if (!pagingStore.isStarted()) { return; } if (pagingStore.getNumberOfPages() == 0) { return; } ArrayList<PageSubscription> cursorList = cloneSubscriptions(); long minPage = checkMinPage(cursorList); logger.debugf("Asserting cleanup for address %s, firstPage=%d", pagingStore.getAddress(), minPage); // if the current page is being written... // on that case we need to move to verify it in a different way if (minPage == pagingStore.getCurrentWritingPage() && pagingStore.getCurrentPage().getNumberOfMessages() > 0) { boolean complete = checkPageCompletion(cursorList, minPage); if (!pagingStore.isStarted()) { return; } // All the pages on the cursor are complete.. so we will cleanup everything and store a bookmark if (complete) { cleanupComplete(cursorList); } } for (long i = pagingStore.getFirstPage(); i < minPage; i++) { if (!checkPageCompletion(cursorList, i)) { break; } Page page = pagingStore.depage(); if (page == null) { break; } depagedPages.add(page); } if (pagingStore.getNumberOfPages() == 0 || pagingStore.getNumberOfPages() == 1 && pagingStore.getCurrentPage().getNumberOfMessages() == 0) { pagingStore.stopPaging(); } else { if (logger.isTraceEnabled()) { logger.trace("Couldn't cleanup page on address " + this.pagingStore.getAddress() + " as numberOfPages == " + pagingStore.getNumberOfPages() + " and currentPage.numberOfMessages = " + pagingStore.getCurrentPage().getNumberOfMessages()); } } } catch (Exception ex) { ActiveMQServerLogger.LOGGER.problemCleaningPageAddress(ex, pagingStore.getAddress()); return; } finally { pagingStore.unlock(); } } finishCleanup(depagedPages); } // Protected as a way to inject testing protected void cleanupComplete(ArrayList<PageSubscription> cursorList) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Address " + pagingStore.getAddress() + " is leaving page mode as all messages are consumed and acknowledged from the page store"); } pagingStore.forceAnotherPage(); Page currentPage = pagingStore.getCurrentPage(); storeBookmark(cursorList, currentPage); pagingStore.stopPaging(); } // Protected as a way to inject testing protected void finishCleanup(ArrayList<Page> depagedPages) { logger.tracef("this(%s) finishing cleanup on %s", this, depagedPages); try { for (Page depagedPage : depagedPages) { PageCache cache; PagedMessage[] pgdMessages; synchronized (softCache) { cache = softCache.get((long) depagedPage.getPageId()); } if (logger.isTraceEnabled()) { logger.trace("Removing pageNr=" + depagedPage.getPageId() + " from page-cache"); } if (cache == null) { // The page is not on cache any more // We need to read the page-file before deleting it // to make sure we remove any large-messages pending storageManager.beforePageRead(); List<PagedMessage> pgdMessagesList = null; try { depagedPage.open(); pgdMessagesList = depagedPage.read(storageManager); } finally { try { depagedPage.close(false); } catch (Exception e) { } storageManager.afterPageRead(); } depagedPage.close(false); pgdMessages = pgdMessagesList.toArray(new PagedMessage[pgdMessagesList.size()]); } else { pgdMessages = cache.getMessages(); } depagedPage.delete(pgdMessages); onDeletePage(depagedPage); synchronized (softCache) { softCache.remove((long) depagedPage.getPageId()); } } } catch (Exception ex) { ActiveMQServerLogger.LOGGER.problemCleaningPageAddress(ex, pagingStore.getAddress()); return; } } private boolean checkPageCompletion(ArrayList<PageSubscription> cursorList, long minPage) { logger.tracef("checkPageCompletion(%d)", minPage); boolean complete = true; for (PageSubscription cursor : cursorList) { if (!cursor.isComplete(minPage)) { if (logger.isDebugEnabled()) { logger.debug("Cursor " + cursor + " was considered incomplete at pageNr=" + minPage); } complete = false; break; } else { if (logger.isDebugEnabled()) { logger.debug("Cursor " + cursor + " was considered **complete** at pageNr=" + minPage); } } } return complete; } /** * @return */ private synchronized ArrayList<PageSubscription> cloneSubscriptions() { ArrayList<PageSubscription> cursorList = new ArrayList<>(activeCursors.values()); return cursorList; } protected void onDeletePage(Page deletedPage) throws Exception { List<PageSubscription> subscriptions = cloneSubscriptions(); for (PageSubscription subs : subscriptions) { subs.onDeletePage(deletedPage); } } /** * @param cursorList * @param currentPage * @throws Exception */ protected void storeBookmark(ArrayList<PageSubscription> cursorList, Page currentPage) throws Exception { try { // First step: Move every cursor to the next bookmarked page (that was just created) for (PageSubscription cursor : cursorList) { cursor.confirmPosition(new PagePositionImpl(currentPage.getPageId(), -1)); } while (!storageManager.waitOnOperations(5000)) { ActiveMQServerLogger.LOGGER.problemCompletingOperations(storageManager.getContext()); } } finally { for (PageSubscription cursor : cursorList) { cursor.enableAutoCleanup(); } } } @Override public void printDebug() { System.out.println("Debug information for PageCursorProviderImpl:"); for (PageCache cache : softCache.values()) { System.out.println("Cache " + cache); } } @Override public String toString() { return "PageCursorProviderImpl{" + "pagingStore=" + pagingStore + '}'; } // Package protected --------------------------------------------- // Protected ----------------------------------------------------- /* Protected as we may let test cases to instrument the test */ protected PageCacheImpl createPageCache(final long pageId) throws Exception { return new PageCacheImpl(pagingStore.createPage((int) pageId)); } // Private ------------------------------------------------------- /** * This method is synchronized because we want it to be atomic with the cursors being used */ private long checkMinPage(Collection<PageSubscription> cursorList) { long minPage = Long.MAX_VALUE; for (PageSubscription cursor : cursorList) { long firstPage = cursor.getFirstPage(); if (logger.isDebugEnabled()) { logger.debug(this.pagingStore.getAddress() + " has a cursor " + cursor + " with first page=" + firstPage); } // the cursor will return -1 if the cursor is empty if (firstPage >= 0 && firstPage < minPage) { minPage = firstPage; } } if (logger.isDebugEnabled()) { logger.debug(this.pagingStore.getAddress() + " has minPage=" + minPage); } return minPage; } // Inner classes ------------------------------------------------- }