/**
* Copyright 2016 Yahoo 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 org.apache.bookkeeper.mledger.impl;
import static org.apache.bookkeeper.mledger.util.SafeRun.safeRun;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.bookkeeper.client.AsyncCallback.ReadCallback;
import org.apache.bookkeeper.client.BKException;
import org.apache.bookkeeper.client.LedgerEntry;
import org.apache.bookkeeper.client.LedgerHandle;
import org.apache.bookkeeper.mledger.AsyncCallbacks;
import org.apache.bookkeeper.mledger.AsyncCallbacks.ReadEntriesCallback;
import org.apache.bookkeeper.mledger.Entry;
import org.apache.bookkeeper.mledger.ManagedLedgerException;
import org.apache.bookkeeper.mledger.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Longs;
public class EntryCacheManager {
private final long maxSize;
private final long evictionTriggerThreshold;
private final double cacheEvictionWatermak;
private final AtomicLong currentSize = new AtomicLong(0);
private final ConcurrentMap<String, EntryCache> caches = Maps.newConcurrentMap();
private final EntryCacheEvictionPolicy evictionPolicy;
private final AtomicBoolean evictionInProgress = new AtomicBoolean(false);
private final ManagedLedgerFactoryImpl mlFactory;
protected final ManagedLedgerFactoryMBeanImpl mlFactoryMBean;
protected static final double MB = 1024 * 1024;
private static final double evictionTriggerThresholdPercent = 0.98;
/**
*
*/
public EntryCacheManager(ManagedLedgerFactoryImpl factory) {
this.maxSize = factory.getConfig().getMaxCacheSize();
this.evictionTriggerThreshold = (long) (maxSize * evictionTriggerThresholdPercent);
this.cacheEvictionWatermak = factory.getConfig().getCacheEvictionWatermark();
this.evictionPolicy = new EntryCacheDefaultEvictionPolicy();
this.mlFactory = factory;
this.mlFactoryMBean = factory.mbean;
log.info("Initialized managed-ledger entry cache of {} Mb", maxSize / MB);
}
public EntryCache getEntryCache(ManagedLedgerImpl ml) {
if (maxSize == 0) {
// Cache is disabled
return new EntryCacheDisabled(ml);
}
EntryCache newEntryCache = new EntryCacheImpl(this, ml);
EntryCache currentEntryCache = caches.putIfAbsent(ml.getName(), newEntryCache);
if (currentEntryCache != null) {
return currentEntryCache;
} else {
return newEntryCache;
}
}
void removeEntryCache(String name) {
EntryCache entryCache = caches.remove(name);
if (entryCache == null) {
return;
}
long size = entryCache.getSize();
entryCache.clear();
if (log.isDebugEnabled()) {
log.debug("Removed cache for {} - Size: {} -- Current Size: {}", name, size / MB, currentSize.get() / MB);
}
}
boolean hasSpaceInCache() {
long currentSize = this.currentSize.get();
// Trigger a single eviction in background. While the eviction is running we stop inserting entries in the cache
if (currentSize > evictionTriggerThreshold && evictionInProgress.compareAndSet(false, true)) {
mlFactory.executor.execute(safeRun(() -> {
// Trigger a new cache eviction cycle to bring the used memory below the cacheEvictionWatermark
// percentage limit
long sizeToEvict = currentSize - (long) (maxSize * cacheEvictionWatermak);
long startTime = System.nanoTime();
log.info("Triggering cache eviction. total size: {} Mb -- Need to discard: {} Mb", currentSize / MB,
sizeToEvict / MB);
try {
evictionPolicy.doEviction(Lists.newArrayList(caches.values()), sizeToEvict);
long endTime = System.nanoTime();
double durationMs = TimeUnit.NANOSECONDS.toMicros(endTime - startTime) / 1000.0;
log.info("Eviction completed. Removed {} Mb in {} ms", (currentSize - this.currentSize.get()) / MB,
durationMs);
} finally {
mlFactoryMBean.recordCacheEviction();
evictionInProgress.set(false);
}
}));
}
return currentSize < maxSize;
}
void entryAdded(long size) {
currentSize.addAndGet(size);
}
void entriesRemoved(long size) {
currentSize.addAndGet(-size);
}
public long getSize() {
return currentSize.get();
}
public long getMaxSize() {
return maxSize;
}
public void clear() {
caches.values().forEach(cache -> cache.clear());
}
protected class EntryCacheDisabled implements EntryCache {
private final ManagedLedgerImpl ml;
public EntryCacheDisabled(ManagedLedgerImpl ml) {
this.ml = ml;
}
@Override
public String getName() {
return ml.getName();
}
@Override
public boolean insert(EntryImpl entry) {
return false;
}
@Override
public void invalidateEntries(PositionImpl lastPosition) {
}
@Override
public void invalidateAllEntries(long ledgerId) {
}
@Override
public void clear() {
}
@Override
public Pair<Integer, Long> evictEntries(long sizeToFree) {
return Pair.create(0, (long) 0);
}
@Override
public void asyncReadEntry(LedgerHandle lh, long firstEntry, long lastEntry, boolean isSlowestReader,
final ReadEntriesCallback callback, Object ctx) {
lh.asyncReadEntries(firstEntry, lastEntry, new ReadCallback() {
public void readComplete(int rc, LedgerHandle lh, Enumeration<LedgerEntry> seq, Object bkctx) {
if (rc != BKException.Code.OK) {
callback.readEntriesFailed(new ManagedLedgerException(BKException.create(rc)), ctx);
return;
}
List<Entry> entries = Lists.newArrayList();
long totalSize = 0;
while (seq.hasMoreElements()) {
// Insert the entries at the end of the list (they will be unsorted for now)
LedgerEntry ledgerEntry = seq.nextElement();
EntryImpl entry = EntryImpl.create(ledgerEntry);
ledgerEntry.getEntryBuffer().release();
entries.add(entry);
totalSize += entry.getLength();
}
mlFactoryMBean.recordCacheMiss(entries.size(), totalSize);
ml.mbean.addReadEntriesSample(entries.size(), totalSize);
callback.readEntriesComplete(entries, null);
}
}, null);
}
@Override
public void asyncReadEntry(LedgerHandle lh, PositionImpl position, AsyncCallbacks.ReadEntryCallback callback,
Object ctx) {
}
@Override
public long getSize() {
return 0;
}
@Override
public int compareTo(EntryCache other) {
return Longs.compare(getSize(), other.getSize());
}
}
private static final Logger log = LoggerFactory.getLogger(EntryCacheManager.class);
}