org.nerd4j.utils.cache.AbstractSelfLoadingCache Maven / Gradle / Ivy
/*
* #%L
* Nerd4j Utils
* %%
* Copyright (C) 2011 - 2016 Nerd4j
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
package org.nerd4j.utils.cache;
import org.nerd4j.utils.lang.Is;
import org.nerd4j.utils.lang.Require;
import org.nerd4j.utils.lang.RequirementFailure;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Abstract implementation of {@link SelfLoadingCache}.
*
*
* This class provides most of the common logic and
* some useful extension hooks. Any custom implementation
* of the {@link SelfLoadingCache} interface should
* extend this class.
*
* @param type of the data model to cache.
*
* @author Massimo Coluzzi
* @since 2.0.0
*/
public abstract class AbstractSelfLoadingCache implements SelfLoadingCache
{
/**
* Logging system.
*
* By default the logging level is {@link Level#WARNING} and will only log exceptions.
*
* If you want to log any caching operation you need to set the logging level to {@link Level#INFO}.
*
* If you want to dig into deeper detail about cache loading and updating operations,
* you need to set the logging level to {@link Level#FINE}.
*
* If you want to log everything is happening in the cache, including the data that is going to be
* cached, you need to set the logging level to {@link Level#FINEST}.
*
* If you trust this class to work properly and you do not want your console output to get dirty
* you can turn off this log by setting the logging level to {@link Level#OFF}.
*
* To change the {@link Logger} levels and outputs follow the
* Java documentation.
*/
public static final Logger logger;
/**
* The {@link ExecutorService} to use to insert or update
* cache entries in an asynchronous way.
*/
protected static final ExecutorService executorService;
static
{
logger = Logger.getLogger( AbstractSelfLoadingCache.class.getName() );
logger.setLevel( Level.WARNING );
final AtomicInteger counter = new AtomicInteger(0);
final ThreadFactory threadFactory
= task -> new Thread( task, "SelfLoadingCache-thread-" + counter.getAndIncrement() );
executorService = Executors.newCachedThreadPool( threadFactory );
}
/** Configurations to be used to define the cache behavior. */
protected final CacheConfig config;
/** Underlying caching system provider. */
protected final CacheProvider cacheProvider;
/**
* Tells if this {@link SelfLoadingCache} is disabled.
* If this flag is set to {@code true} this instance
* will skip any caching operation and all data will
* be loaded on each request.
*
* This option should be used to debugging purposes only,
* using this option in production can be very dangerous.
*/
protected boolean disabledThis;
/**
* Constructor with parameters.
*
* @param cacheConfig configurations to be used to define the cache behavior.
* @param cacheProvider underlying caching system provider.
*/
public AbstractSelfLoadingCache( CacheConfig cacheConfig, CacheProvider cacheProvider )
{
super();
this.disabledThis = false;
this.config = Require.nonNull( cacheConfig, "The cache configurations are mandatory" );
this.cacheProvider = Require.nonNull( cacheProvider, "The cache provider is mandatory" );
}
/* ******************* */
/* INTERFACE METHODS */
/* ******************* */
/**
* {@inheritDoc}
*/
@Override
public void disableThis( boolean disable )
{
this.disabledThis = disable;
}
/**
* {@inheritDoc}
*/
@Override
public V get( CacheKey key, DataProvider dataProvider )
{
Require.nonNull( key, "The cache key to search for is mandatory" );
Require.nonNull( dataProvider, "The data provider is mandatory" );
/*
* If this or all SelfLoadingCaches are disabled no caching
* logic will be done and the data will be loaded from the DataProvider.
*/
if( disabledThis || CacheConfig.disabledAll )
return load( key, dataProvider );
/* We try to get the entry from the cache. */
final CacheEntry entry = get( key );
if( entry == null )
{
logger.log( Level.INFO, "Cache MISS: for {0}", key );
/*
* If the entry is null one of this happen:
* 1. the entry is not present in cache;
* 2. there was an error reaching the cache provider.
* In both cases we try to load it from the data provider
* and insert it on cache again.
*/
return insert( key, dataProvider );
}
if( entry.isExpired() )
{
logger.log( Level.INFO, "Cache entry EXPIRED: for {0}", key );
/*
* The update will be executed in different ways from the extending
* classes. This method can return the updated value or the old one
* depending on the implementation.
*/
return update( key, entry, dataProvider );
}
logCacheHit( key, entry );
return entry.getValue();
}
/**
* {@inheritDoc}
*/
@Override
public void evict( CacheKey key )
{
/*
* If this or all SelfLoadingCaches are disabled no caching
* logic will be done.
*/
if( disabledThis || CacheConfig.disabledAll )
return;
try{
logger.log( Level.INFO, "Going to EVICT cache for {0}", key );
cacheProvider.remove( key );
}catch( Exception ex )
{
throwCacheProviderException( "evicting", key, ex );
}
}
/* ******************* */
/* PROTECTED METHODS */
/* ******************* */
/**
* Returns the value from the cache if available
* and {@code null} otherwise.
*
* This method returns {@code null} in both cases:
*
* - If the cache provider returns {@code null}.
* - If the cache provider throws an exception.
*
*
* @param key the key to search for.
* @return the cached entry or {@code null}.
* @throws CacheProviderException if an error occurred during
* cache operations and the flag
* {@link CacheConfig#isThrowCacheProviderExceptions()}
* is {@code true}.
*/
protected CacheEntry get( CacheKey key )
{
try{
logger.log( Level.FINE, "Going to get cache entry for {0}", key );
return cacheProvider.get( key );
}catch( Exception ex )
{
throwCacheProviderException( "getting", key, ex );
}
return null;
}
/**
* Puts the given {@code key-value} pair into the cache.
*
* @param key the key to cache.
* @param value the value to cache.
* @throws CacheProviderException if an error occurred during
* cache operations and the flag
* {@link CacheConfig#isThrowCacheProviderExceptions()}
* is {@code true}.
*/
protected void put( CacheKey key, V value )
{
try{
final long duration = config.getCacheDuration();
if( logger.isLoggable(Level.FINEST) )
logger.finest( "Going to put " + value + " into cache with " + key + " for " + duration + " ms" );
else if( logger.isLoggable(Level.FINE) )
logger.fine( "Going to set cache entry with " + key + " for " + duration + " ms" );
cacheProvider.put( key, value, duration );
}catch( Exception ex )
{
throwCacheProviderException( "putting", key, ex );
}
}
/**
* Loads the value from the {@link DataProvider}.
*
* @param key the key to search for.
* @param dataProvider the {@link DataProvider} to invoke.
* @return the data to cache.
* @throws DataProviderException if there is a failure
* in the data loading.
*/
protected V load( CacheKey key, DataProvider dataProvider )
{
try{
logger.log( Level.FINE, "Going to load data for {0}", key );
return dataProvider.retrieve( key );
}catch( Exception ex )
{
throw getDataProviderException( "loading", key, ex );
}
}
/**
* Returns {@code true} if the given cache key
* has been touched.
*
* This method returns {@code true} even if
* {@link CacheProvider#touch(CacheKey,long)}
* fails in exception because this forces the system
* to load fresh data and update the cache.
*
* @param key the key to search for.
* @return {@code true} if the given cache key has been touched.
* @throws CacheProviderException if an error occurred during
* cache operations and the flag
* {@link CacheConfig#isThrowCacheProviderExceptions()}
* is {@code true}.
*/
private boolean touch( CacheKey key )
{
try {
final long duration = config.getTouchDuration();
if( logger.isLoggable(Level.FINE) )
logger.fine( "Going to touch the cache entry for " + key +
" deferring the expiration by " + duration + " ms" );
return cacheProvider.touch( key, duration );
}catch( Exception ex )
{
throwCacheProviderException( "touching", key, ex );
}
/*
* If exceptions are suppressed we return true
* so the data will be reloaded and the application
* will not crash if an error occurs in the cache.
*/
return true;
}
/**
* Performs the loading of te data related to the given
* key and puts the {@code } pair into the cache.
*
* @param operation the operation to log.
* @param key key to cache.
* @param dataProvider provider of the values to be cached.
* @return the loaded value.
* @throws CacheProviderException if an error occurred during
* cache operations and the flag
* {@link CacheConfig#isThrowCacheProviderExceptions()}
* is {@code true}.
* @throws DataProviderException if there is a failure
* in the data loading.
*/
protected V loadAndPut( String operation, CacheKey key, DataProvider dataProvider )
{
if( logger.isLoggable(Level.FINE) )
logger.fine( "Going to " + operation + " the cache entry for " + key );
final V value = load( key, dataProvider );
put( key, value );
return value;
}
/**
* Inserts the given key and the related value into the cache.
*
* If this cache is configured to perform inserts synchronously
* (the default) then this method will invoke {@link #loadAndPut(String,CacheKey,DataProvider)},
* otherwise the insert will be performed by another thread while
* this method returns {@code null}.
*
* @param key key to cache.
* @param dataProvider provider of the values to be cached.
* @return the inserted value, or {@code null}.
* @throws CacheProviderException if an error occurred during
* cache operations and the flag
* {@link CacheConfig#isThrowCacheProviderExceptions()}
* is {@code true}.
* @throws DataProviderException if there is a failure
* in the data loading.
*/
protected V insert( CacheKey key, DataProvider dataProvider )
{
if( config.isAsyncInsert() )
{
executorService.execute( () -> loadAndPut("insert", key, dataProvider) );
return null;
}
else
return loadAndPut( "insert", key, dataProvider );
}
/**
* Updates the cache for the given key and the related value.
*
* If this cache is configured to perform updates asynchronously
* (the default) then the update will be performed by another thread while
* this method returns the current cached value, otherwise this method will
* invoke {#loadAndPut(String,CacheKey,DataProvider)} and return the fresh
* loaded value.
*
* If multiple threads try to update the same key only one will succeed,
* all the other will return the currently cached value.
*
* @param key key to cache.
* @param entry the expired cache entry.
* @param dataProvider provider of the values to be cached.
* @return the fresh loaded value, or the cached one.
* @throws CacheProviderException if an error occurred during
* cache operations and the flag
* {@link CacheConfig#isThrowCacheProviderExceptions()}
* is {@code true}.
* @throws DataProviderException if there is a failure
* in the data loading.
*/
protected V update( CacheKey key, CacheEntry entry, DataProvider dataProvider )
{
if( ! touch(key) )
{
/*
* If the touch method fails then another thread is updating
* this key, therefore we can return immediately.
*/
logger.log( Level.FINE, "Touch failed, someone else is updating the cache entry for {0}", key );
return entry.getValue();
}
/*
* If we reach this point means that this thread won the race condition,
* the update will be performed immediately or asynchronously depending
* on how this cache has been configured.
*/
if( config.isAsyncUpdate() )
{
executorService.execute( () -> loadAndPut("update", key, dataProvider) );
return entry.getValue();
}
else
return loadAndPut( "update", key, dataProvider );
}
/* ***************** */
/* PRIVATE METHODS */
/* ***************** */
/**
* Throws a new {@link DataProviderException} with the given cause
* and the given message.
*
* @param operation the operation that caused the error.
* @param key the cache key for which the error occurred.
* @param cause the original exception.
* @throws CacheProviderException
*/
private DataProviderException getDataProviderException( String operation, CacheKey key, Exception cause )
{
if( logger.isLoggable(Level.WARNING) )
logger.warning( "An error occurred while " + operation + " data for key " + key + ": " + cause );
if( cause instanceof DataProviderException )
throw (DataProviderException) cause;
else
throw new DataProviderException( "Unhandled exception occurred in data provider", cause );
}
/**
* Throws a new {@link CacheProviderException} with the given cause
* and the given message if the flag
* {@link CacheConfig#isThrowCacheProviderExceptions()} is {@code true}.
*
* @param operation the operation that caused the error.
* @param key the cache key for which the error occurred.
* @param cause the original exception.
* @throws CacheProviderException if required by the configuration.
*/
private void throwCacheProviderException( String operation, CacheKey key, Exception cause )
{
if( logger.isLoggable(Level.WARNING) )
logger.warning( "An error occurred while " + operation + " a cache entry for key " + key + ": " + cause );
if( config.isThrowCacheProviderExceptions() )
{
if( cause instanceof CacheProviderException )
throw (CacheProviderException) cause;
else
throw new CacheProviderException( "Unhandled exception occurred in cache provider", cause );
}
}
/**
* Logs the information related to a cache-hit.
*
* @param key the cache key to log.
* @param entry the cache entry to log.
*/
private void logCacheHit( CacheKey key, CacheEntry> entry )
{
if( logger.isLoggable(Level.FINEST) )
logger.fine( "Cache HIT: for key " + key + " with value " + entry.getValue() + " expiring at " + entry.getExpiration() );
else if( logger.isLoggable(Level.FINE) )
logger.fine( "Cache HIT: for key " + key + " expiring at " + entry.getExpiration() );
else if( logger.isLoggable(Level.INFO) )
logger.info( "Cache HIT: for key " + key );
}
}