com.ocadotechnology.indexedcache.IndexedImmutableObjectCache Maven / Gradle / Ivy
/*
* Copyright © 2017-2023 Ocado (Ocava)
*
* 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.ocadotechnology.indexedcache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.ParametersAreNonnullByDefault;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.UnmodifiableIterator;
import com.ocadotechnology.id.Identified;
import com.ocadotechnology.id.Identity;
/**
* This implementation is not thread-safe.
*
* Calling any method which modifies the cache while another invocation is modifying the cache will cause a
* ConcurrentModificationException.
*
* Calling any method which queries the cache while another invocation is modifying the cache will cause a
* ConcurrentModificationException
*
* Calling a method which updates the cache while another invocation is querying the cache will not be detected. This
* is a limitation of the implementation, designed to be as performant as possible. It is expected that thorough test
* coverage of the calling code should detect this case as it seems unlikely that a multi-threaded approach could ensure
* that the overlap only ever occurred in one direction
*
* Calling a method which queries the cache while another invocation is querying the cache is deliberately permitted.
*/
@ParametersAreNonnullByDefault
public class IndexedImmutableObjectCache, I> implements StateChangeListenable {
/** There are several implementations of PredicateIndex and OptionalOneToOneIndex.
* There are optional methods to allow callers to hint about the expected workload
* and for the cache to (possibly) take that into account.
*/
public enum Hints {
optimiseForUpdate,
optimiseForQuery,
optimiseForInfrequentChanges
}
private final ObjectStore objectStore;
private final List> indexes = new ArrayList<>();
private final List> stateChangeListeners = new ArrayList<>();
private final List> atomicStateChangeListeners = new ArrayList<>();
private final AtomicReference updatingThread = new AtomicReference<>(null); // The thread name of the thread currently updating the cache, null if no update is ongoing
public static , I> IndexedImmutableObjectCache createHashMapBackedCache() {
return new IndexedImmutableObjectCache<>(new HashMapObjectStore<>(128, 0.99f));
}
public static , I> IndexedImmutableObjectCache createHashMapBackedCache(int initialSize, float fillFactor) {
return new IndexedImmutableObjectCache<>(new HashMapObjectStore<>(initialSize, fillFactor));
}
public IndexedImmutableObjectCache(ObjectStore objectStore) {
this.objectStore = objectStore;
}
/**
* Updates all of the provided values in the cache. Indexes will be updated and any appropriate cache update listeners will be run.
* Accepts addint, updating or deleting objects in the cache.
*
* @param updates An collection of {@link Change} objects containing the value expected to be present in the cache and the value to be added
* @throws CacheUpdateException if any expected values do not match the objects present in the cache
*/
public void updateAll(ImmutableCollection> updates) throws CacheUpdateException {
try {
updateStarting();
objectStore.updateAll(updates);
updateIndexes(updates);
} finally {
updateComplete();
}
}
/**
* Adds all of the provided values to the cache. Indexes will be updated and any appropriate cache update listeners will be run.
*
* @param newObjects the collection of values to be added to the cache
* @throws CacheUpdateException if any objects are already present in the cache with matching ids
*/
public void addAll(ImmutableCollection newObjects) throws CacheUpdateException {
try {
updateStarting();
objectStore.addAll(newObjects);
updateIndexes(newObjects.stream().map(Change::add).collect(ImmutableList.toImmutableList()));
} finally {
updateComplete();
}
}
/**
* Adds the provided value to the cache. Indexes will be updated and any appropriate cache update listeners will be run.
*
* @param newObject the value to be added to the cache
* @throws CacheUpdateException if there is an object stored in the cache with the provided id
*/
public void add(C newObject) throws CacheUpdateException {
try {
updateStarting();
objectStore.add(newObject);
updateIndexes(newObject, null);
} finally {
updateComplete();
}
}
/**
* Updates the provided value in the cache. Indexes will be updated and any appropriate cache update listeners will be run.
* Accepts adding, updating or deleting an object in the cache.
*
* @param original the value expected to be in the cache, or null if the object is new
* @param newObject the value to be added to the cache, or null if the object is to be deleted
* @throws CacheUpdateException if the object stored in the cache with the new id does not match the provided original
* @throws IllegalArgumentException if both original and newObject are null
*/
public void update(@CheckForNull C original, @CheckForNull C newObject) throws CacheUpdateException {
try {
updateStarting();
if (original == newObject) {
return;
}
objectStore.update(original, newObject);
updateIndexes(newObject, original);
} finally {
updateComplete();
}
}
/**
* Removes all of the objects with the provided IDs from the cache.
*
* Removes from the cache all objects matching the identities in {@code ids}.
* Indexes will be updated and any appropriate cache update listeners will be run.
*
* @param ids the collection of ids for values to be removed from the cache
* @return a collection of the old values where any updates occurred, or else an empty collection if no updates were performed
* @throws CacheUpdateException if any of the provided ids do not map to objects in the cache
*/
public ImmutableCollection deleteAll(ImmutableCollection> ids) throws CacheUpdateException {
try {
updateStarting();
ImmutableCollection oldObjects = objectStore.deleteAll(ids);
updateIndexes(oldObjects.stream().map(Change::delete).collect(ImmutableList.toImmutableList()));
return oldObjects;
} finally {
updateComplete();
}
}
/**
* Removes the object with the provided ID from the cache.
*
* Indexes will be updated and any appropriate cache update listeners will be run.
*
* @param id the id for value to be removed from the cache
* @return the old value corresponding to the provided id
* @throws CacheUpdateException if the provided id does not map to one in the cache
*/
public C delete(Identity extends I> id) throws CacheUpdateException {
try {
updateStarting();
C old = objectStore.delete(id);
updateIndexes(null, old);
return old;
} finally {
updateComplete();
}
}
@Override
public > T registerCustomIndex(T index) {
return addIndex(index);
}
@Override
@Deprecated
public void registerStateAddedOrRemovedListener(Consumer super C> consumer) {
registerStateAddedListener(consumer::accept);
registerStateRemovedListener(consumer::accept);
}
@Override
public void registerStateAddedListener(CacheStateAddedListener super C> listener) {
this.stateChangeListeners.add((oldState, updatedState) -> {
if (oldState == null) {
listener.stateAdded(updatedState);
}
});
}
@Override
@SuppressWarnings("unchecked")
public void registerStateChangeListener(CacheStateChangeListener super C> listener) {
// The cast is safe as the objects in the cache should be immutable
this.stateChangeListeners.add((CacheStateChangeListener)listener);
}
/** Applies mapAndFilter
to all updates, then calls listener (if either is non-null).
* Allows listeners to be written that are only notified when an object's type or property changes.
*/
public > void registerStateChangeListener(Function super C, D> mapAndFilter, CacheStateChangeListener super D> listener) {
// The cast is safe as the objects in the cache should be immutable
asFilteringListenable(mapAndFilter).registerStateChangeListener(listener);
}
@Override
@SuppressWarnings("unchecked")
public void registerAtomicStateChangeListener(AtomicStateChangeListener super C> listener) {
// The cast is safe as the objects in the cache should be immutable and the interface uses only immutable collections
this.atomicStateChangeListeners.add((AtomicStateChangeListener)listener);
}
@Override
public void registerStateRemovedListener(CacheStateRemovedListener super C> listener) {
this.stateChangeListeners.add(((oldState, updatedState) -> {
if (updatedState == null) {
listener.stateRemoved(oldState);
}
}));
}
@Override
public void removeStateChangeListener(CacheStateChangeListener super C> listener) {
this.stateChangeListeners.remove(listener);
}
/**
* @param mapAndFilter all arguments will be non-null and present in the cache.
* The return from the function can be null (the null will either be passed to any listeners, or
* the whole update will be ignored (eg, an addition, removal or null-to-null state change).
* @return a listenable that will notify any registered listeners for a subset of the cache's contents (based on mapAndFilter
)
*/
public > StateChangeListenable asFilteringListenable(Function super C, D> mapAndFilter) {
return new FilteringStateChangeListenable<>(this, mapAndFilter);
}
public PredicateIndex addPredicateIndex(Predicate super C> predicate) {
return addPredicateIndex(null, predicate);
}
public PredicateIndex addPredicateIndex(@CheckForNull String name, Predicate super C> predicate) {
return addPredicateIndex(name, predicate, Hints.optimiseForQuery);
}
/**
* An optimiseForUpdate index has zero update overhead and streaming queries are performed by applying the predicate
* function to every object in the parent cache.
*
* An optimiseForQuery index caches a mapping from predicate result to object during update, so that streaming
* queries can directly stream the pre-cached result.
*
* An optimiseForInfrequentChanges index caches a mapping from predicate result to object id during update, so that
* streaming queries can stream the pre-cached result and lookup the actual object in the parent cache. This makes
* querying faster than an optimiseForUpdate index (especially when there are few matches), and updates faster
* than an optimiseForQuery index (when the outcome of the predicate function does not change, the index cache
* requires no changes).
*/
public PredicateIndex addPredicateIndex(Predicate super C> predicate, Hints hint) {
return addPredicateIndex(null, predicate, hint);
}
/**
* An optimiseForUpdate index has zero update overhead and streaming queries are performed by applying the predicate
* function to every object in the parent cache.
*
* An optimiseForQuery index caches a mapping from predicate result to object during update, so that streaming
* queries can directly stream the pre-cached result.
*
* An optimiseForInfrequentChanges index caches a mapping from predicate result to object id during update, so that
* streaming queries can stream the pre-cached result and lookup the actual object in the parent cache. This makes
* querying faster than an optimiseForUpdate index (especially when there are few matches), and updates faster
* than an optimiseForQuery index (when the outcome of the predicate function does not change, the index cache
* requires no changes).
*/
public PredicateIndex addPredicateIndex(@CheckForNull String name, Predicate super C> predicate, Hints hint) {
switch (hint) {
case optimiseForUpdate:
// We could consider a lazy index (build on first query), but our currently use case doesn't justify that
return addIndex(new UncachedPredicateIndex<>(name, this, predicate));
case optimiseForQuery:
return addIndex(new DefaultPredicateIndex<>(name, predicate));
case optimiseForInfrequentChanges:
return addIndex(new IdCachedPredicateIndex<>(name, this, predicate));
default:
throw new UnsupportedOperationException("Missing case:" + hint);
}
}
public PredicateValue addPredicateValue(
Predicate super C> predicate,
Function super C, R> extract,
BiFunction leftAccumulate,
BiFunction rightDecumulate,
T initial) {
return addPredicateValue(null, predicate, extract, leftAccumulate, rightDecumulate, initial);
}
public PredicateValue addPredicateValue(
@CheckForNull String name,
Predicate super C> predicate,
Function super C, R> extract,
BiFunction leftAccumulate,
BiFunction rightDecumulate,
T initial) {
PredicateValue value = new PredicateValue<>(name, predicate, extract, leftAccumulate, rightDecumulate, initial);
return addIndex(value);
}
public PredicateCountValue addPredicateCount(Predicate super C> predicate) {
return addPredicateCount(null, predicate);
}
public PredicateCountValue addPredicateCount(@CheckForNull String name, Predicate super C> predicate) {
PredicateCountValue counter = new PredicateCountValue<>(name, predicate);
return addIndex(counter);
}
public ManyToManyIndex addManyToManyIndex(Function super C, Set> function) {
return addManyToManyIndex(null, function);
}
public ManyToManyIndex addManyToManyIndex(@CheckForNull String name, Function super C, Set> function) {
ManyToManyIndex index = new ManyToManyIndex<>(name, function);
return addIndex(index);
}
/**
* @param function key extraction function
* @param comparator A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public OptionalSortedManyToManyIndex addOptionalSortedManyToManyIndex(
Function super C, Optional>> function,
Comparator super C> comparator) {
return addOptionalSortedManyToManyIndex(null, function, comparator);
}
/**
* @param name optional String parameter - the name of the index.
* @param function key extraction function
* @param comparator A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public OptionalSortedManyToManyIndex addOptionalSortedManyToManyIndex(
@CheckForNull String name,
Function super C, Optional>> function,
Comparator super C> comparator) {
OptionalSortedManyToManyIndex index = new OptionalSortedManyToManyIndex<>(name, function, comparator);
return addIndex(index);
}
public OneToOneIndex addOneToOneIndex(Function super C, R> function) {
return addOneToOneIndex(null, function);
}
public OneToOneIndex addOneToOneIndex(@CheckForNull String name, Function super C, R> function) {
return addOneToOneIndex(name, function, Hints.optimiseForQuery);
}
public OneToOneIndex addOneToOneIndex(Function super C, R> function, Hints hint) {
return addOneToOneIndex(null, function, hint);
}
public OneToOneIndex addOneToOneIndex(@CheckForNull String name, Function super C, R> function, Hints hint) {
return addIndex(new OneToOneIndex<>(name, function, hint));
}
public OneToManyIndex addOneToManyIndex(Function super C, R> function) {
return addOneToManyIndex(null, function);
}
public OneToManyIndex addOneToManyIndex(@CheckForNull String name, Function super C, R> function) {
return addIndex(OneToManyIndex.create(name, function));
}
public ManyToOneIndex addManyToOneIndex(Function super C, Collection> function) {
return addManyToOneIndex(null, function);
}
public ManyToOneIndex addManyToOneIndex(@CheckForNull String name, Function super C, Collection> function) {
ManyToOneIndex index = new ManyToOneIndex<>(name, function);
return addIndex(index);
}
public OptionalOneToManyIndex addOptionalOneToManyIndex(Function super C, Optional> function) {
return addOptionalOneToManyIndex(null, function);
}
public OptionalOneToManyIndex addOptionalOneToManyIndex(@CheckForNull String name, Function super C, Optional> function) {
OptionalOneToManyIndex index = new OptionalOneToManyIndex<>(name, function);
return addIndex(index);
}
public OptionalOneToOneIndex addOptionalOneToOneIndex(Function super C, Optional> function) {
return addOptionalOneToOneIndex(null, function);
}
public OptionalOneToOneIndex addOptionalOneToOneIndex(@CheckForNull String name, Function super C, Optional> function) {
return addOptionalOneToOneIndex(name, function, Hints.optimiseForQuery);
}
/** An Index optimised for update is a little faster. Query times are equal.
* The update-optimised implementation does not separately validate uniqueness of value
* (which is what makes a faster update possible).
*/
public OptionalOneToOneIndex addOptionalOneToOneIndex(Function super C, Optional> function, Hints hint) {
return addOptionalOneToOneIndex(null, function, hint);
}
/** An Index optimised for update is a little faster. Query times are equal.
* The update-optimised implementation does not separately validate uniqueness of value
* (which is what makes a faster update possible).
*/
public OptionalOneToOneIndex addOptionalOneToOneIndex(@CheckForNull String name, Function super C, Optional> function, Hints hint) {
return addIndex(OptionalOneToOneIndexFactory.newOptionalOneToOneIndex(name, function, hint));
}
/**
* @param function key extraction function.
* @param comparator A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public SortedOneToManyIndex addSortedOneToManyIndex(
Function super C, R> function,
Comparator super C> comparator) {
return addSortedOneToManyIndex(null, function, comparator);
}
/**
* @param name optional String parameter - the name of the index.
* @param function key extraction function.
* @param comparator A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public SortedOneToManyIndex addSortedOneToManyIndex(
@CheckForNull String name,
Function super C, R> function,
Comparator super C> comparator) {
SortedOneToManyIndex index = new SortedOneToManyIndex<>(name, function, comparator);
return addIndex(index);
}
/**
* @param function key extraction function.
* @param comparatorGenerator generates a comparator for a given key
* A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public SeparatelySortedOneToManyIndex addSeparatelySortedOneToManyIndex(
Function super C, R> function,
Function> comparatorGenerator) {
return addSeparatelySortedOneToManyIndex(null, function, comparatorGenerator);
}
/**
* @param name optional String parameter - the name of the index.
* @param function key extraction function.
* @param comparatorGenerator generates a comparator for a given key
* A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public SeparatelySortedOneToManyIndex addSeparatelySortedOneToManyIndex(
@CheckForNull String name,
Function super C, R> function,
Function> comparatorGenerator) {
SeparatelySortedOneToManyIndex index = new SeparatelySortedOneToManyIndex<>(name, function, comparatorGenerator);
return addIndex(index);
}
/**
* @param function key extraction function
* @param comparator A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public OptionalSortedOneToManyIndex addOptionalSortedOneToManyIndex(
Function super C, Optional> function,
Comparator super C> comparator) {
return addOptionalSortedOneToManyIndex(null, function, comparator);
}
/**
* @param name optional String parameter - the name of the index.
* @param function key extraction function
* @param comparator A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public OptionalSortedOneToManyIndex addOptionalSortedOneToManyIndex(
@CheckForNull String name,
Function super C, Optional> function,
Comparator super C> comparator) {
OptionalSortedOneToManyIndex index = new OptionalSortedOneToManyIndex<>(name, function, comparator);
return addIndex(index);
}
public CachedGroupBy cacheGroupBy(Function super C, G> groupByExtractor, Collector super C, ?, T> collector) {
return cacheGroupBy(null, groupByExtractor, collector);
}
public CachedGroupBy cacheGroupBy(@CheckForNull String name, Function super C, G> groupByExtractor, Collector super C, ?, T> collector) {
CachedGroupBy cachedGroupByAggregation = new CachedGroupBy<>(name, groupByExtractor, collector);
return addIndex(cachedGroupByAggregation);
}
public MappedPredicateIndex addMappedPredicateIndex(Predicate super C> predicate, Function super C, R> mappingFunction) {
return addMappedPredicateIndex(null, predicate, mappingFunction);
}
public MappedPredicateIndex addMappedPredicateIndex(@CheckForNull String name, Predicate super C> predicate, Function super C, R> mappingFunction) {
MappedPredicateIndex index = new MappedPredicateIndex<>(name, predicate, mappingFunction);
return addIndex(index);
}
/**
* @param comparator - A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public CachedSort addCacheSort(Comparator super C> comparator) {
return addCacheSort(null, comparator);
}
/**
* @param name optional String parameter - the name of the index.
* @param comparator - A comparator on a set of elements C which is consistent with equals().
* More formally, a total-order comparator on a set of elements C where
* compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true.
* This requirement is strictly enforced. Violating it will produce an IllegalStateException
* and leave the cache in an inconsistent state.
*/
public CachedSort addCacheSort(@CheckForNull String name, Comparator super C> comparator) {
CachedSort cacheSort = new CachedSort<>(name, comparator);
return addIndex(cacheSort);
}
public C get(@CheckForNull Identity id) {
return objectStore.get(id);
}
public boolean containsId(@CheckForNull Identity id) {
return objectStore.containsId(id);
}
public int size() {
return objectStore.size();
}
public boolean isEmpty() {
return objectStore.size() == 0;
}
@Override
public Stream stream() {
return objectStore.stream();
}
@Override
public UnmodifiableIterator iterator() {
return objectStore.iterator();
}
@Override
public void forEach(Consumer action) {
objectStore.forEach(action);
}
@SuppressWarnings("unchecked")
private > T addIndex(T index) {
try {
updateStarting();
// The cast is safe as the objects in the cache should be immutable and the interface uses only immutable collections
Index castedIndex = (Index) index;
indexes.add(castedIndex);
//Do not collect to an ImmutableSet - Guava's default collector does not infer the stream size
//which results in a lot of collisions and array extensions.
ImmutableList> allStates = objectStore.stream().map(Change::add).collect(ImmutableList.toImmutableList());
try {
castedIndex.updateAll(allStates);
} catch (IndexUpdateException e) {
throw new IllegalStateException("Failed to add new index", e);
}
return index;
} finally {
updateComplete();
}
}
private void updateIndexes(@CheckForNull C newValue, @CheckForNull C oldValue) {
for (int i = 0; i < indexes.size(); ++i) {
try {
indexes.get(i).update(newValue, oldValue);
} catch (IndexUpdateException e) {
rollbackSingleUpdate(newValue, oldValue, i, e);
throw new CacheUpdateException("Failed to update indices", e);
}
}
updateStateChangeListeners(oldValue, newValue);
updateAtomicStateChangeListeners(oldValue, newValue);
}
private void updateAtomicStateChangeListeners(@CheckForNull C oldValue, @CheckForNull C newValue) {
if (atomicStateChangeListeners.isEmpty()) {
return;
}
ImmutableList> changes = ImmutableList.of(Change.change(oldValue, newValue));
atomicStateChangeListeners.forEach(l -> l.stateChanged(changes));
}
private void updateIndexes(ImmutableCollection> changes) {
for (int i = 0; i < indexes.size(); ++i) {
try {
indexes.get(i).updateAll(changes);
} catch (IndexUpdateException e) {
rollbackBatchUpdate(changes, i, e);
throw new CacheUpdateException("Failed to update indices", e);
}
}
changes.forEach(update -> updateStateChangeListeners(update.originalObject, update.newObject));
if (!atomicStateChangeListeners.isEmpty()) {
atomicStateChangeListeners.forEach(l -> l.stateChanged(changes));
}
}
private void updateStateChangeListeners(@CheckForNull C oldState, @CheckForNull C newState) {
stateChangeListeners.forEach(s -> s.stateChanged(oldState, newState));
}
private void rollbackSingleUpdate(@CheckForNull C newValue, @CheckForNull C oldValue, int failedIndexNumber, IndexUpdateException cause) {
try {
for (int i = 0; i < failedIndexNumber; ++i) {
indexes.get(i).update(oldValue, newValue);
}
objectStore.update(newValue, oldValue);
} catch (IndexUpdateException | CacheUpdateException e) {
throw new IllegalStateException("Failed to rollback changes after index failure: " + cause.getMessage(), e);
}
}
private void rollbackBatchUpdate(ImmutableCollection> changes, int failedIndexNumber, IndexUpdateException cause) {
ImmutableList> reverseChanges = changes.stream()
.map(Change::inverse)
.collect(ImmutableList.toImmutableList());
try {
for (int i = 0; i < failedIndexNumber; ++i) {
indexes.get(i).updateAll(reverseChanges);
}
objectStore.updateAll(reverseChanges);
} catch (IndexUpdateException | CacheUpdateException e) {
throw new IllegalStateException("Failed to rollback changes after index failure: " + cause.getMessage(), e);
}
}
public void clear() {
try {
updateStarting();
ImmutableCollection> clearedObjects = objectStore.stream().map(Change::delete).collect(ImmutableList.toImmutableList());
objectStore.clear();
updateIndexes(clearedObjects);
} finally {
updateComplete();
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("objects", objectStore.size())
.toString();
}
public ImmutableMap, C> snapshotObjects() {
return objectStore.snapshot();
}
private void updateStarting() {
if (!updatingThread.compareAndSet(null, Thread.currentThread().getName())) {
failUpdate();
}
}
/**
* Method separated out to make it easier for the JVM to inline the updateStarting method for performance
*/
private void failUpdate() {
throw new ConcurrentModificationException(
String.format("Attempting to update cache while another update is ongoing. currentThread=[%s] otherThread=[%s]",
Thread.currentThread().getName(),
updatingThread));
}
private void updateComplete() {
updatingThread.compareAndSet(Thread.currentThread().getName(), null);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy