org.apache.activemq.artemis.core.paging.cursor.impl.PageSubscriptionImpl Maven / Gradle / Ivy
/*
* 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.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.filter.Filter;
import org.apache.activemq.artemis.core.io.IOCallback;
import org.apache.activemq.artemis.core.paging.PageTransactionInfo;
import org.apache.activemq.artemis.core.paging.PagedMessage;
import org.apache.activemq.artemis.core.paging.PagingStore;
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.PageIterator;
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.PageSubscriptionCounter;
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.server.MessageReference;
import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.core.transaction.Transaction;
import org.apache.activemq.artemis.core.transaction.TransactionOperationAbstract;
import org.apache.activemq.artemis.core.transaction.TransactionPropertyIndexes;
import org.apache.activemq.artemis.core.transaction.impl.TransactionImpl;
import org.apache.activemq.artemis.utils.actors.ArtemisExecutor;
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet;
import org.apache.activemq.artemis.utils.collections.ConcurrentLongHashMap;
import org.jboss.logging.Logger;
import static org.apache.activemq.artemis.core.server.impl.QueueImpl.DELIVERY_TIMEOUT;
public final class PageSubscriptionImpl implements PageSubscription {
private static final Logger logger = Logger.getLogger(PageSubscriptionImpl.class);
private static final PagedReference dummyPagedRef = new PagedReferenceImpl(null, null, null);
private boolean empty = true;
/** for tests */
public AtomicInteger getScheduledCleanupCount() {
return scheduledCleanupCount;
}
// Number of scheduled cleanups, to avoid too many schedules
private final AtomicInteger scheduledCleanupCount = new AtomicInteger(0);
private volatile boolean autoCleanup = true;
private final StorageManager store;
private final long cursorId;
private Queue queue;
private final boolean persistent;
private final Filter filter;
private final PagingStore pageStore;
private final PageCursorProvider cursorProvider;
private volatile PagePosition lastAckedPosition;
private List recoveredACK;
private final SortedMap consumedPages = new TreeMap<>();
private final PageSubscriptionCounter counter;
private final ArtemisExecutor executor;
private final AtomicLong deliveredCount = new AtomicLong(0);
private final AtomicLong deliveredSize = new AtomicLong(0);
// Each CursorIterator will record their current PageReader in this map
private final ConcurrentLongHashMap pageReaders = new ConcurrentLongHashMap<>();
PageSubscriptionImpl(final PageCursorProvider cursorProvider,
final PagingStore pageStore,
final StorageManager store,
final ArtemisExecutor executor,
final Filter filter,
final long cursorId,
final boolean persistent) {
this.pageStore = pageStore;
this.store = store;
this.cursorProvider = cursorProvider;
this.cursorId = cursorId;
this.executor = executor;
this.filter = filter;
this.persistent = persistent;
this.counter = new PageSubscriptionCounterImpl(store, this, executor, persistent, cursorId);
}
// Public --------------------------------------------------------
@Override
public PagingStore getPagingStore() {
return pageStore;
}
@Override
public Queue getQueue() {
return queue;
}
@Override
public boolean isPaging() {
return pageStore.isPaging();
}
@Override
public void setQueue(Queue queue) {
this.queue = queue;
}
@Override
public void disableAutoCleanup() {
autoCleanup = false;
}
@Override
public void enableAutoCleanup() {
autoCleanup = true;
}
public PageCursorProvider getProvider() {
return cursorProvider;
}
@Override
public void notEmpty() {
synchronized (consumedPages) {
this.empty = false;
}
}
@Override
public void bookmark(PagePosition position) throws Exception {
PageCursorInfo cursorInfo = getPageInfo(position);
if (position.getMessageNr() > 0) {
cursorInfo.confirmed.addAndGet(position.getMessageNr());
}
confirmPosition(position);
}
@Override
public long getMessageCount() {
if (empty) {
return 0;
} else {
return counter.getValue() - deliveredCount.get();
}
}
@Override
public long getPersistentSize() {
if (empty) {
return 0;
} else {
//A negative value could happen if an old journal was loaded that didn't have
//size metrics for old records
long messageSize = counter.getPersistentSize() - deliveredSize.get();
return messageSize > 0 ? messageSize : 0;
}
}
@Override
public PageSubscriptionCounter getCounter() {
return counter;
}
/**
* A page marked as complete will be ignored until it's cleared.
*
* Usually paging is a stream of messages but in certain scenarios (such as a pending prepared
* TX) we may have big holes on the page streaming, and we will need to ignore such pages on the
* cursor/subscription.
*/
@Override
public boolean reloadPageCompletion(PagePosition position) throws Exception {
if (!pageStore.checkPageFileExists((int)position.getPageNr())) {
return false;
}
// if the current page is complete, we must move it out of the way
if (pageStore.getCurrentPage() != null &&
pageStore.getCurrentPage().getPageId() == position.getPageNr()) {
pageStore.forceAnotherPage();
}
PageCursorInfo info = new PageCursorInfo(position.getPageNr(), position.getMessageNr());
info.setCompleteInfo(position);
synchronized (consumedPages) {
consumedPages.put(Long.valueOf(position.getPageNr()), info);
}
return true;
}
@Override
public void scheduleCleanupCheck() {
if (autoCleanup) {
if (logger.isTraceEnabled()) {
logger.trace("Scheduling cleanup", new Exception("trace"));
}
if (scheduledCleanupCount.get() > 2) {
return;
}
scheduledCleanupCount.incrementAndGet();
executor.execute(new Runnable() {
@Override
public void run() {
try {
if (autoCleanup) {
cleanupEntries(false);
}
} catch (Exception e) {
ActiveMQServerLogger.LOGGER.problemCleaningCursorPages(e);
} finally {
scheduledCleanupCount.decrementAndGet();
}
}
});
}
}
@Override
public void onPageModeCleared(Transaction tx) throws Exception {
if (counter != null) {
// this could be null on testcases
counter.delete(tx);
}
this.empty = true;
}
/**
* It will cleanup all the records for completed pages
*/
@Override
public void cleanupEntries(final boolean completeDelete) throws Exception {
if (completeDelete) {
counter.delete();
}
if (logger.isTraceEnabled()) {
logger.trace("cleanupEntries", new Exception("trace"));
}
Transaction tx = new TransactionImpl(store);
boolean persist = false;
final ArrayList completedPages = new ArrayList<>();
// First get the completed pages using a lock
synchronized (consumedPages) {
// lastAckedPosition = null means no acks were done yet, so we are not ready to cleanup
if (lastAckedPosition == null) {
return;
}
for (Entry entry : consumedPages.entrySet()) {
PageCursorInfo info = entry.getValue();
if (info.isDone() && !info.isPendingDelete()) {
Page currentPage = pageStore.getCurrentPage();
if (currentPage != null && entry.getKey() == pageStore.getCurrentPage().getPageId() &&
currentPage.isLive()) {
logger.trace("We can't clear page " + entry.getKey() +
" now since it's the current page");
} else {
info.setPendingDelete();
completedPages.add(entry.getValue());
}
}
}
}
for (PageCursorInfo infoPG : completedPages) {
// HORNETQ-1017: There are a few cases where a pending transaction might set a big hole on the page system
// where we need to ignore these pages in case of a restart.
// for that reason when we delete complete ACKs we store a single record per page file that will
// be removed once the page file is deleted
// notice also that this record is added as part of the same transaction where the information is deleted.
// In case of a TX Failure (a crash on the server) this will be recovered on the next cleanup once the
// server is restarted.
// first will mark the page as complete
if (isPersistent()) {
PagePosition completePage = new PagePositionImpl(infoPG.getPageId(), infoPG.getNumberOfMessages());
infoPG.setCompleteInfo(completePage);
store.storePageCompleteTransactional(tx.getID(), this.getId(), completePage);
if (!persist) {
persist = true;
tx.setContainsPersistent();
}
}
// it will delete the page ack records
for (PagePosition pos : infoPG.acks) {
if (pos.getRecordID() >= 0) {
store.deleteCursorAcknowledgeTransactional(tx.getID(), pos.getRecordID());
if (!persist) {
// only need to set it once
tx.setContainsPersistent();
persist = true;
}
}
}
infoPG.acks.clear();
infoPG.acks = Collections.synchronizedSet(new LinkedHashSet());
infoPG.removedReferences.clear();
infoPG.removedReferences = new ConcurrentHashSet<>();
}
tx.addOperation(new TransactionOperationAbstract() {
@Override
public void afterCommit(final Transaction tx1) {
executor.execute(new Runnable() {
@Override
public void run() {
if (!completeDelete) {
cursorProvider.scheduleCleanup();
}
}
});
}
});
tx.commit();
}
@Override
public String toString() {
return "PageSubscriptionImpl [cursorId=" + cursorId + ", queue=" + queue + ", filter = " + filter + "]";
}
private PagedReference getReference(PagePosition pos) {
PagedMessage pagedMessage;
PageReader pageReader = pageReaders.get(pos.getPageNr());
if (pageReader != null) {
pagedMessage = pageReader.getMessage(pos, true, false);
} else {
pagedMessage = cursorProvider.getMessage(pos);
}
return cursorProvider.newReference(pos, pagedMessage, this);
}
@Override
public PageIterator iterator() {
return new CursorIterator();
}
@Override
public PageIterator iterator(boolean browsing) {
return new CursorIterator(browsing);
}
private PagedReference internalGetNext(final PagePositionAndFileOffset pos) {
PagePosition retPos = pos.nextPagePostion();
PageCache cache = null;
while (retPos.getPageNr() <= pageStore.getCurrentWritingPage()) {
PageReader pageReader = pageReaders.get(retPos.getPageNr());
if (pageReader == null) {
cache = cursorProvider.getPageCache(retPos.getPageNr());
} else {
cache = pageReader;
}
/**
* In following cases, we should move to the next page
* case 1: cache == null means file might be deleted unexpectedly.
* case 2: cache is not live and contains no messages.
* case 3: cache is not live and next message is beyond what's available at the current page.
*/
if (cache == null || (!cache.isLive() && (retPos.getMessageNr() >= cache.getNumberOfMessages() || cache.getNumberOfMessages() == 0))) {
// Save current empty page every time we move to next page
saveEmptyPageAsConsumedPage(cache);
retPos = moveNextPage(retPos);
cache = null;
} else {
// We need to break loop to get message if cache is live or the next message number is in the range of current page
break;
}
}
if (cache != null) {
PagedMessage serverMessage;
if (cache instanceof PageReader) {
serverMessage = ((PageReader) cache).getMessage(retPos, false, true);
PageCache previousPageCache = pageReaders.putIfAbsent(retPos.getPageNr(), (PageReader) cache);
if (previousPageCache != null && previousPageCache != cache) {
// Maybe other cursor iterators have added page reader, we have to close this one to avoid file leak
cache.close();
}
} else {
serverMessage = cache.getMessage(retPos);
}
if (serverMessage != null) {
return cursorProvider.newReference(retPos, serverMessage, this);
}
}
return null;
}
private PagePosition moveNextPage(final PagePosition pos) {
PagePosition retPos = pos;
PageReader pageReader = pageReaders.remove(pos.getPageNr());
if (pageReader != null) {
pageReader.close();
}
while (true) {
retPos = retPos.nextPage();
synchronized (consumedPages) {
PageCursorInfo pageInfo = consumedPages.get(retPos.getPageNr());
// any deleted or complete page will be ignored on the moveNextPage, we will just keep going
if (pageInfo == null || (!pageInfo.isPendingDelete() && pageInfo.getCompleteInfo() == null)) {
return retPos;
}
}
}
}
private boolean routed(PagedMessage message) {
long id = getId();
for (long qid : message.getQueueIDs()) {
if (qid == id) {
return true;
}
}
return false;
}
/**
*
*/
private synchronized PagePositionAndFileOffset getStartPosition() {
return new PagePositionAndFileOffset(-1, new PagePositionImpl(pageStore.getFirstPage(), -1));
}
@Override
public void confirmPosition(final Transaction tx, final PagePosition position) throws Exception {
// if the cursor is persistent
if (persistent) {
store.storeCursorAcknowledgeTransactional(tx.getID(), cursorId, position);
}
installTXCallback(tx, position);
}
private void confirmPosition(final Transaction tx, final PagePosition position, final long persistentSize) throws Exception {
// if the cursor is persistent
if (persistent) {
store.storeCursorAcknowledgeTransactional(tx.getID(), cursorId, position);
}
installTXCallback(tx, position, persistentSize);
}
@Override
public void ackTx(final Transaction tx, final PagedReference reference) throws Exception {
//pre-calculate persistentSize
final long persistentSize = getPersistentSize(reference);
confirmPosition(tx, reference.getPosition(), persistentSize);
counter.increment(tx, -1, -persistentSize);
PageTransactionInfo txInfo = getPageTransaction(reference);
if (txInfo != null) {
txInfo.storeUpdate(store, pageStore.getPagingManager(), tx);
}
}
@Override
public void ack(final PagedReference reference) throws Exception {
// Need to do the ACK and counter atomically (inside a TX) or the counter could get out of sync
Transaction tx = new TransactionImpl(this.store);
ackTx(tx, reference);
tx.commit();
}
@Override
public boolean contains(PagedReference ref) throws Exception {
// We first verify if the message was routed to this queue
boolean routed = false;
for (long idRef : ref.getPagedMessage().getQueueIDs()) {
if (idRef == this.cursorId) {
routed = true;
break;
}
}
if (!routed) {
return false;
} else {
// if it's been routed here, we have to verify if it was acked
return !getPageInfo(ref.getPosition()).isAck(ref.getPosition());
}
}
@Override
public void confirmPosition(final PagePosition position) throws Exception {
// if we are dealing with a persistent cursor
if (persistent) {
store.storeCursorAcknowledge(cursorId, position);
}
store.afterCompleteOperations(new IOCallback() {
volatile String error = "";
@Override
public void onError(final int errorCode, final String errorMessage) {
error = " errorCode=" + errorCode + ", msg=" + errorMessage;
ActiveMQServerLogger.LOGGER.pageSubscriptionError(this, error);
}
@Override
public void done() {
processACK(position);
}
@Override
public String toString() {
return IOCallback.class.getSimpleName() + "(" + PageSubscriptionImpl.class.getSimpleName() + ") " + error;
}
});
}
@Override
public long getFirstPage() {
synchronized (consumedPages) {
if (empty && consumedPages.isEmpty()) {
return -1;
}
long lastPageSeen = 0;
for (Map.Entry info : consumedPages.entrySet()) {
lastPageSeen = info.getKey();
if (!info.getValue().isDone() && !info.getValue().isPendingDelete()) {
return info.getKey();
}
}
return lastPageSeen;
}
}
@Override
public void addPendingDelivery(final PagePosition position) {
PageCursorInfo info = getPageInfo(position);
if (info != null) {
info.incrementPendingTX();
}
}
@Override
public void removePendingDelivery(final PagePosition position) {
PageCursorInfo info = getPageInfo(position);
if (info != null) {
info.decrementPendingTX();
}
}
@Override
public void redeliver(final PageIterator iterator, final PagePosition position) {
iterator.redeliver(position);
synchronized (consumedPages) {
PageCursorInfo pageInfo = consumedPages.get(position.getPageNr());
if (pageInfo != null) {
pageInfo.decrementPendingTX();
} else {
// this shouldn't really happen.
}
}
}
@Override
public PagedMessage queryMessage(PagePosition pos) {
PageReader pageReader = pageReaders.get(pos.getPageNr());
if (pageReader != null) {
return pageReader.getMessage(pos, true, false);
} else {
return cursorProvider.getMessage(pos);
}
}
/**
* Theres no need to synchronize this method as it's only called from journal load on startup
*/
@Override
public void reloadACK(final PagePosition position) {
if (recoveredACK == null) {
recoveredACK = new LinkedList<>();
}
recoveredACK.add(position);
}
@Override
public void reloadPreparedACK(final Transaction tx, final PagePosition position) {
deliveredCount.incrementAndGet();
installTXCallback(tx, position);
}
@Override
public void positionIgnored(final PagePosition position) {
processACK(position);
}
@Override
public void lateDeliveryRollback(PagePosition position) {
PageCursorInfo cursorInfo = processACK(position);
cursorInfo.decrementPendingTX();
}
@Override
public boolean isComplete(long page) {
logger.tracef("%s isComplete %d", this, page);
synchronized (consumedPages) {
if (empty && consumedPages.isEmpty()) {
if (logger.isTraceEnabled()) {
logger.tracef("isComplete(%d)::Subscription %s has empty=%s, consumedPages.isEmpty=%s", page, this, empty, consumedPages.isEmpty());
}
return true;
}
PageCursorInfo info = consumedPages.get(page);
if (info == null && empty) {
logger.tracef("isComplete(%d)::::Couldn't find info and it is empty", page);
return true;
} else {
boolean isDone = info != null && info.isDone();
if (logger.isTraceEnabled()) {
logger.tracef("isComplete(%d):: found info=%s, isDone=%s", (Object) page, info, isDone);
}
return isDone;
}
}
}
/**
* All the data associated with the cursor should go away here
*/
@Override
public void destroy() throws Exception {
final long tx = store.generateID();
try {
boolean isPersistent = false;
synchronized (consumedPages) {
for (PageCursorInfo cursor : consumedPages.values()) {
for (PagePosition info : cursor.acks) {
if (info.getRecordID() >= 0) {
isPersistent = true;
store.deleteCursorAcknowledgeTransactional(tx, info.getRecordID());
}
}
PagePosition completeInfo = cursor.getCompleteInfo();
if (completeInfo != null && completeInfo.getRecordID() >= 0) {
store.deletePageComplete(completeInfo.getRecordID());
cursor.setCompleteInfo(null);
}
}
}
if (isPersistent) {
store.commit(tx);
}
cursorProvider.close(this);
} catch (Exception e) {
try {
store.rollback(tx);
} catch (Exception ignored) {
// exception of the exception.. nothing that can be done here
}
}
}
@Override
public long getId() {
return cursorId;
}
@Override
public boolean isPersistent() {
return persistent;
}
@Override
public void processReload() throws Exception {
if (recoveredACK != null) {
if (logger.isTraceEnabled()) {
logger.trace("********** processing reload!!!!!!!");
}
Collections.sort(recoveredACK);
long txDeleteCursorOnReload = -1;
for (PagePosition pos : recoveredACK) {
lastAckedPosition = pos;
PageCursorInfo pageInfo = getPageInfo(pos);
PageCache cache = null;
if (pageInfo != null) {
cache = pageInfo.getValidCache();
}
if (cache == null || pos.getMessageNr() >= 0 && cache.getMessage(pos) == null) {
ActiveMQServerLogger.LOGGER.pageNotFound(pos);
if (txDeleteCursorOnReload == -1) {
txDeleteCursorOnReload = store.generateID();
}
store.deleteCursorAcknowledgeTransactional(txDeleteCursorOnReload, pos.getRecordID());
} else {
pageInfo.loadACK(pos);
}
}
if (txDeleteCursorOnReload >= 0) {
store.commit(txDeleteCursorOnReload);
}
recoveredACK.clear();
recoveredACK = null;
}
}
@Override
public void flushExecutors() {
if (!executor.flush(10, TimeUnit.SECONDS)) {
ActiveMQServerLogger.LOGGER.timedOutFlushingExecutorsPagingCursor(this);
}
}
@Override
public void stop() {
flushExecutors();
}
@Override
public void printDebug() {
printDebug(toString());
}
public void printDebug(final String msg) {
System.out.println("Debug information on PageCurorImpl- " + msg);
for (PageCursorInfo info : consumedPages.values()) {
System.out.println(info);
}
}
@Override
public void onDeletePage(Page deletedPage) throws Exception {
PageCursorInfo info;
synchronized (consumedPages) {
info = consumedPages.remove(Long.valueOf(deletedPage.getPageId()));
}
if (info != null) {
PagePosition completeInfo = info.getCompleteInfo();
if (completeInfo != null) {
try {
store.deletePageComplete(completeInfo.getRecordID());
} catch (Exception e) {
ActiveMQServerLogger.LOGGER.errorDeletingPageCompleteRecord(e);
}
info.setCompleteInfo(null);
}
for (PagePosition deleteInfo : info.acks) {
if (deleteInfo.getRecordID() >= 0) {
try {
store.deleteCursorAcknowledge(deleteInfo.getRecordID());
} catch (Exception e) {
ActiveMQServerLogger.LOGGER.errorDeletingPageCompleteRecord(e);
}
}
}
info.acks.clear();
}
}
@Override
public ArtemisExecutor getExecutor() {
return executor;
}
@Override
public void reloadPageInfo(long pageNr) {
getPageInfo(pageNr);
}
private PageCursorInfo getPageInfo(final PagePosition pos) {
return getPageInfo(pos.getPageNr());
}
public PageCursorInfo getPageInfo(final long pageNr) {
synchronized (consumedPages) {
PageCursorInfo pageInfo = consumedPages.get(pageNr);
if (pageInfo == null) {
PageCache cache = cursorProvider.getPageCache(pageNr);
if (cache == null) {
return null;
}
assert pageNr == cache.getPageId();
pageInfo = new PageCursorInfo(cache);
consumedPages.put(pageNr, pageInfo);
}
return pageInfo;
}
}
private void saveEmptyPageAsConsumedPage(final PageCache cache) {
if (cache != null && cache.getNumberOfMessages() == 0) {
synchronized (consumedPages) {
PageCursorInfo pageInfo = consumedPages.get(cache.getPageId());
if (pageInfo == null) {
consumedPages.put(cache.getPageId(), new PageCursorInfo(cache));
}
}
}
}
// Package protected ---------------------------------------------
// Protected -----------------------------------------------------
private boolean match(final Message message) {
if (filter == null) {
return true;
} else {
return filter.match(message);
}
}
// Private -------------------------------------------------------
// To be called only after the ACK has been processed and guaranteed to be on storage
// The only exception is on non storage events such as not matching messages
private PageCursorInfo processACK(final PagePosition pos) {
if (lastAckedPosition == null || pos.compareTo(lastAckedPosition) > 0) {
if (logger.isTraceEnabled()) {
logger.trace("a new position is being processed as ACK");
}
if (lastAckedPosition != null && lastAckedPosition.getPageNr() != pos.getPageNr()) {
if (logger.isTraceEnabled()) {
logger.trace("Scheduling cleanup on pageSubscription for address = " + pageStore.getAddress() + " queue = " + this.getQueue().getName());
}
// there's a different page being acked, we will do the check right away
if (autoCleanup) {
scheduleCleanupCheck();
}
}
lastAckedPosition = pos;
}
PageCursorInfo info = getPageInfo(pos);
// This could be null if the page file was removed
if (info == null) {
// This could become null if the page file was deleted, or if the queue was removed maybe?
// it's better to diagnose it (based on support tickets) instead of NPE
ActiveMQServerLogger.LOGGER.nullPageCursorInfo(this.getPagingStore().getAddress().toString(), pos.toString(), cursorId);
} else {
info.addACK(pos);
}
return info;
}
private void installTXCallback(final Transaction tx, final PagePosition position) {
installTXCallback(tx, position, -1);
}
/**
* @param tx
* @param position
* @param persistentSize if negative it needs to be calculated on the fly
*/
private void installTXCallback(final Transaction tx, final PagePosition position, final long persistentSize) {
if (position.getRecordID() >= 0) {
// It needs to persist, otherwise the cursor will return to the fist page position
tx.setContainsPersistent();
}
PageCursorInfo info = getPageInfo(position);
if (info != null) {
PageCache cache = info.getCache();
if (cache != null) {
final long size;
if (persistentSize < 0) {
//cache.getMessage is potentially expensive depending
//on the current cache size and which message is queried
size = getPersistentSize(cache.getMessage(position));
} else {
size = persistentSize;
}
position.setPersistentSize(size);
}
logger.tracef("InstallTXCallback looking up pagePosition %s, result=%s", position, info);
info.remove(position);
PageCursorTX cursorTX = (PageCursorTX) tx.getProperty(TransactionPropertyIndexes.PAGE_CURSOR_POSITIONS);
if (cursorTX == null) {
cursorTX = new PageCursorTX();
tx.putProperty(TransactionPropertyIndexes.PAGE_CURSOR_POSITIONS, cursorTX);
tx.addOperation(cursorTX);
}
cursorTX.addPositionConfirmation(this, position);
}
}
private PageTransactionInfo getPageTransaction(final PagedReference reference) throws ActiveMQException {
if (reference.getTransactionID() >= 0) {
return pageStore.getPagingManager().getTransaction(reference.getTransactionID());
} else {
return null;
}
}
/**
* A callback from the PageCursorInfo. It will be called when all the messages on a page have been acked
*
* @param info
*/
private void onPageDone(final PageCursorInfo info) {
if (autoCleanup) {
scheduleCleanupCheck();
}
}
// Inner classes -------------------------------------------------
/**
* This will hold information about the pending ACKs towards a page.
*
* This instance will be released as soon as the entire page is consumed, releasing the memory at
* that point The ref counts are increased also when a message is ignored for any reason.
*/
public final class PageCursorInfo {
// Number of messages existent on this page
private int numberOfMessages;
private final long pageId;
// Confirmed ACKs on this page
private Set acks = Collections.synchronizedSet(new LinkedHashSet());
private WeakReference cache;
private Set removedReferences = new ConcurrentHashSet<>();
// The page was live at the time of the creation
private final boolean wasLive;
// There's a pending TX to add elements on this page
// also can be used to prevent the page from being deleted too soon.
private final AtomicInteger pendingTX = new AtomicInteger(0);
// There's a pending delete on the async IO pipe
// We're holding this object to avoid delete the pages before the IO is complete,
// however we can't delete these records again
private boolean pendingDelete;
/**
* This is to be set when all the messages are complete on a given page, and we cleanup the records that are marked on it
*/
private PagePosition completePage;
// We need a separate counter as the cursor may be ignoring certain values because of incomplete transactions or
// expressions
private final AtomicInteger confirmed = new AtomicInteger(0);
public boolean isAck(PagePosition position) {
return completePage != null || acks.contains(position);
}
@Override
public String toString() {
try {
return "PageCursorInfo::pageNr=" + pageId +
" numberOfMessage = " +
numberOfMessages +
", confirmed = " +
confirmed +
", isDone=" +
this.isDone() +
" wasLive = " + wasLive;
} catch (Exception e) {
return "PageCursorInfo::pageNr=" + pageId +
" numberOfMessage = " +
numberOfMessages +
", confirmed = " +
confirmed +
", isDone=" +
e.toString();
}
}
private PageCursorInfo(final long pageId, final int numberOfMessages) {
if (numberOfMessages < 0) {
throw new IllegalStateException("numberOfMessages = " + numberOfMessages + " instead of being >=0");
}
this.pageId = pageId;
wasLive = false;
this.numberOfMessages = numberOfMessages;
logger.tracef("Created PageCursorInfo for pageNr=%d, numberOfMessages=%d, not live", pageId, numberOfMessages);
}
private PageCursorInfo(final PageCache cache) {
Objects.requireNonNull(cache);
this.pageId = cache.getPageId();
wasLive = cache.isLive();
this.cache = new WeakReference<>(cache);
if (!wasLive) {
final int numberOfMessages = cache.getNumberOfMessages();
assert numberOfMessages >= 0;
this.numberOfMessages = numberOfMessages;
logger.tracef("Created PageCursorInfo for pageNr=%d, numberOfMessages=%d, cache=%s, not live", pageId, this.numberOfMessages, cache);
} else {
//given that is live, the exact value must be get directly from cache
this.numberOfMessages = -1;
logger.tracef("Created PageCursorInfo for pageNr=%d, cache=%s, live", pageId, cache);
}
}
/**
* @param completePage
*/
public void setCompleteInfo(final PagePosition completePage) {
logger.tracef("Setting up complete page %s on cursor %s on subscription %s", completePage, this, PageSubscriptionImpl.this);
this.completePage = completePage;
}
public PagePosition getCompleteInfo() {
return completePage;
}
public boolean isDone() {
if (logger.isTraceEnabled()) {
logger.trace(PageSubscriptionImpl.this + "::PageCursorInfo(" + pageId + ")::isDone checking with completePage!=null->" + (completePage != null) + " getNumberOfMessages=" + getNumberOfMessages() + ", confirmed=" + confirmed.get() + " and pendingTX=" + pendingTX.get());
}
return completePage != null || (getNumberOfMessages() == confirmed.get() && pendingTX.get() == 0);
}
public boolean isPendingDelete() {
return pendingDelete || completePage != null;
}
public void setPendingDelete() {
pendingDelete = true;
}
/**
* @return the pageId
*/
public long getPageId() {
return pageId;
}
public void incrementPendingTX() {
pendingTX.incrementAndGet();
}
public void decrementPendingTX() {
pendingTX.decrementAndGet();
checkDone();
}
public boolean isRemoved(final PagePosition pos) {
return removedReferences.contains(pos);
}
public void remove(final PagePosition position) {
removedReferences.add(position);
}
public void addACK(final PagePosition posACK) {
if (logger.isTraceEnabled()) {
try {
logger.trace("numberOfMessages = " + getNumberOfMessages() +
" confirmed = " +
(confirmed.get() + 1) +
" pendingTX = " + pendingTX +
", pageNr = " +
pageId + " posACK = " + posACK);
} catch (Throwable ignored) {
logger.debug(ignored.getMessage(), ignored);
}
}
boolean added = internalAddACK(posACK);
// Negative could mean a bookmark on the first element for the page (example -1)
if (added && posACK.getMessageNr() >= 0) {
confirmed.incrementAndGet();
checkDone();
}
}
// To be called during reload
public void loadACK(final PagePosition posACK) {
if (internalAddACK(posACK) && posACK.getMessageNr() >= 0) {
confirmed.incrementAndGet();
}
}
private boolean internalAddACK(final PagePosition posACK) {
removedReferences.add(posACK);
return acks.add(posACK);
}
/**
*
*/
protected void checkDone() {
if (isDone()) {
onPageDone(this);
}
}
private int getNumberOfMessagesFromPageCache() {
// if the page was live at any point, we need to
// get the number of messages from the page-cache
PageCache localCache = getValidCache();
if (localCache == null) return 0;
int numberOfMessage = localCache.getNumberOfMessages();
if (!localCache.isLive()) {
//to avoid further "live" queries
this.numberOfMessages = numberOfMessage;
}
return numberOfMessage;
}
public PageCache getValidCache() {
PageCache localCache = this.cache.get();
if (localCache == null) {
localCache = cursorProvider.getPageCache(pageId);
// this could happen if the file does not exist any more, after cleanup
if (localCache == null) {
return null;
}
this.cache = new WeakReference<>(localCache);
}
return localCache;
}
private int getNumberOfMessages() {
final int numberOfMessages = this.numberOfMessages;
if (wasLive) {
if (numberOfMessages < 0) {
return getNumberOfMessagesFromPageCache();
} else {
return numberOfMessages;
}
} else {
assert numberOfMessages >= 0;
return numberOfMessages;
}
}
/**
* @return the cache
*/
public PageCache getCache() {
return cache != null ? cache.get() : null;
}
public int getPendingTx() {
return pendingTX.get();
}
}
private final class PageCursorTX extends TransactionOperationAbstract {
private final Map> pendingPositions = new HashMap<>();
private void addPositionConfirmation(final PageSubscriptionImpl cursor, final PagePosition position) {
List list = pendingPositions.get(cursor);
if (list == null) {
list = new LinkedList<>();
pendingPositions.put(cursor, list);
}
list.add(position);
}
@Override
public void afterCommit(final Transaction tx) {
for (Entry> entry : pendingPositions.entrySet()) {
PageSubscriptionImpl cursor = entry.getKey();
List positions = entry.getValue();
for (PagePosition confirmed : positions) {
cursor.processACK(confirmed);
cursor.deliveredCount.decrementAndGet();
cursor.deliveredSize.addAndGet(-confirmed.getPersistentSize());
}
}
}
@Override
public List getRelatedMessageReferences() {
return Collections.emptyList();
}
}
private class CursorIterator implements PageIterator {
private PagePositionAndFileOffset position = null;
private PagePositionAndFileOffset lastOperation = null;
private volatile boolean isredelivery = false;
private PagedReference currentDelivery = null;
private volatile PagedReference lastRedelivery = null;
private final boolean browsing;
// We only store the position for redeliveries. They will be read from the SoftCache again during delivery.
private final java.util.Queue redeliveries = new LinkedList<>();
/**
* next element taken on hasNext test.
* it has to be delivered on next next operation
*/
private volatile PagedReference cachedNext;
private CursorIterator(boolean browsing) {
this.browsing = browsing;
}
private CursorIterator() {
this.browsing = false;
}
@Override
public void redeliver(PagePosition reference) {
synchronized (redeliveries) {
redeliveries.add(reference);
}
}
@Override
public void repeat() {
if (isredelivery) {
synchronized (redeliveries) {
cachedNext = lastRedelivery;
}
} else {
if (lastOperation == null) {
position = null;
} else {
position = lastOperation;
}
}
}
@Override
public synchronized PagedReference next() {
if (cachedNext != null) {
currentDelivery = cachedNext;
cachedNext = null;
return currentDelivery;
}
if (position == null) {
position = getStartPosition();
}
currentDelivery = moveNext();
return currentDelivery;
}
private PagedReference moveNext() {
synchronized (PageSubscriptionImpl.this) {
boolean match = false;
PagedReference message;
PagePositionAndFileOffset lastPosition = position;
PagePositionAndFileOffset tmpPosition = position;
long timeout = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(DELIVERY_TIMEOUT);
do {
if (System.nanoTime() - timeout > 0) {
return dummyPagedRef;
}
synchronized (redeliveries) {
PagePosition redelivery = redeliveries.poll();
if (redelivery != null) {
// There's a redelivery pending, we will get it out of that pool instead
isredelivery = true;
PagedReference redeliveredMsg = getReference(redelivery);
lastRedelivery = redeliveredMsg;
return redeliveredMsg;
} else {
lastRedelivery = null;
isredelivery = false;
}
message = internalGetNext(tmpPosition);
}
if (message == null) {
break;
}
int nextFileOffset = message.getPosition().getFileOffset() == -1 ? -1 : message.getPosition().getFileOffset() + message.getPagedMessage().getStoredSize() + Page.SIZE_RECORD;
tmpPosition = new PagePositionAndFileOffset(nextFileOffset, message.getPosition());
boolean valid = true;
boolean ignored = false;
// Validate the scenarios where the message should be considered not valid even to be considered
// 1st... is it routed?
valid = routed(message.getPagedMessage());
if (!valid) {
ignored = true;
}
PageCursorInfo info = getPageInfo(message.getPosition().getPageNr());
position = tmpPosition;
if (!browsing && info != null && (info.isRemoved(message.getPosition()) || info.getCompleteInfo() != null)) {
continue;
}
if (info != null && info.isAck(message.getPosition())) {
continue;
}
// 2nd ... if TX, is it committed?
if (valid && message.getPagedMessage().getTransactionID() >= 0) {
PageTransactionInfo tx = pageStore.getPagingManager().getTransaction(message.getPagedMessage().getTransactionID());
if (tx == null) {
ActiveMQServerLogger.LOGGER.pageSubscriptionCouldntLoad(message.getPagedMessage().getTransactionID(), message.getPosition(), pageStore.getAddress(), queue.getName());
valid = false;
ignored = true;
} else {
if (tx.deliverAfterCommit(CursorIterator.this, PageSubscriptionImpl.this, message.getPosition())) {
valid = false;
ignored = false;
}
}
}
// 3rd... was it previously removed?
if (valid) {
// We don't create a PageCursorInfo unless we are doing a write operation (ack or removing)
// Say you have a Browser that will only read the files... there's no need to control PageCursors is
// nothing
// is being changed. That's why the false is passed as a parameter here
if (!browsing && info != null && info.isRemoved(message.getPosition())) {
valid = false;
}
}
if (valid) {
if (browsing) {
match = match(message.getMessage());
} else {
// if not browsing, we will just trust the routing on the queue
match = true;
}
} else if (!browsing && ignored) {
positionIgnored(message.getPosition());
}
}
while (!match);
if (message != null) {
lastOperation = lastPosition;
}
return message;
}
}
@Override
public synchronized int tryNext() {
// if an unbehaved program called hasNext twice before next, we only cache it once.
if (cachedNext != null) {
return 1;
}
if (!pageStore.isPaging()) {
return 0;
}
PagedReference pagedReference = next();
if (pagedReference == dummyPagedRef) {
return 2;
} else {
cachedNext = pagedReference;
return cachedNext == null ? 0 : 1;
}
}
/**
* QueueImpl::deliver could be calling hasNext while QueueImpl.depage could be using next and hasNext as well.
* It would be a rare race condition but I would prefer avoiding that scenario
*/
@Override
public synchronized boolean hasNext() {
int status;
while ((status = tryNext()) == 2) {
}
return status == 0 ? false : true;
}
@Override
public void remove() {
deliveredCount.incrementAndGet();
PagedReference delivery = currentDelivery;
if (delivery != null) {
PageCursorInfo info = PageSubscriptionImpl.this.getPageInfo(delivery.getPosition());
if (info != null) {
info.remove(delivery.getPosition());
}
}
}
@Override
public void close() {
// When the CursorIterator(especially browse one) is closed, we need to close page they opened
if (position != null) {
PageReader pageReader = pageReaders.remove(position.pagePosition.getPageNr());
if (pageReader != null) {
pageReader.close();
}
}
}
}
/**
* @return the deliveredCount
*/
@Override
public long getDeliveredCount() {
return deliveredCount.get();
}
/**
* @return the deliveredSize
*/
@Override
public long getDeliveredSize() {
return deliveredSize.get();
}
@Override
public void incrementDeliveredSize(long size) {
deliveredSize.addAndGet(size);
}
private long getPersistentSize(PagedMessage msg) {
try {
return msg != null && msg.getPersistentSize() > 0 ? msg.getPersistentSize() : 0;
} catch (ActiveMQException e) {
logger.warn("Error computing persistent size of message: " + msg, e);
return 0;
}
}
private long getPersistentSize(PagedReference ref) {
try {
return ref != null && ref.getPersistentSize() > 0 ? ref.getPersistentSize() : 0;
} catch (ActiveMQException e) {
logger.warn("Error computing persistent size of message: " + ref, e);
return 0;
}
}
protected static class PagePositionAndFileOffset {
private final int nextFileOffset;
private final PagePosition pagePosition;
PagePositionAndFileOffset(int nextFileOffset, PagePosition pagePosition) {
this.nextFileOffset = nextFileOffset;
this.pagePosition = pagePosition;
}
PagePosition nextPagePostion() {
int messageNr = pagePosition.getMessageNr();
return new PagePositionImpl(pagePosition.getPageNr(), messageNr + 1, messageNr + 1 == 0 ? 0 : nextFileOffset);
}
}
}