/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-04 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.storage;
import org.apache.log4j.Logger;
import org.exist.management.Agent;
import org.exist.management.AgentFactory;
import org.exist.storage.cache.Cache;
import org.exist.util.DatabaseConfigurationException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
/**
* CacheManager maintains a global memory pool available
* to all page caches. All caches start with a low default
* setting, but CacheManager can grow individual caches
* until the total memory is reached. Caches can also be
* shrinked if their "load" remains below a given threshold
* between check intervals.The check interval is determined
* by the global sync background thread.
*
* The class computes the available memory in terms of
* pages.
*
* @author wolf
*
*/
public class DefaultCacheManager implements CacheManager {
private final static Logger LOG = Logger.getLogger(DefaultCacheManager.class);
/**
* The maximum fraction of the total memory that can
* be used by a single cache.
*/
public final static double MAX_MEM_USE = 0.9;
/**
* The minimum size a cache needs to have to be
* considered for shrinking, defined in terms of a fraction
* of the overall memory.
*/
public final static double MIN_SHRINK_FACTOR = 0.5;
/**
* The amount by which a large cache will be shrinked if
* other caches request a resize.
*/
public final static double SHRINK_FACTOR = 0.7;
/**
* The minimum number of pages that must be read from a
* cache between check intervals to be not considered for
* shrinking. This is a measure for the "load" of the cache. Caches
* with high load will never be shrinked.
*/
public final static int SHRINK_THRESHOLD = 10000;
public static int DEFAULT_CACHE_SIZE = 64;
public static final String CACHE_SIZE_ATTRIBUTE = "cacheSize";
public static final String PROPERTY_CACHE_SIZE = "db-connection.cache-size";
/** Caches maintained by this class */
private List caches = new ArrayList();
private long totalMem;
/**
* The total maximum amount of pages shared between
* all caches.
*/
private int totalPageCount;
/**
* The number of pages currently used by the active caches.
*/
private int currentPageCount = 0;
/**
* The maximum number of pages that can be allocated by a
* single cache.
*/
private int maxCacheSize;
private int pageSize;
/**
* Signals that a resize had been requested by a cache, but
* the request could not be accepted during normal operations.
* The manager might try to shrink the largest cache during the
* next sync event.
*/
private Cache lastRequest = null;
private String instanceName;
public DefaultCacheManager(BrokerPool pool) {
this.instanceName = pool.getId();
int cacheSize;
if ((pageSize = pool.getConfiguration().getInteger(BrokerPool.PROPERTY_PAGE_SIZE)) < 0)
//TODO : should we share the page size with the native broker ?
pageSize = BrokerPool.DEFAULT_PAGE_SIZE;
if ((cacheSize = pool.getConfiguration().getInteger(PROPERTY_CACHE_SIZE)) < 0) {
cacheSize = DEFAULT_CACHE_SIZE;
}
totalMem = cacheSize * 1024 * 1024;
long max = Runtime.getRuntime().maxMemory();
long maxCache = max >= 768 * 1024 * 1024 ? max / 2 : max / 3;
if (totalMem > maxCache) {
totalMem = maxCache;
LOG.warn("The cacheSize=\"" + cacheSize + "\" setting in conf.xml is too large. Java has only " +
(max / 1024) + "k available. Cache manager will not use more than " + (totalMem / 1024) + "k " +
"to avoid memory issues which may lead to database corruptions.");
}
int buffers = (int) (totalMem / pageSize);
this.totalPageCount = buffers;
this.maxCacheSize = (int) (totalPageCount * MAX_MEM_USE);
NumberFormat nf = NumberFormat.getNumberInstance();
LOG.info("Cache settings: " + nf.format(totalMem / 1024) + "k; totalPages: " + nf.format(totalPageCount) +
"; maxCacheSize: " + nf.format(maxCacheSize));
registerMBean();
}
public void registerCache(Cache cache) {
currentPageCount += cache.getBuffers();
caches.add(cache);
cache.setCacheManager(this);
registerMBean(cache);
}
public void deregisterCache(Cache cache) {
Cache next;
for (int i = 0; i < caches.size(); i++) {
next = (Cache) caches.get(i);
if (cache == next) {
caches.remove(i);
break;
}
}
currentPageCount -= cache.getBuffers();
}
public int requestMem(Cache cache) {
if (currentPageCount >= totalPageCount) {
if (cache.getBuffers() < maxCacheSize)
lastRequest = cache;
// no free pages available
// LOG.debug("Cache " + cache.getFileName() + " cannot be resized");
return -1;
}
if (cache.getGrowthFactor() > 1.0
&& cache.getBuffers() < maxCacheSize) {
synchronized (this) {
if (currentPageCount >= totalPageCount)
// another cache has been resized. Give up
return -1;
// calculate new cache size
int newCacheSize = (int)(cache.getBuffers() * cache.getGrowthFactor());
if (newCacheSize > maxCacheSize)
// new cache size is too large: adjust
newCacheSize = maxCacheSize;
if (currentPageCount + newCacheSize > totalPageCount)
// new cache size exceeds total: adjust
newCacheSize = cache.getBuffers() + (totalPageCount - currentPageCount);
if (LOG.isDebugEnabled()) {
NumberFormat nf = NumberFormat.getNumberInstance();
LOG.debug("Growing cache " + cache.getFileName() + " (a " + cache.getClass().getName() +
") from " + nf.format(cache.getBuffers()) +
" to " + nf.format(newCacheSize));
}
currentPageCount -= cache.getBuffers();
// resize the cache
cache.resize(newCacheSize);
currentPageCount += newCacheSize;
// LOG.debug("currentPageCount = " + currentPageCount + "; max = " + totalPageCount);
return newCacheSize;
}
}
return -1;
}
/**
* Called from the global major sync event to check if caches can
* be shrinked. To be shrinked, the size of a cache needs to be
* larger than the factor defined by {@link #MIN_SHRINK_FACTOR}
* and its load needs to be lower than {@link #SHRINK_THRESHOLD}.
*
* If shrinked, the cache will be reset to the default initial cache size.
*/
public void checkCaches() {
int minSize = (int) (totalPageCount * MIN_SHRINK_FACTOR);
Cache cache;
int load;
for (int i = 0; i < caches.size(); i++) {
cache = (Cache) caches.get(i);
if (cache.getGrowthFactor() > 1.0) {
load = cache.getLoad();
if (cache.getBuffers() > minSize && load < SHRINK_THRESHOLD) {
if (LOG.isDebugEnabled()) {
NumberFormat nf = NumberFormat.getNumberInstance();
LOG.debug("Shrinking cache: " + cache.getFileName() + " (a " + cache.getClass().getName() +
") to " + nf.format(cache.getBuffers()));
}
currentPageCount -= cache.getBuffers();
cache.resize(getDefaultInitialSize());
currentPageCount += getDefaultInitialSize();
}
}
}
}
public void checkDistribution() {
if (lastRequest == null)
return;
int minSize = (int) (totalPageCount * MIN_SHRINK_FACTOR);
Cache cache;
for (int i = 0; i < caches.size(); i++) {
cache = (Cache) caches.get(i);
if (cache.getBuffers() >= minSize) {
int newSize = (int) (cache.getBuffers() * SHRINK_FACTOR);
if (LOG.isDebugEnabled()) {
NumberFormat nf = NumberFormat.getNumberInstance();
LOG.debug("Shrinking cache: " + cache.getFileName() + " (a " + cache.getClass().getName() +
") to " + nf.format(newSize));
}
currentPageCount -= cache.getBuffers();
cache.resize(newSize);
currentPageCount += newSize;
break;
}
}
lastRequest = null;
}
public long getMaxTotal() {
return totalPageCount;
}
public long getCurrentSize() {
return currentPageCount;
}
public long getSizeInBytes() {
return currentPageCount * pageSize;
}
public long getMaxSingle() {
return maxCacheSize;
}
public long getTotalMem() {
return totalMem;
}
/**
* Returns the default initial size for all caches.
*
* @return Default initial size 64.
*/
public int getDefaultInitialSize() {
return DEFAULT_CACHE_SIZE;
}
private void registerMBean() {
Agent agent = AgentFactory.getInstance();
try {
agent.addMBean(instanceName, "org.exist.management." + instanceName +
":type=CacheManager", new org.exist.management.CacheManager(this));
} catch (DatabaseConfigurationException e) {
LOG.warn("Exception while registering cache mbean.", e);
}
}
private void registerMBean(Cache cache) {
Agent agent = AgentFactory.getInstance();
try {
agent.addMBean(instanceName, "org.exist.management." + instanceName + ":type=CacheManager.Cache,name=" +
cache.getFileName() + ",cache-type=" + cache.getType(),
new org.exist.management.Cache(cache));
} catch (DatabaseConfigurationException e) {
LOG.warn("Exception while registering cache mbean.", e);
}
}
}