/* Copyright (c) 2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.di.caching;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.RevCommit;
import org.locationtech.geogig.api.RevFeature;
import org.locationtech.geogig.api.RevFeatureType;
import org.locationtech.geogig.api.RevObject;
import org.locationtech.geogig.api.RevTag;
import org.locationtech.geogig.api.RevTree;
import org.locationtech.geogig.api.plumbing.merge.Conflict;
import org.locationtech.geogig.di.Decorator;
import org.locationtech.geogig.storage.BulkOpListener;
import org.locationtech.geogig.storage.ForwardingObjectDatabase;
import org.locationtech.geogig.storage.ObjectDatabase;
import org.locationtech.geogig.storage.StagingDatabase;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.inject.Provider;
import com.google.inject.util.Providers;
/**
* Method interceptor for {@linnk ObjectDatabase#get(...)} methods that applies caching.
* <p>
* <!-- increases random object lookup on revtrees by 20x, ~40K/s instad of ~2K/s as per
* RevSHA1TreeTest.testPutGet -->
*/
class ObjectDatabaseCacheInterceptor {
private ObjectDatabaseCacheInterceptor() {
// force use of factory methods
}
public static Decorator staging(final Provider<? extends CacheFactory> cacheProvider) {
return new Decorator() {
@Override
public boolean canDecorate(Object subject) {
return subject instanceof StagingDatabase;
}
@SuppressWarnings("unchecked")
@Override
public StagingDatabase decorate(Object subject) {
Provider<StagingDatabase> indexDb = Providers.of((StagingDatabase) subject);
return new CachingStagingDatabase(indexDb, cacheProvider);
}
};
}
public static Decorator objects(final Provider<? extends CacheFactory> cacheProvider) {
return new Decorator() {
@Override
public boolean canDecorate(Object subject) {
return subject instanceof ObjectDatabase && (!(subject instanceof StagingDatabase));
}
@SuppressWarnings("unchecked")
@Override
public ObjectDatabase decorate(Object subject) {
Provider<ObjectDatabase> odb = Providers.of((ObjectDatabase) subject);
CachingObjectDatabase cachingObjectDatabase = new CachingObjectDatabase(odb,
cacheProvider);
return cachingObjectDatabase;
}
};
}
private static class CachingStagingDatabase extends CachingObjectDatabase implements
StagingDatabase {
public CachingStagingDatabase(Provider<StagingDatabase> subject,
Provider<? extends CacheFactory> cacheProvider) {
super(subject, cacheProvider);
}
@Override
public boolean hasConflicts(String namespace) {
return ((StagingDatabase) subject.get()).hasConflicts(namespace);
}
@Override
public Optional<Conflict> getConflict(String namespace, String path) {
return ((StagingDatabase) subject.get()).getConflict(namespace, path);
}
@Override
public List<Conflict> getConflicts(String namespace, String pathFilter) {
return ((StagingDatabase) subject.get()).getConflicts(namespace, pathFilter);
}
@Override
public void addConflict(String namespace, Conflict conflict) {
((StagingDatabase) subject.get()).addConflict(namespace, conflict);
}
@Override
public void removeConflict(String namespace, String path) {
((StagingDatabase) subject.get()).removeConflict(namespace, path);
}
@Override
public void removeConflicts(String namespace) {
((StagingDatabase) subject.get()).removeConflicts(namespace);
}
}
private static class CachingObjectDatabase extends ForwardingObjectDatabase {
private CacheHelper cache;
public CachingObjectDatabase(final Provider<? extends ObjectDatabase> odb,
final Provider<? extends CacheFactory> cacheProvider) {
super(odb);
this.cache = new CacheHelper(cacheProvider);
}
@Override
public @Nullable RevObject getIfPresent(ObjectId id) {
return cache.getIfPresent(id, super.subject.get());
}
@Override
public @Nullable <T extends RevObject> T getIfPresent(ObjectId id, Class<T> type) {
RevObject object = cache.getIfPresent(id, super.subject.get());
return object == null ? null : type.cast(object);
}
@Override
public RevObject get(ObjectId id) throws IllegalArgumentException {
return cache.get(id, super.subject.get());
}
@Override
public <T extends RevObject> T get(ObjectId id, Class<T> type)
throws IllegalArgumentException {
return cache.get(id, type, super.subject.get());
}
@Override
public RevTree getTree(ObjectId id) {
return get(id, RevTree.class);
}
@Override
public RevFeature getFeature(ObjectId id) {
return get(id, RevFeature.class);
}
@Override
public RevFeatureType getFeatureType(ObjectId id) {
return get(id, RevFeatureType.class);
}
@Override
public RevCommit getCommit(ObjectId id) {
return get(id, RevCommit.class);
}
@Override
public RevTag getTag(ObjectId id) {
return get(id, RevTag.class);
}
@Override
public Iterator<RevObject> getAll(final Iterable<ObjectId> ids) {
return getAll(ids, BulkOpListener.NOOP_LISTENER);
}
@Override
public Iterator<RevObject> getAll(final Iterable<ObjectId> ids, BulkOpListener listener) {
return cache.getAll(ids, listener, super.subject.get());
}
@Override
public boolean delete(ObjectId objectId) {
return cache.delete(objectId, super.subject.get());
}
@Override
public long deleteAll(Iterator<ObjectId> ids) {
return deleteAll(ids, BulkOpListener.NOOP_LISTENER);
}
@Override
public long deleteAll(Iterator<ObjectId> ids, BulkOpListener listener) {
return cache.deleteAll(ids, listener, super.subject.get());
}
}
private static class ValueLoader implements Callable<RevObject> {
private ObjectId id;
private ObjectDatabase db;
public ValueLoader(ObjectId id, ObjectDatabase db) {
this.id = id;
this.db = db;
}
@Override
public RevObject call() throws Exception {
RevObject object = db.get(id);
return object;
}
}
private static class CacheHelper {
private Provider<? extends CacheFactory> cacheProvider;
final boolean cacheFeatures = true;// TODO make configurable?
public CacheHelper(final Provider<? extends CacheFactory> cacheProvider) {
this.cacheProvider = cacheProvider;
}
@Nullable
public RevObject getIfPresent(ObjectId id, ObjectDatabase db)
throws IllegalArgumentException {
final Cache<ObjectId, RevObject> cache = cacheProvider.get().get();
RevObject obj = cache.getIfPresent(id);
if (obj == null) {
obj = db.getIfPresent(id);
if (obj != null && isCacheable(obj, cacheFeatures)) {
cache.put(id, obj);
}
}
return obj;
}
public RevObject get(ObjectId id, ObjectDatabase db) throws IllegalArgumentException {
return get(id, RevObject.class, db);
}
public <T extends RevObject> T get(ObjectId id, Class<T> type, ObjectDatabase db)
throws IllegalArgumentException {
final Cache<ObjectId, RevObject> cache = cacheProvider.get().get();
RevObject object;
try {
object = cache.get(id, new ValueLoader(id, db));
} catch (ExecutionException | UncheckedExecutionException e) {
Throwable cause = e.getCause();
Throwables.propagateIfInstanceOf(cause, IllegalArgumentException.class);
Throwables.propagateIfInstanceOf(cause, IllegalStateException.class);
throw new RuntimeException(cause);
}
return type.cast(object);
}
public Iterator<RevObject> getAll(final Iterable<ObjectId> ids,
final BulkOpListener listener, final ObjectDatabase db) {
final int partitionSize = 10_000;
Iterable<List<ObjectId>> partition = Iterables.partition(ids, partitionSize);
final Cache<ObjectId, RevObject> cache = cacheProvider.get().get();
List<Iterator<RevObject>> iterators = new LinkedList<>();
final Set<ObjectId> miss = new HashSet<>();
ImmutableMap<ObjectId, RevObject> present;
for (List<ObjectId> p : partition) {
Set<ObjectId> set = new HashSet<>(p);
present = cache.getAllPresent(set);
if (!present.isEmpty()) {
for (ObjectId id : present.keySet()) {
listener.found(id, null);
set.remove(id);
}
iterators.add(present.values().iterator());
}
miss.addAll(set);
}
if (!miss.isEmpty()) {
Iterator<RevObject> iterator = new AbstractIterator<RevObject>() {
private Iterator<RevObject> delegate = db.getAll(miss, listener);
@Override
protected RevObject computeNext() {
if (delegate.hasNext()) {
RevObject next = delegate.next();
if (isCacheable(next, cacheFeatures)) {
cache.put(next.getId(), next);
}
return next;
}
return endOfData();
}
};
iterators.add(iterator);
}
return Iterators.concat(iterators.iterator());
}
public boolean delete(ObjectId objectId, ObjectDatabase db) {
boolean deleted = db.delete(objectId);
if (deleted) {
final Cache<ObjectId, RevObject> cache = cacheProvider.get().get();
cache.invalidate(objectId);
}
return deleted;
}
public long deleteAll(Iterator<ObjectId> ids, BulkOpListener listener, ObjectDatabase db) {
final BulkOpListener invalidatingListener = new BulkOpListener() {
final Cache<ObjectId, RevObject> cache = cacheProvider.get().get();
@Override
public void deleted(ObjectId id) {
cache.invalidate(id);
}
};
return db.deleteAll(ids, BulkOpListener.composite(listener, invalidatingListener));
}
private final boolean isCacheable(Object object, boolean cacheFeatures) {
if (!cacheFeatures && object instanceof RevFeature) {
return false;
}
// do not cache leaf trees. They tend to be quite large. TODO: make this configurable
if ((object instanceof RevTree) && ((RevTree) object).features().isPresent()) {
return false;
}
return object != null;
}
}
}