/*
* Copyright (C) 2014 Indeed Inc.
*
* Licensed 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 com.indeed.imhotep.service;
import com.google.common.base.Function;
import com.indeed.util.core.Either;
import com.indeed.util.core.io.Closeables2;
import com.indeed.util.core.reference.ReloadableSharedReference;
import com.indeed.flamdex.api.DocIdStream;
import com.indeed.flamdex.api.FlamdexOutOfMemoryException;
import com.indeed.flamdex.api.FlamdexReader;
import com.indeed.flamdex.api.IntTermDocIterator;
import com.indeed.flamdex.api.IntTermIterator;
import com.indeed.flamdex.api.IntValueLookup;
import com.indeed.flamdex.api.StringTermDocIterator;
import com.indeed.flamdex.api.StringTermIterator;
import com.indeed.flamdex.api.StringValueLookup;
import com.indeed.imhotep.ImhotepMemoryCache;
import com.indeed.imhotep.ImhotepStatusDump;
import com.indeed.imhotep.MemoryReservationContext;
import com.indeed.imhotep.MetricKey;
import org.apache.log4j.Logger;
import javax.annotation.Nullable;
import java.io.Closeable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static com.indeed.util.core.Either.Left;
import static com.indeed.util.core.Either.Right;
/**
* @author jsgroth
*/
public class CachedFlamdexReader implements FlamdexReader, MetricCache {
private static final Logger log = Logger.getLogger(CachedFlamdexReader.class);
private final @Nullable MemoryReservationContext memory;
private final @Nullable Closeable readLockRef;
private final FlamdexReader wrapped;
private final MetricCache metricCache;
private final Map<String, Long> intDocFreqCache = new ConcurrentHashMap<String, Long>();
private final Map<String, Long> stringDocFreqCache = new ConcurrentHashMap<String, Long>();
public CachedFlamdexReader(final MemoryReservationContext memory,
final FlamdexReader wrapped,
final @Nullable Closeable readLockRef,
final @Nullable String indexName,
final @Nullable String shardName,
final @Nullable ImhotepMemoryCache<MetricKey, IntValueLookup> freeCache) {
//closer will free these in the opposite order that they are added
this.memory = memory;
this.readLockRef = readLockRef;
this.wrapped = wrapped;
metricCache = new MetricCacheImpl(
new Function<String, Either<FlamdexOutOfMemoryException, IntValueLookup>>() {
@Override
public Either<FlamdexOutOfMemoryException, IntValueLookup> apply(final String metric) {
if (freeCache != null) {
final IntValueLookup intValueLookup = freeCache.tryRemove(new MetricKey(indexName, shardName, metric));
if (intValueLookup != null) {
memory.dehoist(intValueLookup.memoryUsed());
return Right.of(intValueLookup);
}
}
final long memoryUsed = wrapped.memoryRequired(metric);
if (!memory.claimMemory(memoryUsed)) {
return Left.of(new FlamdexOutOfMemoryException());
}
final IntValueLookup lookup;
try {
lookup = wrapped.getMetric(metric);
if (lookup.memoryUsed() != memoryUsed) {
log.error("FlamdexReader.memoryUsed("+metric+"):"+memoryUsed+" does not match lookup.memoryUsed(): "+lookup.memoryUsed());
if (memoryUsed > lookup.memoryUsed()) {
memory.releaseMemory(memoryUsed - lookup.memoryUsed());
} else {
memory.claimMemory(lookup.memoryUsed() - memoryUsed);
}
}
} catch (RuntimeException e) {
memory.releaseMemory(memoryUsed);
throw e;
} catch (FlamdexOutOfMemoryException e) { // not sure why this would be thrown, but just in case...
memory.releaseMemory(memoryUsed);
return Left.of(e);
}
return Right.of(lookup);
}
},
new ReloadableSharedReference.Closer<Map.Entry<String, IntValueLookup>>() {
@Override
public void close(final Map.Entry<String, IntValueLookup> metric) {
if (freeCache == null) {
memory.releaseMemory(metric.getValue());
} else {
freeCache.put(new MetricKey(indexName, shardName, metric.getKey()), metric.getValue());
memory.hoist(metric.getValue().memoryUsed());
}
}
}
);
}
@Override
public Collection<String> getIntFields() {
return wrapped.getIntFields();
}
@Override
public Collection<String> getStringFields() {
return wrapped.getStringFields();
}
@Override
public int getNumDocs() {
return wrapped.getNumDocs();
}
@Override
public String getDirectory() {
return wrapped.getDirectory();
}
@Override
public DocIdStream getDocIdStream() {
return wrapped.getDocIdStream();
}
@Override
public IntTermIterator getIntTermIterator(String field) {
return wrapped.getIntTermIterator(field);
}
@Override
public StringTermIterator getStringTermIterator(String field) {
return wrapped.getStringTermIterator(field);
}
@Override
public IntTermDocIterator getIntTermDocIterator(final String field) {
return wrapped.getIntTermDocIterator(field);
}
@Override
public StringTermDocIterator getStringTermDocIterator(final String field) {
return wrapped.getStringTermDocIterator(field);
}
@Override
public long getIntTotalDocFreq(String field) {
Long docFreq = intDocFreqCache.get(field);
if (docFreq == null) {
docFreq = wrapped.getIntTotalDocFreq(field);
intDocFreqCache.put(field, docFreq);
}
return docFreq;
}
@Override
public long getStringTotalDocFreq(String field) {
Long docFreq = stringDocFreqCache.get(field);
if (docFreq == null) {
docFreq = wrapped.getStringTotalDocFreq(field);
stringDocFreqCache.put(field, docFreq);
}
return docFreq;
}
@Override
public Collection<String> getAvailableMetrics() {
return wrapped.getAvailableMetrics();
}
@Override
public IntValueLookup getMetric(String metric) throws FlamdexOutOfMemoryException {
return metricCache.getMetric(metric);
}
//string lookups are always mmapped so it's not as big of a deal to not cache the references
public StringValueLookup getStringLookup(final String field) throws FlamdexOutOfMemoryException {
return wrapped.getStringLookup(field);
}
@Override
public List<ImhotepStatusDump.MetricDump> getMetricDump() {
return metricCache.getMetricDump();
}
@Override
public Set<String> getLoadedMetrics() {
return metricCache.getLoadedMetrics();
}
@Override
public long memoryRequired(String metric) {
return wrapped.memoryRequired(metric);
}
@Override
public void close() {
try {
if (readLockRef == null) {
Closeables2.closeAll(log, metricCache, wrapped);
} else {
Closeables2.closeAll(log, metricCache, wrapped, readLockRef);
}
} finally {
if (memory == null) {
return;
}
if (memory.usedMemory() > 0) {
log.error("CachedFlamdexReader is leaking! memory reserved after all memory has been freed: "+memory.usedMemory());
}
Closeables2.closeQuietly(memory, log);
}
}
}