/*
* 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.impl;
import java.nio.file.FileStore;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.core.paging.PageTransactionInfo;
import org.apache.activemq.artemis.core.paging.PagingManager;
import org.apache.activemq.artemis.core.paging.PagingStore;
import org.apache.activemq.artemis.core.paging.PagingStoreFactory;
import org.apache.activemq.artemis.core.server.ActiveMQScheduledComponent;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.core.server.files.FileStoreMonitor;
import org.apache.activemq.artemis.core.settings.HierarchicalRepository;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet;
import org.jboss.logging.Logger;
public final class PagingManagerImpl implements PagingManager {
private static final int ARTEMIS_DEBUG_PAGING_INTERVAL = Integer.valueOf(System.getProperty("artemis.debug.paging.interval", "0"));
private static final Logger logger = Logger.getLogger(PagingManagerImpl.class);
private volatile boolean started = false;
/**
* Lock used at the start of synchronization between a live server and its backup.
* Synchronization will lock all {@link PagingStore} instances, and so any operation here that
* requires a lock on a {@link PagingStore} instance needs to take a read-lock on
* {@link #syncLock} to avoid dead-locks.
*/
private final ReentrantReadWriteLock syncLock = new ReentrantReadWriteLock();
private final Set<PagingStore> blockedStored = new ConcurrentHashSet<>();
private final ConcurrentMap<SimpleString, PagingStore> stores = new ConcurrentHashMap<>();
private final HierarchicalRepository<AddressSettings> addressSettingsRepository;
private final PagingStoreFactory pagingStoreFactory;
private final AtomicLong globalSizeBytes = new AtomicLong(0);
private final AtomicLong numberOfMessages = new AtomicLong(0);
private final long maxSize;
private volatile boolean cleanupEnabled = true;
private volatile boolean diskFull = false;
private final ConcurrentMap</*TransactionID*/Long, PageTransactionInfo> transactions = new ConcurrentHashMap<>();
private ActiveMQScheduledComponent scheduledComponent = null;
// Static
// --------------------------------------------------------------------------------------------------------------------------
// Constructors
// --------------------------------------------------------------------------------------------------------------------
public PagingManagerImpl(final PagingStoreFactory pagingSPI,
final HierarchicalRepository<AddressSettings> addressSettingsRepository,
final long maxSize) {
pagingStoreFactory = pagingSPI;
this.addressSettingsRepository = addressSettingsRepository;
addressSettingsRepository.registerListener(this);
this.maxSize = maxSize;
}
public PagingManagerImpl(final PagingStoreFactory pagingSPI,
final HierarchicalRepository<AddressSettings> addressSettingsRepository) {
this(pagingSPI, addressSettingsRepository, -1);
}
@Override
public void addBlockedStore(PagingStore store) {
blockedStored.add(store);
}
@Override
public void onChange() {
reapplySettings();
}
private void reapplySettings() {
for (PagingStore store : stores.values()) {
AddressSettings settings = this.addressSettingsRepository.getMatch(store.getAddress().toString());
store.applySetting(settings);
}
}
@Override
public PagingManagerImpl addSize(int size) {
if (size > 0) {
numberOfMessages.incrementAndGet();
} else {
numberOfMessages.decrementAndGet();
}
long newSize = globalSizeBytes.addAndGet(size);
if (newSize < 0) {
ActiveMQServerLogger.LOGGER.negativeGlobalAddressSize(newSize);
}
if (size < 0) {
checkMemoryRelease();
}
return this;
}
@Override
public long getGlobalSize() {
return globalSizeBytes.get();
}
protected void checkMemoryRelease() {
if (!diskFull && (maxSize < 0 || globalSizeBytes.get() < maxSize) && !blockedStored.isEmpty()) {
Iterator<PagingStore> storeIterator = blockedStored.iterator();
while (storeIterator.hasNext()) {
PagingStore store = storeIterator.next();
if (store.checkReleasedMemory()) {
storeIterator.remove();
}
}
}
}
@Override
public void injectMonitor(FileStoreMonitor monitor) throws Exception {
pagingStoreFactory.injectMonitor(monitor);
monitor.addCallback(new LocalMonitor());
}
class LocalMonitor implements FileStoreMonitor.Callback {
@Override
public void tick(FileStore store, double usage) {
logger.tracef("Tick from store:: %s, usage at %f", store, usage);
}
@Override
public void over(FileStore store, double usage) {
if (!diskFull) {
ActiveMQServerLogger.LOGGER.diskBeyondCapacity();
diskFull = true;
}
}
@Override
public void under(FileStore store, double usage) {
if (diskFull) {
ActiveMQServerLogger.LOGGER.diskCapacityRestored();
diskFull = false;
checkMemoryRelease();
}
}
}
@Override
public boolean isDiskFull() {
return diskFull;
}
@Override
public boolean isUsingGlobalSize() {
return maxSize > 0;
}
@Override
public boolean isGlobalFull() {
return diskFull || maxSize > 0 && globalSizeBytes.get() > maxSize;
}
@Override
public void disableCleanup() {
if (!cleanupEnabled) {
return;
}
lock();
try {
cleanupEnabled = false;
for (PagingStore store : stores.values()) {
store.disableCleanup();
}
} finally {
unlock();
}
}
@Override
public void resumeCleanup() {
if (cleanupEnabled) {
return;
}
lock();
try {
cleanupEnabled = true;
for (PagingStore store : stores.values()) {
store.enableCleanup();
}
} finally {
unlock();
}
}
@Override
public SimpleString[] getStoreNames() {
Set<SimpleString> names = stores.keySet();
return names.toArray(new SimpleString[names.size()]);
}
@Override
public void reloadStores() throws Exception {
lock();
try {
List<PagingStore> reloadedStores = pagingStoreFactory.reloadStores(addressSettingsRepository);
for (PagingStore store : reloadedStores) {
// when reloading, we need to close the previously loaded version of this
// store
PagingStore oldStore = stores.remove(store.getStoreName());
if (oldStore != null) {
oldStore.stop();
oldStore = null;
}
store.start();
stores.put(store.getStoreName(), store);
}
} finally {
unlock();
}
}
@Override
public void deletePageStore(final SimpleString storeName) throws Exception {
syncLock.readLock().lock();
try {
PagingStore store = stores.remove(storeName);
if (store != null) {
store.stop();
}
} finally {
syncLock.readLock().unlock();
}
}
/**
* stores is a ConcurrentHashMap, so we don't need to synchronize this method
*/
@Override
public PagingStore getPageStore(final SimpleString storeName) throws Exception {
PagingStore store = stores.get(storeName);
if (store != null) {
return store;
}
return newStore(storeName);
}
@Override
public void addTransaction(final PageTransactionInfo pageTransaction) {
if (logger.isTraceEnabled()) {
logger.trace("Adding pageTransaction " + pageTransaction.getTransactionID());
}
transactions.put(pageTransaction.getTransactionID(), pageTransaction);
}
@Override
public void removeTransaction(final long id) {
if (logger.isTraceEnabled()) {
logger.trace("Removing pageTransaction " + id);
}
transactions.remove(id);
}
@Override
public PageTransactionInfo getTransaction(final long id) {
if (logger.isTraceEnabled()) {
logger.trace("looking up pageTX = " + id);
}
return transactions.get(id);
}
@Override
public Map<Long, PageTransactionInfo> getTransactions() {
return transactions;
}
@Override
public boolean isStarted() {
return started;
}
@Override
public void start() throws Exception {
lock();
try {
if (started) {
return;
}
pagingStoreFactory.setPagingManager(this);
reloadStores();
if (ARTEMIS_DEBUG_PAGING_INTERVAL > 0) {
this.scheduledComponent = new ActiveMQScheduledComponent(pagingStoreFactory.getScheduledExecutor(), pagingStoreFactory.newExecutor(), ARTEMIS_DEBUG_PAGING_INTERVAL, TimeUnit.SECONDS, false) {
@Override
public void run() {
debug();
}
};
this.scheduledComponent.start();
}
started = true;
} finally {
unlock();
}
}
public void debug() {
logger.info("size = " + globalSizeBytes + " bytes, messages = " + numberOfMessages);
}
@Override
public synchronized void stop() throws Exception {
if (!started) {
return;
}
started = false;
if (scheduledComponent != null) {
this.scheduledComponent.stop();
this.scheduledComponent = null;
}
lock();
try {
for (PagingStore store : stores.values()) {
store.stop();
}
pagingStoreFactory.stop();
} finally {
unlock();
}
}
@Override
public void processReload() throws Exception {
for (PagingStore store : stores.values()) {
store.processReload();
}
}
private PagingStore newStore(final SimpleString address) throws Exception {
syncLock.readLock().lock();
try {
PagingStore store = stores.get(address);
if (store == null) {
store = pagingStoreFactory.newStore(address, addressSettingsRepository.getMatch(address.toString()));
store.start();
if (!cleanupEnabled) {
store.disableCleanup();
}
stores.put(address, store);
}
return store;
} finally {
syncLock.readLock().unlock();
}
}
@Override
public void unlock() {
syncLock.writeLock().unlock();
}
@Override
public void lock() {
syncLock.writeLock().lock();
}
}