Skip to content

Commit

Permalink
Gh-3094: Allow services to use different cache implementations (#3147)
Browse files Browse the repository at this point in the history
* Change CacheServiceLoader to support multiple caches and change Store and Properties classes to use these

* Modify Federated Store to work with new cache approach.
Includes removing redundant inheritance from FederatedStoreCache.

* Fix Federated Store tests.
Mostly changes to use FederatedStoreProperties instead of general properties and remove usage of empty properties

* Fix checking of deprecated property.
Also allow new property in deprecated method

* Remove use of old CacheServiceLoader initialise method from tests

* Remove use of deprecated CacheServiceLoader getService method from tests

* Replace use of deprecated CacheServiceLoader.isEnabled

* Remove direct use of Cache Service from FederatedGraphStorage
Also fix missing generics in Cache.java

* Replace use of deprecated CACHE_SERVICE_CLASS field

* Remove duplicate field identical to existing field and use static import instead

* Deprecated old CacheServiceLoader Test

* Rename old CacheServiceLoader Test

* Add new test for CacheServiceLoader
Change log4j test settings for cache module to show DEBUG messages

* Improve warning for when a subgraph tries to set cache class
The previous approach replaced the class but this wasn't required as re-initialising was already ignored. Now it warns with more details.

* Replace use of old default cache class property in all properties files

* Replace use of deprecated cache service class setter method

* Improve Javadoc for Federated Store Cache classes

* Remove duplicate cache class string fields in Federated Store tests

* Remove unreachable code relating to missing cache class in Federated Store
Improve handling to set the missing cache property instead of behaving as if it was

* Move all cache properties into CacheProperties.java

* Improve test coverage for new changes

* Add test to cover separate cache initialisation in Store

* Fix Store.java typo and improve logic

* Simplify FederatedStorePropertiesTest

* PR Comment - Use only AssertJ in CacheTest.java

* Fix changes

* Fix typo

* PR Comment - Remove 'public' from new unit test class methods

* Add new field to Store containing cache interface objects
These can be used to access these caches without repeatly recreating the Cache objects elsewhere or trying to get the Service directly

* Change FederatedGraphStorage to get caches from Store instead of creating them and add a new test for multiple cache instances
  • Loading branch information
GCHQDeveloper314 authored Mar 12, 2024
1 parent a8134c9 commit c5b4c5a
Show file tree
Hide file tree
Showing 83 changed files with 1,450 additions and 508 deletions.
34 changes: 24 additions & 10 deletions core/cache/src/main/java/uk/gov/gchq/gaffer/cache/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.stream.StreamSupport;

import static java.util.Objects.nonNull;
import static uk.gov.gchq.gaffer.cache.CacheServiceLoader.DEFAULT_SERVICE_NAME;

/**
* Type safe cache, adding and getting is guaranteed to be same type.
Expand All @@ -32,9 +33,22 @@
*/
public class Cache<K, V> {
protected String cacheName;
private final String serviceName;

public Cache(final String cacheName, final String serviceName) {
this.cacheName = cacheName;
// Use the supplied cache service name if it exists, otherwise
// fallback to the default cache service name
if (CacheServiceLoader.isEnabled(serviceName)) {
this.serviceName = serviceName;
} else {
this.serviceName = DEFAULT_SERVICE_NAME;
}
}

public Cache(final String cacheName) {
this.cacheName = cacheName;
this.serviceName = DEFAULT_SERVICE_NAME;
}

/**
Expand All @@ -44,7 +58,7 @@ public Cache(final String cacheName) {
* @throws CacheOperationException if issue getting from cache
*/
public V getFromCache(final String key) throws CacheOperationException {
return CacheServiceLoader.getService().getFromCache(cacheName, key);
return CacheServiceLoader.getService(serviceName).getFromCache(cacheName, key);
}

public String getCacheName() {
Expand All @@ -62,7 +76,7 @@ public String getCacheName() {
* cache
*/
protected void addToCache(final K key, final V value, final boolean overwrite) throws CacheOperationException {
final ICacheService service = CacheServiceLoader.getService();
final ICacheService service = CacheServiceLoader.getService(serviceName);
if (overwrite) {
service.putInCache(getCacheName(), key, value);
} else {
Expand All @@ -73,10 +87,10 @@ protected void addToCache(final K key, final V value, final boolean overwrite) t
public Iterable<K> getAllKeys() {
try {
final Iterable<K> allKeysFromCache;
if (CacheServiceLoader.isEnabled()) {
allKeysFromCache = CacheServiceLoader.getService().getAllKeysFromCache(cacheName);
if (CacheServiceLoader.isEnabled(serviceName)) {
allKeysFromCache = CacheServiceLoader.getService(serviceName).getAllKeysFromCache(cacheName);
} else {
throw new GafferRuntimeException("Cache is not enabled, check it was Initialised");
throw new GafferRuntimeException(String.format("Cache '%s' is not enabled, check it was initialised", serviceName));
}
return (null == allKeysFromCache) ? Collections.emptySet() : allKeysFromCache;
} catch (final Exception e) {
Expand All @@ -90,7 +104,7 @@ public Iterable<K> getAllKeys() {
* @throws CacheOperationException if there was an error trying to clear the cache
*/
public void clearCache() throws CacheOperationException {
CacheServiceLoader.getService().clearCache(cacheName);
CacheServiceLoader.getService(serviceName).clearCache(cacheName);
}

public boolean contains(final String graphId) {
Expand All @@ -104,17 +118,17 @@ public boolean contains(final String graphId) {
* @param key the ID of the key to be deleted
*/
public void deleteFromCache(final String key) {
CacheServiceLoader.getService().removeFromCache(cacheName, key);
CacheServiceLoader.getService(serviceName).removeFromCache(cacheName, key);
}

/**
* Get the cache.
*
* @return ICache
*/
public ICache getCache() {
if (CacheServiceLoader.getService() != null) {
return CacheServiceLoader.getService().getCache(cacheName);
public ICache<K, V> getCache() {
if (CacheServiceLoader.isEnabled(serviceName)) {
return CacheServiceLoader.getService(serviceName).getCache(cacheName);
} else {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2023 Crown Copyright
* Copyright 2016-2024 Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,12 +16,14 @@

package uk.gov.gchq.gaffer.cache;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import uk.gov.gchq.gaffer.cache.util.CacheProperties;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
Expand All @@ -32,44 +34,140 @@
public final class CacheServiceLoader {

private static final Logger LOGGER = LoggerFactory.getLogger(CacheServiceLoader.class);
private static ICacheService service;
public static final String DEFAULT_SERVICE_NAME = "default";
private static final Map<String, ICacheService> SERVICES = new HashMap<>();
private static boolean shutdownHookAdded = false;

/**
* Creates a cache service identified by the given service name, using the supplied class
* name and properties to initialise the service.
* <p> Adds a shutdown hook which gracefully closes the cache service if JVM is stopped.
* This shouldn't be relied upon in a servlet context, instead use ServletLifecycleListener.
*
* @param serviceName name to identify the cache service to initialise
* @param cacheClass class name of the cache service provider to use
* @param properties properties to pass to the cache service provider
* @throws IllegalArgumentException if an invalid cache class is specified
*/
public static void initialise(final String serviceName, final String cacheClass, final Properties properties) {
if (isEnabled(serviceName)) {
LOGGER.debug("Will not initialise as Cache service '{}' was already enabled.", serviceName);
return;
}

if (cacheClass == null) {
throw new IllegalArgumentException("Failed to instantiate cache, cache class was null/missing");
} else if (serviceName == null) {
throw new IllegalArgumentException("Failed to instantiate cache, service name was null/missing");
}

try {
Class<? extends ICacheService> newCacheClass = Class.forName(cacheClass).asSubclass(ICacheService.class);
ICacheService newCacheInstance = newCacheClass.getDeclaredConstructor().newInstance();
SERVICES.put(serviceName, newCacheInstance);
} catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new IllegalArgumentException("Failed to instantiate cache using class " + cacheClass, e);
} catch (final ClassNotFoundException | ClassCastException e) {
throw new IllegalArgumentException(String.format("Failed to instantiate cache, class '%s' is missing or invalid", cacheClass), e);
}

SERVICES.get(serviceName).initialise(properties);

if (!shutdownHookAdded) {
Runtime.getRuntime().addShutdownHook(new Thread(CacheServiceLoader::shutdown));
shutdownHookAdded = true;
}
}

/**
* Creates a default cache service using the supplied class name to initialise the service.
* No properties are passed to the cache service, this is primarily a convenience method.
* See {@link #initialise(String, String, Properties)} for further details.
*
* @param cacheClass class name of the cache service provider to use
* @throws IllegalArgumentException if an invalid cache class is specified
*/
public static void initialise(final String cacheClass) {
initialise(DEFAULT_SERVICE_NAME, cacheClass, null);
}

/**
* Get the default cache service object.
*
* @return the default cache service
*/
public static ICacheService getDefaultService() {
return SERVICES.get(DEFAULT_SERVICE_NAME);
}

/**
* Get the cache service identified by the supplied name.
*
* @param serviceName name identifying the cache service
* @return the cache service
*/
public static ICacheService getService(final String serviceName) {
return SERVICES.get(serviceName);
}

/**
* @return true if the default cache service is enabled
*/
public static boolean isDefaultEnabled() {
return SERVICES.containsKey(DEFAULT_SERVICE_NAME);
}

/**
* @param serviceName name identifying a cache service
* @return true if a cache service with that name exists (and is therefore enabled)
*/
public static boolean isEnabled(final String serviceName) {
return SERVICES.containsKey(serviceName);
}

/**
* Looks at a system property and initialises an appropriate cache service. Adds a shutdown hook
* which gracefully closes the cache service if JVM is stopped. This should not be relied upon
* in a servlet context - use the ServletLifecycleListener located in the REST module instead
* in a servlet context - use the ServletLifecycleListener located in the REST module instead.
* @deprecated Instead use {@link #initialise(String, String, Properties)}, or optionally
* {@link #initialise(String)} when writing tests.
*
* @param properties the cache service properties
* @throws IllegalArgumentException if an invalid cache class is specified in the system property
*/
@Deprecated
public static void initialise(final Properties properties) {
LOGGER.warn("Calling the deprecated initialise method initialises the default cache service, " +
"this will cause problems if you are using service specific cache services");
if (null == properties) {
LOGGER.warn("received null properties - exiting initialise method without creating service");
return;
}
final String cacheClass = properties.getProperty(CacheProperties.CACHE_SERVICE_CLASS);
final String cacheClass = (properties.getProperty(CacheProperties.CACHE_SERVICE_DEFAULT_CLASS) != null) ?
properties.getProperty(CacheProperties.CACHE_SERVICE_DEFAULT_CLASS) :
properties.getProperty(CacheProperties.CACHE_SERVICE_CLASS);

if (null == cacheClass) {
if (null == service) {
LOGGER.debug("No cache service class was specified in properties.");
}
LOGGER.debug("No cache service class was specified in properties.");
return;
}

if (isEnabled()) {
if (isDefaultEnabled()) {
LOGGER.debug("Will not initialise as Cache service was already enabled.");
return;
}

try {
service = Class.forName(cacheClass).asSubclass(ICacheService.class).newInstance();

} catch (final InstantiationException | IllegalAccessException | ClassNotFoundException e) {
Class<? extends ICacheService> newCacheClass = Class.forName(cacheClass).asSubclass(ICacheService.class);
ICacheService newCacheInstance = newCacheClass.getDeclaredConstructor().newInstance();
SERVICES.put(DEFAULT_SERVICE_NAME, newCacheInstance);
} catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new IllegalArgumentException("Failed to instantiate cache using class " + cacheClass, e);
} catch (final ClassNotFoundException | ClassCastException e) {
throw new IllegalArgumentException(String.format("Failed to instantiate cache, class '%s' is invalid", cacheClass), e);
}

service.initialise(properties);
SERVICES.get(DEFAULT_SERVICE_NAME).initialise(properties);

if (!shutdownHookAdded) {
Runtime.getRuntime().addShutdownHook(new Thread(CacheServiceLoader::shutdown));
Expand All @@ -78,31 +176,41 @@ public static void initialise(final Properties properties) {
}

/**
* Get the cache service object.
* Get the default cache service object.
* @deprecated Cache services should instead be
* fetched by name using {@link #getService(String)},
* or by using {@link #getDefaultService()}.
*
* @return the cache service
* @return the default cache service
*/
@SuppressFBWarnings(value = "MS_EXPOSE_REP", justification = "Intended behaviour")
@Deprecated
public static ICacheService getService() {
return service;
LOGGER.warn("Calling the deprecated getService method returns the default cache service, " +
"this will cause problems if you are using service specific cache services");
return getDefaultService();
}

/**
* @return true if the cache is enabled
* @return true if the default cache is enabled
* @deprecated Use {@link #isDefaultEnabled()} instead,
* or {@link #isEnabled(String)} with a service name.
*/
@Deprecated
public static boolean isEnabled() {
return null != service;
LOGGER.warn("Calling the deprecated isEnabled method only returns if the default cache service " +
"is enabled, this will cause problems if you are using service specific cache services");
return isDefaultEnabled();
}

/**
* Gracefully shutdown and reset the cache service.
*/
public static void shutdown() {
if (null != service) {
for (final ICacheService service: SERVICES.values()) {
service.shutdown();
}

service = null;
SERVICES.clear();
}

private CacheServiceLoader() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 Crown Copyright
* Copyright 2016-2024 Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,10 +26,42 @@ private CacheProperties() {
}

/**
* Name of the system property to use in order to define the cache service class.
* Name of a system property previously used for defining the default cache service class.
* @deprecated Use {@link #CACHE_SERVICE_DEFAULT_CLASS} instead.
*/
@Deprecated
public static final String CACHE_SERVICE_CLASS = "gaffer.cache.service.class";

/**
* Name of the system property to use for defining the default cache service class.
*/
public static final String CACHE_SERVICE_DEFAULT_CLASS = "gaffer.cache.service.default.class";

/**
* Name of the system property to use for defining a cache service class dedicated to the Job Tracker.
*/
public static final String CACHE_SERVICE_JOB_TRACKER_CLASS = "gaffer.cache.service.jobtracker.class";

/**
* Name of the system property to use for defining a cache service class dedicated to Named Views.
*/
public static final String CACHE_SERVICE_NAMED_VIEW_CLASS = "gaffer.cache.service.namedview.class";

/**
* Name of the system property to use for defining a cache service class dedicated to Named Operations.
*/
public static final String CACHE_SERVICE_NAMED_OPERATION_CLASS = "gaffer.cache.service.namedoperation.class";

/**
* Names of the system properties used to set the suffix for all caches or per cache.
* CASE INSENSITIVE
* e.g. gaffer.cache.service.default.suffix="v2"
*/
public static final String CACHE_SERVICE_DEFAULT_SUFFIX = "gaffer.cache.service.default.suffix";
public static final String CACHE_SERVICE_NAMED_OPERATION_SUFFIX = "gaffer.cache.service.named.operation.suffix";
public static final String CACHE_SERVICE_JOB_TRACKER_SUFFIX = "gaffer.cache.service.job.tracker.suffix";
public static final String CACHE_SERVICE_NAMED_VIEW_SUFFIX = "gaffer.cache.service.named.view.suffix";

/**
* Name of the system property to use in order to locate the cache config file.
*/
Expand Down
Loading

0 comments on commit c5b4c5a

Please sign in to comment.