All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.techempower.cache.CacheGroup Maven / Gradle / Ivy

There is a newer version: 3.3.14
Show newest version
/*******************************************************************************
 * Copyright (c) 2018, TechEmpower, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name TechEmpower, Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

package com.techempower.cache;

import gnu.trove.map.*;
import gnu.trove.map.hash.*;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;

import com.techempower.data.*;
import com.techempower.helper.*;
import com.techempower.util.*;

/**
 * This class is a simple data structure that holds objects of a particular
 * unspecified type, keyed by an long identifier. It can be limited to a
 * particular size, and will trim itself if it exceeds that size.
 * 

* This is a default implementation of a CacheGroup that can be extended by * applications to have custom behaviors. For example, the trim methods may be * extended to use a LRU (Least-Recently Used) replacement algorithm. *

* We avoid thread synchronization on read operations (getting objects). Write * operations (adding, removing objects for example) are synchronized. *

* Read operations are made threadsafe without synchronization through the use * of CopyOnWriteArrayList (from the Java concurrency package) and * ConcurrentLongMap, which is a simple re-working of ConcurrentHashMap also * from the Java concurrency package. *

* Testing so far indicates read performance that is greatly improved over * previous versions that used synchronization for both reads and writes. */ public class CacheGroup extends EntityGroup implements Initializable { // // Member variables. // private ConcurrentMap objects = new ConcurrentHashMap<>(); private List objectsInOrder = new CopyOnWriteArrayList<>(); private volatile boolean initialized = false; private boolean errorOnInitialize = false; private long lowestIdentity = Long.MAX_VALUE; private long highestIdentity = 0; // // Member methods. // /** * Constructor. */ protected CacheGroup(EntityStore controller, Class type, String table, String id, EntityMaker maker, Comparator comparator, String where, String[] whereArguments, boolean readOnly, boolean distribute) { super(controller, type, table, id, maker, comparator, where, whereArguments, readOnly, distribute); } /** * Creates a new {@link Builder}, which is used to construct an * {@link CacheGroup}. Example usage: * *

   * CacheGroup<Foo> = CacheGroup.of(Foo.class) // new Builder
   *     .table("foos") // modified Builder
   *     .id("fooID") // modified Builder
   *     .build(controller); // new CacheGroup
   * 
* *

Note that a {@link EntityStore#register(EntityGroup.Builder)} method * exists, so in the common case where you only want to register the group and * don't care to retain your own reference to it, calling * {@code .build(controller)} is unnecessary. For example: * *

   * register(CacheGroup.of(Foo.class) // new Builder
   *     .table("foos") // modified Builder
   *     .id("fooID") // modified Builder
   * ); // the register method calls .build(controller) for us
   * 
* * @param type The type of the entities. * @return A new {@link Builder}. */ public static Builder of(Class type) { return new Builder<>(type); } /** * Sets the objects list to the contents of the provided Collection. */ public void setObjects(Collection objects) { if (readOnly()) { throw new EntityException("EntityGroup for " + name() + " is read-only. The \"setObjects\" method is not permitted."); } synchronized (this) { if (objects == null) { this.objects = new ConcurrentHashMap<>(); this.objectsInOrder = new CopyOnWriteArrayList<>(); } else { final ConcurrentMap workMap = new ConcurrentHashMap<>( objects.size()); final List workList = new ArrayList<>(objects.size()); for (T object : objects) { workMap.put(object.getId(), object); workList.add(object); } // Replace the member variables. this.objects = workMap; // Avoid maintaining the sorted list if not needed. if (comparator() != EntityGroup.NO_COMPARATOR) { this.objectsInOrder = new CopyOnWriteArrayList<>(workList); } // If we're setting objects from somewhere else, we should assume // that this is initializing the cache group. setInitialized(true); } // Recalculate high and low identities if these high/low values have // been used in the past. calculateHighLowIdentitiesRecalc(); } } /** * Resets this group, removing all the objects, and setting the initialized * flag to false. The group will be rebuilt from the database on next use. * Can lead to temporary stale data in certain edge cases. See * this issue * for details. */ @Override public void reset() { synchronized (this) { setInitialized(false); setErrorOnInitialize(false); resetHighLowIdentities(); } } /** * Synchronously resets and re-initializes this group, removing all the * objects, and setting the initialized flag to false. The group will * is then rebuilt from the database before the synchronization block * ends. To avoids stale data in certain edge cases that {@link #reset()} * can lead to. */ @Override public void resetSynchronous() { synchronized (this) { reset(); initializeIfNecessary(); } } @Override public T get(long id) { initializeIfNecessary(); return this.objects.get(id); } /** * Gets an object from the objects map in a raw manner--that is, calling * the map's get method directly without any pre-initialization. */ protected T getRaw(long id) { return this.objects.get(id); } @Override public List list() { initializeIfNecessary(); // If sorting is disabled, return values(). if (comparator() == EntityGroup.NO_COMPARATOR) { return new ArrayList<>(this.objects.values()); } return new ArrayList<>(this.objectsInOrder); } @Override public TLongObjectMap map() { initializeIfNecessary(); // Grab a reference. final ConcurrentMap map = this.objects; final TLongObjectMap toReturn = new TLongObjectHashMap<>(map.size()); final Iterator values = map.values().iterator(); T object; while (values.hasNext()) { object = values.next(); toReturn.put(object.getId(), object); } return toReturn; } /** * Add an object to the cache and the data store. */ @Override public int put(T object) { if (readOnly()) { throw new EntityException("EntityGroup for " + name() + " is read-only. The \"put\" method is not permitted."); } initializeIfNecessary(); // Get a snapshot of the current persistence state. final boolean persisted = isPersisted(object); // Persist the object. int rowsUpdated = putPersistent(object); if (persisted) { // Object was already persisted, so let's reorder in case the field // we are ordered by was changed. reorder(object.getId()); } else { // Object was not persisted, so let's add it to the cache. addToCache(object); } return rowsUpdated; } /** * Called by CacheGroup.put to persist an entity to the database via a * call to the method of the same name provided by the superclass: * EntityGroup.put. *

* This method is exposed as a protected class to allow subclasses to * specialize the persistence behavior. */ protected int putPersistent(T object) { // Ask the EntityGroup parent to persist the changes. return super.put(object); } /** * Add objects to the cache and the data store. */ @SuppressWarnings("unchecked") @Override public int putAll(Collection objectsToPut) { if (readOnly()) { throw new EntityException("EntityGroup for " + name() + " is read-only. The \"putAll\" method is not permitted."); } initializeIfNecessary(); // Get a snapshot of the current persistence state. final List persisted = new ArrayList<>(objectsToPut.size()); final List nonPersisted = new ArrayList<>(objectsToPut.size()); for (T object : objectsToPut) { if (isPersisted(object)) { persisted.add(object.getId()); } else { nonPersisted.add(object); } } // Persist the changes. int rowsUpdated = putAllPersistent(objectsToPut); // Reorder any previously-persisted objects in case the field we are // ordered by was changed. reorder(CollectionHelper.toLongArray(persisted)); // Add any objects we did not previously know about. addToCache(nonPersisted.toArray((T[])Array.newInstance(type(), nonPersisted.size()))); return rowsUpdated; } /** * Called by CacheGroup.putAll to persist a list of entities to the * database via a call to the method of the same name provided by the * superclass: EntityGroup.putAll. *

* This method is exposed as a protected class to allow subclasses to * specialize the persistence behavior. */ protected int putAllPersistent(Collection objectsToPut) { // Ask the EntityGroup parent to persist the objects. return super.putAll(objectsToPut); } /** * Remove an object from the cache and the data store. */ @Override public void remove(long id) { if (readOnly()) { throw new EntityException("EntityGroup for " + name() + " is read-only. The \"remove\" method is not permitted."); } initializeIfNecessary(); // Remove the persistent instance of the object. removePersistent(id); // Remove it from the cache. removeFromCache(id); } /** * Called by CacheGroup.remove to remove an object from the persistent * store (database). *

* This method is exposed as a protected class to allow subclasses to * specialize the persistence behavior. */ protected void removePersistent(long id) { // Ask the EntityGroup parent to remove the persisted object. super.remove(id); } /** * Add objects to the cache only, not affecting the data store. */ @SafeVarargs public final void addToCache(T... objectsToAdd) { // Grab references. final ConcurrentMap map = this.objects; final Comparator comparator = comparator(); // Avoid maintaining the sorted list if not needed. if (comparator == EntityGroup.NO_COMPARATOR) { for (T object : objectsToAdd) { map.put(object.getId(), object); if (areHighLowIdentitiesInitialized()) { if (object.getId() < this.lowestIdentity) { this.lowestIdentity = object.getId(); } if (object.getId() > this.highestIdentity) { this.highestIdentity = object.getId(); } } } } else { final List orderedList = this.objectsInOrder; synchronized (this) { for (T object : objectsToAdd) { // Only proceed if we don't already have this reference in the cache. if (!map.containsValue(object)) { // If we already have a reference with the same ID, let's remove it. if (map.containsKey(object.getId())) { // Remove the existing reference from the ordered list. orderedList.remove(map.get(object.getId())); } map.put(object.getId(), object); int search = Collections.binarySearch(orderedList, object, comparator); if (search < 0) { orderedList.add(-search - 1, object); } else { orderedList.add(object); } if (areHighLowIdentitiesInitialized()) { if (object.getId() < this.lowestIdentity) { this.lowestIdentity = object.getId(); } if (object.getId() > this.highestIdentity) { this.highestIdentity = object.getId(); } } } } } } } /** * Removes an object from the cache only, not affecting the data store. */ public boolean removeFromCache(T object) { return removeFromCache(object.getId()); } /** * Removes objects from the cache only, not affecting the data store. */ public boolean removeFromCache(long... ids) { // Grab references. final ConcurrentMap map = this.objects; // Skip updating objectsInOrder if not using sorting. if (comparator() == EntityGroup.NO_COMPARATOR) { for (long id : ids) { map.remove(id); } return true; } // Using sorting, so maintain objectsInOrder. final List orderedList = this.objectsInOrder; synchronized (this) { for (long id : ids) { orderedList.remove(map.remove(id)); } // Recalculate high/low identities if needed. calculateHighLowIdentitiesRecalc(); return true; } } /** * Does the cacheGroup contain this particular object? * * @param object The object to search for. */ public boolean contains(T object) { initializeIfNecessary(); return this.objects.containsValue(object); } /** * Does the cacheGroup contain this particular object as specified * by the identifier? * * @param id The ID (identifier) to search for. */ public boolean contains(long id) { initializeIfNecessary(); return this.objects.containsKey(id); } /** * Returns the result of calling the objects map's contains method without * any pre-initialization. */ protected boolean containsRaw(long id) { return this.objects.containsKey(id); } /** * Returns the result of calling the objects map's contains method without * any pre-initialization. */ protected boolean containsRaw(Identifiable object) { return this.objects.containsValue(object); } /** * If true, this indicates that a database failure caused initialization * to fail. */ public boolean isErrorOnInitialize() { return this.errorOnInitialize; } /** * When fetching a cache group's contents from the database, the cache * controller may set this flag to true to indicate that an error was * encountered during initialization. */ public void setErrorOnInitialize(boolean errorOnInitialize) { this.errorOnInitialize = errorOnInitialize; } /** * Calls initialize() if the Cache Group has not already been initialized. * That is, if the initialized flag is false. */ public void initializeIfNecessary() { if (!this.initialized) { synchronized (this) { if (!this.initialized) { initialize(); } } } } /** * Initializes this CacheGroup. */ @Override public void initialize() { synchronized (this) { List allObjects = fetchAllPersistedObjects(); // Avoid maintaining the sorted list if not needed. if (comparator() != EntityGroup.NO_COMPARATOR) { this.objectsInOrder = new CopyOnWriteArrayList<>(allObjects); } copyListToObjectMap(allObjects); // Reset the high and low identities. resetHighLowIdentities(); setInitialized(true); // Execute custom post-initialization processing. customPostInitialization(); } } /** * Called by initialize to fetch a list of persistent entities using the * EntityGroup.list method. */ protected List fetchAllPersistedObjects() { return super.list(); } /** * Executes custom post-initialization processing for the group. Note that * the group will remain blocked (within the initialization "synchronized" * block) until this method returns. Post-initialization processing should * therefore be as quick as possible. */ protected void customPostInitialization() { // Does nothing in this base class. } /** * Copies ordered objects to the LongMap. */ protected void copyListToObjectMap(List l) { final Iterator iter = l.iterator(); // Create a new map, work, to populate. final ConcurrentMap work = new ConcurrentHashMap<>( l.size()); T co; while (iter.hasNext()) { co = iter.next(); work.put(co.getId(), co); } // Replace the member variable. this.objects = work; } /** * Sets the initialized flag. *

* Calls to initialize should set the initialized flag to true. *

* Calls to reset will set the initialized flag to false. */ protected void setInitialized(boolean initialized) { this.initialized = initialized; } /** * Gets the initialized variable. See setInitialized. */ @Override public boolean isInitialized() { return this.initialized; } @Override public long lowest() { calculateHighLowIdentitiesInitial(); return this.lowestIdentity; } @Override public long highest() { calculateHighLowIdentitiesInitial(); return this.highestIdentity; } /** * Resets the highest and lowest identity member variables. */ protected void resetHighLowIdentities() { this.lowestIdentity = Long.MAX_VALUE; this.highestIdentity = 0; } /** * Determines if the highest and lowest identity member variables have * been initialized (whether they have been used). */ protected boolean areHighLowIdentitiesInitialized() { return ( (this.lowestIdentity < Long.MAX_VALUE) || (this.highestIdentity > 0) ); } /** * Determines the highest and lowest identity of objects in the group * and store these values in the member variables. */ protected void calculateHighLowIdentities() { // Use objects instead of objectsInOrder in case NO_COMPARATOR is specified and // objectsInOrder is not maintained. final Iterator iter = this.objects.values().iterator(); Identifiable co; long id; while (iter.hasNext()) { co = (Identifiable)iter.next(); id = co.getId(); if (id < this.lowestIdentity) { this.lowestIdentity = id; } if (id > this.highestIdentity) { this.highestIdentity = id; } } } /** * Calculates the high and low identities if they have not yet been * calculated. This is called by getLowestIdentity and getHighestIdentity * to allow for lazy-initialization of these member variables. */ protected void calculateHighLowIdentitiesInitial() { if (!areHighLowIdentitiesInitialized()) { calculateHighLowIdentities(); } } /** * Recalculates high and low identities only if they have been initialized. * This is used when objects are added to the collection but only when * the high/low values have been used in the past. This avoids the * computation expense if the application never uses these methods. */ protected void calculateHighLowIdentitiesRecalc() { if (areHighLowIdentitiesInitialized()) { calculateHighLowIdentities(); } } /** * Returns the current size of the cacheGroup. */ @Override public int size() { initializeIfNecessary(); return this.objects.size(); } /** * This method ensures that the next time the specified objects are * fetched, they will be at least as current as the time of this method * call. * * @param ids the ids of the objects */ @Override public void refresh(long... ids) { // If the group is not initialized, we're done. if (!this.initialized) { return; } // Grab references. final ConcurrentMap map = this.objects; final List orderedList = this.objectsInOrder; final Comparator comparator = comparator(); synchronized (this) { // Fetch the new objects. final TLongObjectMap objectsMap = super.map(CollectionHelper.toList(ids)); for (long id : ids) { // Remove the object with this id from the cache, if it's there. if (comparator == EntityGroup.NO_COMPARATOR) { map.remove(id); } else { // Only update orderedList if sorting is desired. orderedList.remove(map.remove(id)); } final T object = objectsMap.get(id); // Put the newly loaded object into the cache. if (object != null) { map.put(id, object); // Only update orderedList if sorting is desired. if (comparator != EntityGroup.NO_COMPARATOR) { // Use the comparator to insert it at the appropriate position. int search = Collections.binarySearch(orderedList, object, comparator); if (search < 0) { orderedList.add(-search - 1, object); } else { orderedList.add(object); } } } } } } /** * This method ensures that the next time the ordered objects are fetched, * the specified objects will be in the correct positions. * * @param ids the ids of the objects */ @Override public void reorder(long... ids) { if (comparator() == EntityGroup.NO_COMPARATOR) { // Nothing to do. return; } // Grab references. final ConcurrentMap map = this.objects; final List orderedList = this.objectsInOrder; synchronized (this) { // If the group is not initialized, we're done. if (!this.initialized) { return; } for (long id : ids) { final T object = map.get(id); if (object != null) { // Since writes to the list are expensive, let's see if it's already // in the correct order before modifying anything. final int index = orderedList.indexOf(object); final T previous = (index == 0) ? null : orderedList.get(index - 1); final T next = (index == this.objectsInOrder.size() - 1) ? null : orderedList.get(index + 1); if ((previous == null || comparator().compare(object, previous) >= 0) && (next == null || comparator().compare(object, next) <= 0)) { return; } // Boo, it's out of order. We need to do two write operations now, // which causes the list to copy itself twice. orderedList.remove(object); final int search = Collections.binarySearch(orderedList, object, comparator()); if (search < 0) { orderedList.add(-search - 1, object); } else { orderedList.add(object); } } } } } @Override public String toString() { return "CacheGroup [" + name() + "; ro: " + this.readOnly() + "; distribute: " + this.distribute() + "]"; } @Override public void removeAll(Collection ids) { if (readOnly()) { throw new EntityException("EntityGroup for " + name() + " is read-only. The \"removeAll\" method is not permitted."); } initializeIfNecessary(); // Ask the EntityGroup parent to persist the changes. removeAllPersistent(ids); // Remove it from the cache. this.removeFromCache(CollectionHelper.toLongArray(ids)); } /** * Called by CacheGroup.removeAll to remove a collection of objects from the * persistent store (database). *

* This method is exposed as a protected class to allow subclasses to * specialize the persistence behavior. */ protected void removeAllPersistent(Collection ids) { // Ask the EntityGroup parent to persist the changes. super.removeAll(ids); } @Override public List list(Collection ids) { initializeIfNecessary(); final List theObjects = new ArrayList<>(ids.size()); for (long id : ids) { final T object = this.objects.get(id); if (object != null) { theObjects.add(object); } } return theObjects; } @Override public TLongObjectMap map(Collection ids) { initializeIfNecessary(); final TLongObjectMap theObjects = new TLongObjectHashMap<>(ids.size()); for (long id : ids) { final T object = this.objects.get(id); if (object != null) { theObjects.put(id, object); } } return theObjects; } // // Inner classes. // /** * Creates new instances of {@code CacheGroup}. */ public static class Builder extends EntityGroup.Builder { protected Builder(Class type) { super(type); // CacheGroups default to true, since it is assumed that other instances are // also using CacheGroups for this entity and will therefore need to notify // DistributionListeners. However, if only a single instance uses a CacheGroup, // this could be set to false to reduce noise on the message queue. this.distribute = true; } @Override public CacheGroup build(EntityStore controller) { if (controller == null) { throw new NullPointerException(); } return new CacheGroup<>( controller, this.type, this.table, this.id, this.maker, this.comparator, this.where, this.whereArguments, this.readOnly, this.distribute); } @Override public Builder table(String tableName) { super.table(tableName); return this; } @Override public Builder id(String idFieldName) { super.id(idFieldName); return this; } @Override public Builder readOnly() { super.readOnly(); return this; } @Override public Builder distribute(boolean distribute) { super.distribute(distribute); return this; } @Override public Builder maker(EntityMaker entityMaker) { super.maker(entityMaker); return this; } @Override public Builder comparator(Comparator entityComparator) { super.comparator(entityComparator); return this; } @Override public Builder comparator(String methodName) { super.comparator(methodName); return this; } @Override public Builder where(String whereClause, String... arguments) { super.where(whereClause, arguments); return this; } @Override public Builder constructorArgs(Object... arguments) { super.constructorArgs(arguments); return this; } } // End Builder. } // End CacheGroup.





© 2015 - 2024 Weber Informatics LLC | Privacy Policy