/*
* 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.query.lucene;
import org.apache.commons.collections.map.LRUMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements a Document number cache with a fixed size and a LRU strategy.
*/
final class DocNumberCache {
/**
* Logger instance for this class.
*/
private static final Logger log = LoggerFactory.getLogger(DocNumberCache.class);
/**
* Log cache statistics at most every 10 seconds.
*/
private static final long LOG_INTERVAL = 1000 * 10;
/**
* The number of cache segments.
*/
private static final int CACHE_SEGMENTS = 0x10;
/**
* Mask to calculate segment number.
*/
private static final int CACHE_SEGMENTS_MASK = CACHE_SEGMENTS - 1;
/**
* LRU Maps where key=uuid value=reader;docNumber
*/
private final LRUMap[] docNumbers = new LRUMap[CACHE_SEGMENTS];
/**
* Timestamp of the last cache statistics log.
*/
private long lastLog;
/**
* Cache misses.
*/
private long misses;
/**
* Cache accesses;
*/
private long accesses;
/**
* Creates a new <code>DocNumberCache</code> with a limiting
* <code>size</code>.
*
* @param size the cache limit.
*/
DocNumberCache(int size) {
size = size / CACHE_SEGMENTS;
if (size < 0x40) {
// minimum size is 0x40 * 0x10 = 1024
size = 0x40;
}
for (int i = 0; i < docNumbers.length; i++) {
docNumbers[i] = new LRUMap(size);
}
}
/**
* Puts a document number into the cache using a uuid as key. An entry is
* only overwritten if the according reader is younger than the reader
* associated with the existing entry.
*
* @param uuid the key.
* @param reader the index reader from where the document number was read.
* @param n the document number.
*/
void put(String uuid, CachingIndexReader reader, int n) {
LRUMap cacheSegment = docNumbers[getSegmentIndex(uuid.charAt(0))];
synchronized (cacheSegment) {
Entry e = (Entry) cacheSegment.get(uuid);
if (e != null) {
// existing entry
// ignore if reader is older than the one in entry
if (reader.getCreationTick() <= e.creationTick) {
if (log.isDebugEnabled()) {
log.debug("Ignoring put(). New entry is not from a newer reader. "
+ "existing: " + e.creationTick
+ ", new: " + reader.getCreationTick());
}
e = null;
}
} else {
// entry did not exist
e = new Entry(reader.getCreationTick(), n);
}
if (e != null) {
cacheSegment.put(uuid, e);
}
}
}
/**
* Returns the cache entry for <code>uuid</code>, or <code>null</code> if
* no entry exists for <code>uuid</code>.
*
* @param uuid the key.
* @return cache entry or <code>null</code>.
*/
Entry get(String uuid) {
LRUMap cacheSegment = docNumbers[getSegmentIndex(uuid.charAt(0))];
Entry entry;
synchronized (cacheSegment) {
entry = (Entry) cacheSegment.get(uuid);
}
if (log.isInfoEnabled()) {
accesses++;
if (entry == null) {
misses++;
}
// log at most after 1000 accesses and every 10 seconds
if (accesses > 1000 && System.currentTimeMillis() - lastLog > LOG_INTERVAL) {
long ratio = 100;
if (misses != 0) {
ratio -= misses * 100L / accesses;
}
StringBuffer statistics = new StringBuffer();
int inUse = 0;
for (LRUMap docNumber : docNumbers) {
inUse += docNumber.size();
}
statistics.append("size=").append(inUse);
statistics.append("/").append(docNumbers[0].maxSize() * CACHE_SEGMENTS);
statistics.append(", #accesses=").append(accesses);
statistics.append(", #hits=").append((accesses - misses));
statistics.append(", #misses=").append(misses);
statistics.append(", cacheRatio=").append(ratio).append("%");
log.info(statistics.toString());
accesses = 0;
misses = 0;
lastLog = System.currentTimeMillis();
}
}
return entry;
}
/**
* Returns the segment index for character c.
*/
private static int getSegmentIndex(char c) {
if (c > '9') {
c += 9;
}
return c & CACHE_SEGMENTS_MASK;
}
public static final class Entry {
/**
* The creation tick of the IndexReader.
*/
final long creationTick;
/**
* The document number.
*/
final int doc;
Entry(long creationTick, int doc) {
this.creationTick = creationTick;
this.doc = doc;
}
}
}