/*
* 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.jackrabbit.core.cache;
import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class manages the size of the caches used in Jackrabbit. The combined
* size of all caches must be limited to avoid out of memory problems. The
* available memory is dynamically distributed across the caches each second.
* This class tries to calculates the best cache sizes by comparing the access
* counts of each cache, and the used memory. The idea is, the more a cache is
* accessed, the more memory it should get, while the cache should not shrink
* too quickly. A minimum and maximum size per cache is defined as well. After
* distributing the memory in this way, there might be some unused memory (if
* one or more caches did not use some of the allocated memory). This unused
* memory is distributed evenly across the full caches.
*/
public class CacheManager implements CacheAccessListener {
/** The logger instance. */
private static Logger log = LoggerFactory.getLogger(CacheManager.class);
/** The default maximum amount of memory to distribute across the caches. */
private static final long DEFAULT_MAX_MEMORY = 16 * 1024 * 1024;
/** The default minimum size of a cache. */
private static final long DEFAULT_MIN_MEMORY_PER_CACHE = 128 * 1024;
/** The default maximum memory per cache. */
private static final long DEFAULT_MAX_MEMORY_PER_CACHE = 4 * 1024 * 1024;
/** The set of caches (weakly referenced). */
private WeakHashMap<Cache, Object> caches = new WeakHashMap<Cache, Object>();
/** The default minimum resize interval (in ms). */
private static final int DEFAULT_MIN_RESIZE_INTERVAL = 1000;
/** The default minimum stats logging interval (in ms). */
private static final int DEFAULT_LOG_STATS_INTERVAL = 60 * 1000;
/** The size of a big object, to detect if a cache is full or not. */
private static final int BIG_OBJECT_SIZE = 16 * 1024;
/** The amount of memory to distribute across the caches. */
private long maxMemory = Long.getLong(
"org.apache.jackrabbit.maxCacheMemory",
DEFAULT_MAX_MEMORY);
/** The minimum size of a cache. */
private long minMemoryPerCache = Long.getLong(
"org.apache.jackrabbit.minMemoryPerCache",
DEFAULT_MIN_MEMORY_PER_CACHE);
/** The maximum memory per cache (unless, there is some unused memory). */
private long maxMemoryPerCache = Long.getLong(
"org.apache.jackrabbit.maxMemoryPerCache",
DEFAULT_MAX_MEMORY_PER_CACHE);
/** The minimum resize interval time */
private long minResizeInterval = Long.getLong(
"org.apache.jackrabbit.cacheResizeInterval",
DEFAULT_MIN_RESIZE_INTERVAL);
/** The minimum interval time between stats are logged */
private long minLogStatsInterval = Long.getLong(
"org.apache.jackrabbit.cacheLogStatsInterval",
DEFAULT_LOG_STATS_INTERVAL);
/** The last time the caches where resized. */
private volatile long nextResize =
System.currentTimeMillis() + DEFAULT_MIN_RESIZE_INTERVAL;
/** The last time the cache stats were logged. */
private volatile long nextLogStats =
System.currentTimeMillis() + DEFAULT_LOG_STATS_INTERVAL;
public long getMaxMemory() {
return maxMemory;
}
public void setMaxMemory(final long maxMemory) {
this.maxMemory = maxMemory;
}
public long getMaxMemoryPerCache() {
return maxMemoryPerCache;
}
public void setMaxMemoryPerCache(final long maxMemoryPerCache) {
this.maxMemoryPerCache = maxMemoryPerCache;
}
public long getMinMemoryPerCache() {
return minMemoryPerCache;
}
public void setMinMemoryPerCache(final long minMemoryPerCache) {
this.minMemoryPerCache = minMemoryPerCache;
}
public long getMinResizeInterval() {
return minResizeInterval;
}
public void setMinResizeInterval(long minResizeInterval) {
this.minResizeInterval = minResizeInterval;
}
/**
* After one of the caches is accessed a number of times, this method is called.
* Resize the caches if required.
*/
public void cacheAccessed(long accessCount) {
logCacheStats();
long now = System.currentTimeMillis();
if (now < nextResize) {
return;
}
synchronized (this) {
// the previous test was not synchronized (for speed)
// so we need another synchronized test
if (now < nextResize) {
return;
}
nextResize = now + minResizeInterval;
resizeAll();
nextResize = System.currentTimeMillis() + minResizeInterval;
}
}
/**
* Log info about the caches.
*/
private void logCacheStats() {
if (log.isDebugEnabled()) {
long now = System.currentTimeMillis();
if (now < nextLogStats) {
return;
}
// JCR-3194 avoid ConcurrentModificationException
List<Cache> list = new ArrayList<Cache>();
synchronized (caches) {
list.addAll(caches.keySet());
}
for (Cache cache : list) {
log.debug(cache.getCacheInfoAsString());
}
nextLogStats = now + minLogStatsInterval;
}
}
/**
* Re-calculate the maximum memory for each cache, and set the new limits.
*/
private void resizeAll() {
if (log.isTraceEnabled()) {
log.trace("resizeAll size=" + caches.size());
}
// get strong references
// entries in a weak hash map may disappear any time
// so can't use size() / keySet() directly
// only using the iterator guarantees that we don't get null references
List<Cache> list = new ArrayList<Cache>();
synchronized (caches) {
list.addAll(caches.keySet());
}
if (list.size() == 0) {
// nothing to do
return;
}
CacheInfo[] infos = new CacheInfo[list.size()];
for (int i = 0; i < list.size(); i++) {
infos[i] = new CacheInfo((Cache) list.get(i));
}
// calculate the total access count and memory used
long totalAccessCount = 0;
long totalMemoryUsed = 0;
for (CacheInfo info : infos) {
totalAccessCount += info.getAccessCount();
totalMemoryUsed += info.getMemoryUsed();
}
// try to distribute the memory based on the access count
// and memory used (higher numbers - more memory)
// and find out how many caches are full
// 50% is distributed according to access count,
// and 50% according to memory used
double memoryPerAccess = (double) maxMemory / 2.
/ Math.max(1., (double) totalAccessCount);
double memoryPerUsed = (double) maxMemory / 2.
/ Math.max(1., (double) totalMemoryUsed);
int fullCacheCount = 0;
for (CacheInfo info : infos) {
long mem = (long) (memoryPerAccess * info.getAccessCount());
mem += (long) (memoryPerUsed * info.getMemoryUsed());
mem = Math.min(mem, maxMemoryPerCache);
if (info.wasFull()) {
fullCacheCount++;
} else {
mem = Math.min(mem, info.getMemoryUsed());
}
mem = Math.min(mem, maxMemoryPerCache);
mem = Math.max(mem, minMemoryPerCache);
info.setMemory(mem);
}
// calculate the unused memory
long unusedMemory = maxMemory;
for (CacheInfo info : infos) {
unusedMemory -= info.getMemory();
}
// distribute the remaining memory evenly across the full caches
if (unusedMemory > 0 && fullCacheCount > 0) {
for (CacheInfo info : infos) {
if (info.wasFull()) {
info.setMemory(info.getMemory() + unusedMemory
/ fullCacheCount);
}
}
}
// set the new limit
for (CacheInfo info : infos) {
Cache cache = info.getCache();
if (log.isTraceEnabled()) {
log.trace(cache + " now:" + cache.getMaxMemorySize() + " used:"
+ info.getMemoryUsed() + " access:" + info.getAccessCount()
+ " new:" + info.getMemory());
}
cache.setMaxMemorySize(info.getMemory());
}
}
/**
* Add a new cache to the list.
* This call does not trigger recalculating the cache sizes.
*
* @param cache the cache to add
*/
public void add(Cache cache) {
synchronized (caches) {
caches.put(cache, null);
}
}
/**
* Remove a cache. As this class only has a weak reference to each cache,
* calling this method is not strictly required.
* This call does not trigger recalculating the cache sizes.
*
* @param cache
* the cache to remove
*/
public void remove(Cache cache) {
synchronized (caches) {
caches.remove(cache);
}
}
/**
* Internal copy of the cache information.
*/
public static class CacheInfo {
private Cache cache;
private long accessCount;
private long memory;
private long memoryUsed;
private boolean wasFull;
CacheInfo(Cache cache) {
this.cache = cache;
// copy the data as this runs in a different thread
// the exact values are not important, but it is important that the
// values don't change
this.memory = cache.getMaxMemorySize();
this.memoryUsed = cache.getMemoryUsed();
this.accessCount = cache.getAccessCount();
// reset the access count, so that concurrent cache access is not lost
cache.resetAccessCount();
// if the memory used plus one large object is smaller than the
// allocated memory,
// then the memory was not fully used
wasFull = (memoryUsed + BIG_OBJECT_SIZE) >= memory;
}
boolean wasFull() {
return wasFull;
}
long getAccessCount() {
return accessCount;
}
long getMemoryUsed() {
return memoryUsed;
}
void setMemory(long mem) {
this.memory = mem;
}
long getMemory() {
return memory;
}
Cache getCache() {
return cache;
}
}
public void disposeCache(Cache cache) {
remove(cache);
}
}