/* * 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.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.apache.activemq.artemis.api.core.Pair; 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.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.TransactionOperation; 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.jboss.logging.Logger; /** * This class will encapsulate the persistent counters for the PagingSubscription */ public class PageSubscriptionCounterImpl implements PageSubscriptionCounter { private static final Logger logger = Logger.getLogger(PageSubscriptionCounterImpl.class); private static final int FLUSH_COUNTER = 1000; private final long subscriptionID; // the journal record id that is holding the current value private long recordID = -1; private boolean persistent; private final PageSubscription subscription; private final StorageManager storage; private final Executor executor; private final AtomicLong value = new AtomicLong(0); private final AtomicLong added = new AtomicLong(0); private final AtomicLong pendingValue = new AtomicLong(0); private final LinkedList<Long> incrementRecords = new LinkedList<>(); // We are storing pending counters for non transactional writes on page // we will recount a page case we still see pending records // as soon as we close a page we remove these records replacing by a regular page increment record // A Map per pageID, each page will have a set of IDs, with the increment on each one private final Map<Long, Pair<Long, AtomicInteger>> pendingCounters = new HashMap<>(); private LinkedList<Pair<Long, Integer>> loadList; private final Runnable cleanupCheck = new Runnable() { @Override public void run() { cleanup(); } }; public PageSubscriptionCounterImpl(final StorageManager storage, final PageSubscription subscription, final Executor executor, final boolean persistent, final long subscriptionID) { this.subscriptionID = subscriptionID; this.executor = executor; this.storage = storage; this.persistent = persistent; this.subscription = subscription; } @Override public long getValueAdded() { return added.get() + pendingValue.get(); } @Override public long getValue() { return value.get() + pendingValue.get(); } /** * This is used only on non transactional paging * * @param page * @param increment * @throws Exception */ @Override public synchronized void pendingCounter(Page page, int increment) throws Exception { if (!persistent) { return; // nothing to be done } Pair<Long, AtomicInteger> pendingInfo = pendingCounters.get((long) page.getPageId()); if (pendingInfo == null) { // We have to make sure this is sync here // not syncing this to disk may cause the page files to be out of sync on pages. // we can't afford the case where a page file is written without a record here long id = storage.storePendingCounter(this.subscriptionID, page.getPageId(), increment); pendingInfo = new Pair<>(id, new AtomicInteger(1)); pendingCounters.put((long) page.getPageId(), pendingInfo); } else { pendingInfo.getB().addAndGet(increment); } pendingValue.addAndGet(increment); page.addPendingCounter(this); } /** * Cleanup temporary page counters on non transactional paged messages * * @param pageID */ @Override public void cleanupNonTXCounters(final long pageID) throws Exception { Pair<Long, AtomicInteger> pendingInfo; synchronized (this) { pendingInfo = pendingCounters.remove(pageID); } if (pendingInfo != null) { final AtomicInteger valueCleaned = pendingInfo.getB(); Transaction tx = new TransactionImpl(storage); storage.deletePendingPageCounter(tx.getID(), pendingInfo.getA()); // To apply the increment of the value just being cleaned increment(tx, valueCleaned.get()); tx.addOperation(new TransactionOperationAbstract() { @Override public void afterCommit(Transaction tx) { pendingValue.addAndGet(-valueCleaned.get()); } }); tx.commit(); } } @Override public void increment(Transaction tx, int add) throws Exception { if (tx == null) { if (persistent) { long id = storage.storePageCounterInc(this.subscriptionID, add); incrementProcessed(id, add); } else { incrementProcessed(-1, add); } } else { if (persistent) { tx.setContainsPersistent(); long id = storage.storePageCounterInc(tx.getID(), this.subscriptionID, add); applyIncrementOnTX(tx, id, add); } else { applyIncrementOnTX(tx, -1, add); } } } /** * This method will install the TXs * * @param tx * @param recordID1 * @param add */ @Override public void applyIncrementOnTX(Transaction tx, long recordID1, int add) { CounterOperations oper = (CounterOperations) tx.getProperty(TransactionPropertyIndexes.PAGE_COUNT_INC); if (oper == null) { oper = new CounterOperations(); tx.putProperty(TransactionPropertyIndexes.PAGE_COUNT_INC, oper); tx.addOperation(oper); } oper.operations.add(new ItemOper(this, recordID1, add)); } @Override public synchronized void loadValue(final long recordID1, final long value1) { if (this.subscription != null) { // it could be null on testcases... which is ok this.subscription.notEmpty(); } this.value.set(value1); this.added.set(value1); this.recordID = recordID1; } public synchronized void incrementProcessed(long id, int add) { addInc(id, add); if (incrementRecords.size() > FLUSH_COUNTER) { executor.execute(cleanupCheck); } } @Override public void delete() throws Exception { Transaction tx = new TransactionImpl(storage); delete(tx); tx.commit(); } @Override public void delete(Transaction tx) throws Exception { // always lock the StorageManager first. storage.readLock(); try { synchronized (this) { for (Long record : incrementRecords) { storage.deleteIncrementRecord(tx.getID(), record.longValue()); tx.setContainsPersistent(); } if (recordID >= 0) { storage.deletePageCounter(tx.getID(), this.recordID); tx.setContainsPersistent(); } recordID = -1; value.set(0); incrementRecords.clear(); } } finally { storage.readUnLock(); } } @Override public void loadInc(long id, int add) { if (loadList == null) { loadList = new LinkedList<>(); } loadList.add(new Pair<>(id, add)); } @Override public void processReload() { if (loadList != null) { if (subscription != null) { // it could be null on testcases subscription.notEmpty(); } for (Pair<Long, Integer> incElement : loadList) { value.addAndGet(incElement.getB()); added.addAndGet(incElement.getB()); incrementRecords.add(incElement.getA()); } loadList.clear(); loadList = null; } } @Override public synchronized void addInc(long id, int variance) { value.addAndGet(variance); if (variance > 0) { added.addAndGet(variance); } if (id >= 0) { incrementRecords.add(id); } } /** * used on testing only */ public void setPersistent(final boolean persistent) { this.persistent = persistent; } /** * This method should always be called from a single threaded executor */ protected void cleanup() { ArrayList<Long> deleteList; long valueReplace; synchronized (this) { if (incrementRecords.size() <= FLUSH_COUNTER) { return; } valueReplace = value.get(); deleteList = new ArrayList<>(incrementRecords); incrementRecords.clear(); } long newRecordID = -1; long txCleanup = storage.generateID(); try { for (Long value1 : deleteList) { storage.deleteIncrementRecord(txCleanup, value1); } if (recordID >= 0) { storage.deletePageCounter(txCleanup, recordID); } newRecordID = storage.storePageCounter(txCleanup, subscriptionID, valueReplace); if (logger.isTraceEnabled()) { logger.trace("Replacing page-counter record = " + recordID + " by record = " + newRecordID + " on subscriptionID = " + this.subscriptionID + " for queue = " + this.subscription.getQueue().getName()); } storage.commit(txCleanup); } catch (Exception e) { newRecordID = recordID; ActiveMQServerLogger.LOGGER.problemCleaningPagesubscriptionCounter(e); try { storage.rollback(txCleanup); } catch (Exception ignored) { } } finally { recordID = newRecordID; } } private static class ItemOper { private ItemOper(PageSubscriptionCounterImpl counter, long id, int add) { this.counter = counter; this.id = id; this.amount = add; } PageSubscriptionCounterImpl counter; long id; int amount; } private static class CounterOperations extends TransactionOperationAbstract implements TransactionOperation { LinkedList<ItemOper> operations = new LinkedList<>(); @Override public void afterCommit(Transaction tx) { for (ItemOper oper : operations) { oper.counter.incrementProcessed(oper.id, oper.amount); } } } }