From 796ac59fcef766258b9819530270e86628b30ad8 Mon Sep 17 00:00:00 2001 From: Roi Menashe <33356310+roimenashe@users.noreply.github.com> Date: Wed, 24 Mar 2021 11:25:31 +0200 Subject: [PATCH] Feature/aerospike cache improvements (#151) * Spring Cache Aerospike improvements: 1. TTL configuration support in AerospikeCacheManager. 2. Added integration tests. 3. Code cleanup & Apache license copyrights. * add missing implementations * Replace AerospikeClient with IAerospikeClient. * Support clear() method. * Added a test for "cache should not evict" scenario. * Move AerospikeSerializingCache functionality to AerospikeCache class. * Remove unnecessary casting. * Reformat imports. * Change access levels. * Refactor: 1. Remove usage of deprecated method "addCache" in getCache. 2. Use external Aerospike cache utils instead of duplicate code. 3. Code cleanup. * add license header; add private constructor * Added documentation for AerospikeCache. * 1. Fix: GetAerospikeCache creates the cache's namespace with the given name. 2. Return Cache instead of AerospikeCache in getMissingCache. * Added tests to cover Aerospike cache clear() scenarios. * Refactor: 1. Using AerospikeCacheConfiguration to keep all cache level configuration. 2. New AerospikeCache name that matches an AerospikeCacheConfiguration (the name no longer represents an Aerospike namespace, namespace is now part of the cache configuration). 3. Added Builder pattern to the cache manager and cache configuration to reduce the number of constructors. 4. Modify Aerospike cache tests to the new design. 5. Throwable instead of Exception. * Wrap get() method with Cache.ValueRetrievalException instead of log.warn message. * In case the key already exists return the existing value. Else, write the new given key-value and return null. * Added documentation for AerospikeCache methods. * Fix: adding another second to thread.sleep when clearing cache (build via git actions sometimes fails cause clear() was not completed). * Fix: remove clear cache test since clearing cache is async method and nothing guarantees that it will finish before the thread sleep duration finishes which causes git actions build to fail. * Fix: call AerospikeOperations count() method with set name only (no need to provide the class). * Avoid passing nulls in constructors. * 1. Remove Builder pattern from AerospikeCacheManager, add the missing relevant constructors. 2. Avoid passing nulls to constructors. 3. Code cleanup - modify docs and variable names. * Refactor: change configuration structure (providing a namespace is mandatory). default set is null (write directly to the namespace). default expiration is 0 (Aerospike Server's default). * 1. Changing "cache with TTL" test to use a configured cache instead of another cache manager. 2. Adding another test to cover two cache managers scenario. * Change set and expirationInSeconds to final. Co-authored-by: yrizhkov Co-authored-by: Eugene R --- .../data/aerospike/cache/AerospikeCache.java | 180 +++++++++++++----- .../cache/AerospikeCacheConfiguration.java | 28 +++ .../cache/AerospikeCacheManager.java | 180 +++++------------- .../data/aerospike/BaseIntegrationTests.java | 2 + ...AerospikeCacheManagerIntegrationTests.java | 155 ++++++++++++++- .../cache/AerospikeCacheMangerTests.java | 40 ++-- .../aerospike/config/CommonTestConfig.java | 37 +++- 7 files changed, 413 insertions(+), 209 deletions(-) create mode 100644 src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheConfiguration.java diff --git a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCache.java b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCache.java index 3093e82dc..3fa78ff50 100644 --- a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCache.java +++ b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCache.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2019 the original author or authors. * * 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 * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://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, @@ -13,102 +13,180 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.aerospike.cache; - -import java.util.concurrent.Callable; -import org.springframework.cache.Cache; -import org.springframework.cache.support.SimpleValueWrapper; +package org.springframework.data.aerospike.cache; -import com.aerospike.client.AerospikeClient; -import com.aerospike.client.Bin; +import com.aerospike.client.IAerospikeClient; import com.aerospike.client.Key; -import com.aerospike.client.Operation; import com.aerospike.client.Record; import com.aerospike.client.policy.RecordExistsAction; import com.aerospike.client.policy.WritePolicy; +import org.springframework.cache.Cache; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.data.aerospike.convert.AerospikeConverter; +import org.springframework.data.aerospike.convert.AerospikeReadData; +import org.springframework.data.aerospike.convert.AerospikeWriteData; + +import java.util.Objects; +import java.util.concurrent.Callable; /** - * + * A Cache {@link org.springframework.cache.Cache} implementation backed by Aerospike database as store. + * Create and configure Aerospike cache instances via {@link AerospikeCacheManager}. + * * @author Venil Noronha */ -//TODO: extract this to the separate repository aerospike-spring-cache public class AerospikeCache implements Cache { private static final String VALUE = "value"; - protected AerospikeClient client; - protected String namespace; - protected String set; - protected WritePolicy createOnly; - - public AerospikeCache(String namespace, String set, AerospikeClient client, - long expiration){ + private final String name; + private final IAerospikeClient client; + private final AerospikeConverter aerospikeConverter; + private final AerospikeCacheConfiguration cacheConfiguration; + private final WritePolicy createOnly; + private final WritePolicy writePolicyForPut; + + public AerospikeCache(String name, + IAerospikeClient client, + AerospikeConverter aerospikeConverter, + AerospikeCacheConfiguration cacheConfiguration) { + this.name = name; this.client = client; - this.namespace = namespace; - this.set = set; - this.createOnly = new WritePolicy(client.writePolicyDefault); + this.aerospikeConverter = aerospikeConverter; + this.cacheConfiguration = cacheConfiguration; + this.createOnly = new WritePolicy(client.getWritePolicyDefault()); this.createOnly.recordExistsAction = RecordExistsAction.CREATE_ONLY; + this.createOnly.expiration = cacheConfiguration.getExpirationInSeconds(); + this.writePolicyForPut = new WritePolicy(client.getWritePolicyDefault()); + this.writePolicyForPut.expiration = cacheConfiguration.getExpirationInSeconds(); } - protected Key getKey(Object key){ - return new Key(namespace, set, key.toString()); - } - - private ValueWrapper toWrapper(Record record) { - return (record != null ? new SimpleValueWrapper(record.getValue(VALUE)) : null); - } - + /** + * Clears the cache by truncating the configured cache's set (in the configured namespace). + */ @Override public void clear() { - // TODO Auto-generated method stub + client.truncate(null, cacheConfiguration.getNamespace(), cacheConfiguration.getSet(), null); } + /** + * Deletes the key from Aerospike database. + * @param key The key to delete. + */ @Override public void evict(Object key) { - this.client.delete(null, getKey(key)); - + client.delete(null, getKey(key)); } + /** + * Get cache's name. + * @return The cache's name. + */ @Override - public ValueWrapper get(Object key) { - Record record = client.get(null, getKey(key)); - ValueWrapper vr = toWrapper(record); - return vr; + public String getName() { + return name; } - @SuppressWarnings("unchecked") + /** + * Get the underlying native cache provider - the Aerospike client. + * @return The aerospike client. + */ @Override - public T get(Object key, Class type) { - return (T) client.get(null, getKey(key)); + public Object getNativeCache() { + return client; } + /** + * Return the value (bins) from the Aerospike database to which this cache maps the specified key, obtaining that value from valueLoader if necessary. + * This method provides a simple substitute for the conventional "if cached, return; otherwise create, cache and return" pattern. + * @param key The key whose associated value is to be returned. + * @param valueLoader The value loader that might contain the value (bins). + * @return The value (bins) to which this cache maps the specified key. + */ @Override - public String getName() { - return this.namespace+":"+this.set; + @SuppressWarnings("unchecked") + public T get(Object key, Callable valueLoader) { + T value = (T) client.get(null, getKey(key)).getValue(VALUE); + if (Objects.isNull(value)) { + try { + value = valueLoader.call(); + if (Objects.nonNull(value)) { + put(key, value); + } + } catch (Throwable e) { + throw new Cache.ValueRetrievalException(key, valueLoader, e); + } + } + return value; } + /** + * Return the value (bins) from the Aerospike database to which this cache maps the specified key. + * Generically specifying a type that return value will be cast to. + * @param key The key whose associated value (bins) is to be returned. + * @param type The required type of the returned value (may be null to bypass a type check; in case of a null value found in the cache, the specified type is irrelevant). + * @return The value (bins) to which this cache maps the specified key (which may be null itself), or also null if the cache contains no mapping for this key. + */ @Override - public Object getNativeCache() { - return client; + public T get(Object key, Class type) { + Key dbKey = getKey(key); + Record record = client.get(null, dbKey); + if (record != null) { + AerospikeReadData data = AerospikeReadData.forRead(dbKey, record); + return aerospikeConverter.read(type, data); + } + return null; } + /** + * Returns the value (bins) from the Aerospike database to which this cache maps the specified key. + * Returns null if the cache contains no mapping for this key; otherwise, the cached value (which may be null itself) will be returned in a Cache.ValueWrapper. + * @param key The key whose associated value (bins) is to be returned. + * @return The value (bins) to which this cache maps the specified key, contained within a Cache.ValueWrapper which may also hold a cached null value. + * A straight null being returned means that the cache contains no mapping for this key. + */ @Override - public void put(Object key, Object value) { - client.put(null, getKey(key), new Bin(VALUE, value)); + public ValueWrapper get(Object key) { + Object value = get(key, Object.class); + return (value != null ? new SimpleValueWrapper(value) : null); } + /** + * Write the key-value pair to Aerospike database. + * @param key The key to write. + * @param value The value to write. + */ @Override - public ValueWrapper putIfAbsent(Object key, Object value) { - Record record = client.operate(this.createOnly, getKey(key), Operation.put(new Bin(VALUE, value)), Operation.get(VALUE)); - return toWrapper(record); + public void put(Object key, Object value) { + serializeAndPut(writePolicyForPut, key, value); } + /** + * Write the key-value pair to Aerospike database if the key doesn't already exists. + * @param key The key to write. + * @param value The value (bins) to write. + * @return In case the key already exists return the existing value, else return null. + */ @Override - public T get(Object key, Callable valueLoader) { - // TODO Auto-generated method stub + public ValueWrapper putIfAbsent(Object key, Object value) { + ValueWrapper valueWrapper = get(key); + // Key already exists, return the existing value + if (valueWrapper != null) { + return valueWrapper; + } + // Key doesn't exists, write the new given key-value to Aerospike database and return null + serializeAndPut(createOnly, key, value); return null; } + private Key getKey(Object key){ + return new Key(cacheConfiguration.getNamespace(), cacheConfiguration.getSet(), key.toString()); + } + private void serializeAndPut(WritePolicy writePolicy, Object key, Object value) { + AerospikeWriteData data = AerospikeWriteData.forWrite(); + aerospikeConverter.write(value, data); + client.put(writePolicy, getKey(key), data.getBinsAsArray()); + } } diff --git a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheConfiguration.java b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheConfiguration.java new file mode 100644 index 000000000..74276b75e --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheConfiguration.java @@ -0,0 +1,28 @@ +package org.springframework.data.aerospike.cache; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Default set is null meaning write directly to the namespace. + * Default expiration is 0 meaning use the server's default namespace configuration variable "default-ttl". + */ +@Getter +@AllArgsConstructor +public class AerospikeCacheConfiguration { + private final String namespace; + private final String set; + private final int expirationInSeconds; + + public AerospikeCacheConfiguration (String namespace) { + this(namespace, null, 0); + } + + public AerospikeCacheConfiguration (String namespace, String set) { + this(namespace, set, 0); + } + + public AerospikeCacheConfiguration (String namespace, int expirationInSeconds) { + this(namespace, null, expirationInSeconds); + } +} diff --git a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheManager.java b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheManager.java index 3121854a8..b82123011 100644 --- a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheManager.java +++ b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheManager.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2019 the original author or authors. * * 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 * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://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, @@ -16,19 +16,12 @@ package org.springframework.data.aerospike.cache; -import com.aerospike.client.AerospikeClient; -import com.aerospike.client.Key; -import com.aerospike.client.Record; -import com.aerospike.client.policy.WritePolicy; +import com.aerospike.client.IAerospikeClient; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.cache.support.SimpleValueWrapper; import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; import org.springframework.cache.transaction.TransactionAwareCacheDecorator; import org.springframework.data.aerospike.convert.AerospikeConverter; -import org.springframework.data.aerospike.convert.AerospikeReadData; -import org.springframework.data.aerospike.convert.AerospikeWriteData; -import org.springframework.data.aerospike.convert.MappingAerospikeConverter; import org.springframework.util.Assert; import java.util.*; @@ -41,111 +34,65 @@ * Setting {@link #setTransactionAware(boolean)} to true will force Caches to * be decorated as {@link TransactionAwareCacheDecorator} so values will only be written * to the cache after successful commit of surrounding transaction. - * + * * @author Venil Noronha */ -//TODO: extract this to the separate repository aerospike-spring-cache public class AerospikeCacheManager extends AbstractTransactionSupportingCacheManager { - protected static final String DEFAULT_SET_NAME = "aerospike"; - - private final AerospikeClient aerospikeClient; + private final IAerospikeClient aerospikeClient; private final AerospikeConverter aerospikeConverter; - private final String setName; - private final Set configuredCacheNames; - - /** - * Create a new {@link AerospikeCacheManager} instance with no caches and with the - * set name "aerospike". - * - * @param aerospikeClient the {@link AerospikeClient} instance. - * @param aerospikeConverter - */ - public AerospikeCacheManager(AerospikeClient aerospikeClient, MappingAerospikeConverter aerospikeConverter) { - this(aerospikeClient, Collections.emptyList(), aerospikeConverter); - } - - /** - * Create a new {@link AerospikeCacheManager} instance with no caches and with the - * specified set name. - * - * @param aerospikeClient the {@link AerospikeClient} instance. - * @param setName the set name. - * @param aerospikeConverter - */ - public AerospikeCacheManager(AerospikeClient aerospikeClient, String setName, MappingAerospikeConverter aerospikeConverter) { - this(aerospikeClient, Collections.emptyList(), setName, aerospikeConverter); - } + private final AerospikeCacheConfiguration defaultCacheConfiguration; + private final Map initialPerCacheConfiguration; /** - * Create a new {@link AerospikeCacheManager} instance with the specified caches and - * with the set name "aerospike". - * - * @param aerospikeClient the {@link AerospikeClient} instance. - * @param cacheNames the default caches to create. - * @param aerospikeConverter + * Create a new {@link AerospikeCacheManager} instance - + * Specifying a default cache configuration. + * + * @param aerospikeClient the instance that implements {@link IAerospikeClient}. + * @param aerospikeConverter the instance that implements {@link AerospikeConverter}. + * @param defaultCacheConfiguration the default cache configuration. */ - public AerospikeCacheManager(AerospikeClient aerospikeClient, - Collection cacheNames, MappingAerospikeConverter aerospikeConverter) { - this(aerospikeClient, cacheNames, DEFAULT_SET_NAME, aerospikeConverter); + public AerospikeCacheManager(IAerospikeClient aerospikeClient, + AerospikeConverter aerospikeConverter, + AerospikeCacheConfiguration defaultCacheConfiguration) { + this(aerospikeClient, aerospikeConverter, defaultCacheConfiguration, new LinkedHashMap<>()); } /** - * Create a new {@link AerospikeCacheManager} instance with the specified caches and - * with the specified set name. - * - * @param aerospikeClient the {@link AerospikeClient} instance. - * @param cacheNames the default caches to create. - * @param setName the set name. - * @param aerospikeConverter + * Create a new {@link AerospikeCacheManager} instance - + * Specifying a default cache configuration and a map of caches (cache names) and matching configurations. + * + * @param aerospikeClient the instance that implements {@link IAerospikeClient}. + * @param aerospikeConverter the instance that implements {@link AerospikeConverter}. + * @param defaultCacheConfiguration the default aerospike cache configuration. + * @param initialPerCacheConfiguration a map of caches (cache names) and matching configurations. */ - public AerospikeCacheManager(AerospikeClient aerospikeClient, - Collection cacheNames, String setName, MappingAerospikeConverter aerospikeConverter) { - Assert.notNull(aerospikeClient, "AerospikeClient must not be null"); - Assert.notNull(cacheNames, "Cache names must not be null"); - Assert.notNull(setName, "Set name must not be null"); + public AerospikeCacheManager(IAerospikeClient aerospikeClient, + AerospikeConverter aerospikeConverter, + AerospikeCacheConfiguration defaultCacheConfiguration, + Map initialPerCacheConfiguration) { + Assert.notNull(aerospikeClient, "The aerospike client must not be null"); + Assert.notNull(aerospikeConverter, "The aerospike converter must not be null"); + Assert.notNull(defaultCacheConfiguration, "The default cache configuration must not be null"); + Assert.notNull(initialPerCacheConfiguration, "The initial per cache configuration must not be null"); this.aerospikeClient = aerospikeClient; this.aerospikeConverter = aerospikeConverter; - this.setName = setName; - this.configuredCacheNames = new LinkedHashSet(cacheNames); + this.defaultCacheConfiguration = defaultCacheConfiguration; + this.initialPerCacheConfiguration = initialPerCacheConfiguration; } @Override protected Collection loadCaches() { - List caches = new ArrayList(); - for (String cacheName : configuredCacheNames) { - caches.add(createCache(cacheName)); + List caches = new ArrayList<>(); + for (Map.Entry entry : initialPerCacheConfiguration.entrySet()) { + caches.add(createCache(entry.getKey(), entry.getValue())); } return caches; } @Override - protected Cache getMissingCache(String cacheName) { - return createCache(cacheName); - } - - protected AerospikeCache createCache(String cacheName) { - return new AerospikeSerializingCache(cacheName); - } - - @Override - public Cache getCache(String name) { - Cache cache = lookupAerospikeCache(name); - if (cache != null) { - return cache; - } - else { - Cache missingCache = getMissingCache(name); - if (missingCache != null) { - addCache(missingCache); - return lookupAerospikeCache(name); // may be decorated - } - return null; - } - } - - protected Cache lookupAerospikeCache(String name) { - return lookupCache(name + ":" + setName); + protected Cache getMissingCache(String name) { + return createCache(name); } @Override @@ -156,50 +103,15 @@ protected Cache decorateCache(Cache cache) { return super.decorateCache(cache); } - protected boolean isCacheAlreadyDecorated(Cache cache) { - return isTransactionAware() && cache instanceof TransactionAwareCacheDecorator; + private AerospikeCache createCache(String name) { + return new AerospikeCache(name, aerospikeClient, aerospikeConverter, defaultCacheConfiguration); } - public class AerospikeSerializingCache extends AerospikeCache { - - public AerospikeSerializingCache(String namespace) { - super(namespace, setName, aerospikeClient, -1); - } - - @Override - public T get(Object key, Class type) { - Key dbKey = getKey(key); - Record record = client.get(null, dbKey); - if (record != null) { - AerospikeReadData data = AerospikeReadData.forRead(dbKey, record); - T value = aerospikeConverter.read(type, data); - return value; - } - return null; - } - - @Override - public ValueWrapper get(Object key) { - Object value = get(key, Object.class); - return (value != null ? new SimpleValueWrapper(value) : null); - } - - private void serializeAndPut(WritePolicy writePolicy, Object key, Object value) { - AerospikeWriteData data = AerospikeWriteData.forWrite(); - aerospikeConverter.write(value, data); - client.put(writePolicy, getKey(key), data.getBinsAsArray()); - } - - @Override - public void put(Object key, Object value) { - serializeAndPut(null, key, value); - } - - @Override - public ValueWrapper putIfAbsent(Object key, Object value) { - serializeAndPut(createOnly, key, value); - return get(key); - } + private AerospikeCache createCache(String name, AerospikeCacheConfiguration cacheConfiguration) { + return new AerospikeCache(name, aerospikeClient, aerospikeConverter, cacheConfiguration); } + private boolean isCacheAlreadyDecorated(Cache cache) { + return isTransactionAware() && cache instanceof TransactionAwareCacheDecorator; + } } diff --git a/src/test/java/org/springframework/data/aerospike/BaseIntegrationTests.java b/src/test/java/org/springframework/data/aerospike/BaseIntegrationTests.java index 1971770a7..6447c9401 100644 --- a/src/test/java/org/springframework/data/aerospike/BaseIntegrationTests.java +++ b/src/test/java/org/springframework/data/aerospike/BaseIntegrationTests.java @@ -8,6 +8,8 @@ public abstract class BaseIntegrationTests { + public static final String DEFAULT_SET_NAME = "aerospike"; + @Value("${embedded.aerospike.namespace}") protected String namespace; diff --git a/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerIntegrationTests.java b/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerIntegrationTests.java index 435bd39ab..ef67b0808 100644 --- a/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerIntegrationTests.java +++ b/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerIntegrationTests.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.aerospike.cache; import com.aerospike.client.AerospikeClient; @@ -6,31 +22,39 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.aerospike.BaseBlockingIntegrationTests; +import org.springframework.data.aerospike.core.AerospikeOperations; import static org.assertj.core.api.Assertions.assertThat; public class AerospikeCacheManagerIntegrationTests extends BaseBlockingIntegrationTests { private static final String KEY = "foo"; + private static final String KEY_THAT_MATCHES_CONDITION = "abcdef"; private static final String VALUE = "bar"; @Autowired AerospikeClient client; @Autowired CachingComponent cachingComponent; + @Autowired + AerospikeOperations aerospikeOperations; + @Autowired + AerospikeCacheManager aerospikeCacheManager; @AfterEach public void tearDown() { cachingComponent.reset(); - client.delete(null, new Key(getNameSpace(), AerospikeCacheManager.DEFAULT_SET_NAME, KEY)); + client.delete(null, new Key(getNameSpace(), DEFAULT_SET_NAME, KEY)); + client.delete(null, new Key(getNameSpace(), DEFAULT_SET_NAME, KEY_THAT_MATCHES_CONDITION)); } @Test public void shouldCache() { - CachedObject response1 = cachingComponent.cachingMethod(KEY); - CachedObject response2 = cachingComponent.cachingMethod(KEY); + CachedObject response1 = cachingComponent.cacheableMethod(KEY); + CachedObject response2 = cachingComponent.cacheableMethod(KEY); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); @@ -41,9 +65,9 @@ public void shouldCache() { @Test public void shouldEvictCache() { - CachedObject response1 = cachingComponent.cachingMethod(KEY); - cachingComponent.cacheEvictingMethod(KEY); - CachedObject response2 = cachingComponent.cachingMethod(KEY); + CachedObject response1 = cachingComponent.cacheableMethod(KEY); + cachingComponent.cacheEvictMethod(KEY); + CachedObject response2 = cachingComponent.cacheableMethod(KEY); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); @@ -52,6 +76,96 @@ public void shouldEvictCache() { assertThat(cachingComponent.getNoOfCalls()).isEqualTo(2); } + @Test + public void shouldNotEvictCacheEvictingDifferentKey() { + CachedObject response1 = cachingComponent.cacheableMethod(KEY); + cachingComponent.cacheEvictMethod("not-the-relevant-key"); + CachedObject response2 = cachingComponent.cacheableMethod(KEY); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheUsingCachePut() { + CachedObject response1 = cachingComponent.cachePutMethod(KEY); + CachedObject response2 = cachingComponent.cacheableMethod(KEY); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheKeyMatchesCondition() { + CachedObject response1 = cachingComponent.cacheableWithCondition(KEY_THAT_MATCHES_CONDITION); + CachedObject response2 = cachingComponent.cacheableWithCondition(KEY_THAT_MATCHES_CONDITION); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldNotCacheKeyDoesNotMatchCondition() { + CachedObject response1 = cachingComponent.cacheableWithCondition(KEY); + CachedObject response2 = cachingComponent.cacheableWithCondition(KEY); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(2); + } + + @Test + public void shouldCacheWithConfiguredTTL() throws InterruptedException { + CachedObject response1 = cachingComponent.cacheableMethodWithTTL(KEY); + CachedObject response2 = cachingComponent.cacheableMethodWithTTL(KEY); + + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + + Thread.sleep(3000); + + CachedObject response3 = cachingComponent.cacheableMethodWithTTL(KEY); + + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(2); + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(response3).isNotNull(); + assertThat(response3.getValue()).isEqualTo(VALUE); + } + + @Test + public void shouldCacheUsingAnotherCacheManager() { + CachedObject response1 = cachingComponent.cacheableMethodWithAnotherCacheManager(KEY); + CachedObject response2 = cachingComponent.cacheableMethodWithAnotherCacheManager(KEY); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldNotClearCacheClearingDifferentCache() throws InterruptedException { + CachedObject response1 = cachingComponent.cacheableMethod(KEY); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(1); + aerospikeCacheManager.getCache("DIFFERENT-EXISTING-CACHE").clear(); + Thread.sleep(500); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(1); + } + public static class CachingComponent { private int noOfCalls = 0; @@ -61,14 +175,38 @@ public void reset() { } @Cacheable("TEST") - public CachedObject cachingMethod(String param) { + public CachedObject cacheableMethod(String param) { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable(value = "CACHE-WITH-TTL") + public CachedObject cacheableMethodWithTTL(String param) { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable(value = "TEST", cacheManager = "anotherCacheManager") + public CachedObject cacheableMethodWithAnotherCacheManager(String param) { noOfCalls++; return new CachedObject(VALUE); } @CacheEvict("TEST") - public void cacheEvictingMethod(String param) { + public void cacheEvictMethod(String param) { + + } + + @CachePut("TEST") + public CachedObject cachePutMethod(String param) { + noOfCalls++; + return new CachedObject(VALUE); + } + @Cacheable(value = "TEST", condition = "#param.startsWith('abc')") + public CachedObject cacheableWithCondition(String param) { + noOfCalls++; + return new CachedObject(VALUE); } public int getNoOfCalls() { @@ -87,5 +225,4 @@ public String getValue() { return value; } } - } diff --git a/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheMangerTests.java b/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheMangerTests.java index 3c56afcaa..41455d1d3 100644 --- a/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheMangerTests.java +++ b/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheMangerTests.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2019 the original author or authors. * * 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 * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://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, @@ -19,12 +19,14 @@ import com.aerospike.client.AerospikeClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.Cache; import org.springframework.cache.transaction.TransactionAwareCacheDecorator; import org.springframework.data.aerospike.BaseBlockingIntegrationTests; import org.springframework.data.aerospike.convert.MappingAerospikeConverter; -import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -34,12 +36,18 @@ */ public class AerospikeCacheMangerTests extends BaseBlockingIntegrationTests { - @Autowired AerospikeClient client; - @Autowired MappingAerospikeConverter converter; + @Value("${embedded.aerospike.namespace}") + public String namespace; + + @Autowired + AerospikeClient client; + @Autowired + MappingAerospikeConverter converter; @Test public void missingCache() { - AerospikeCacheManager manager = new AerospikeCacheManager(client, converter); + AerospikeCacheConfiguration aerospikeCacheConfiguration = new AerospikeCacheConfiguration(namespace, DEFAULT_SET_NAME); + AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration); manager.afterPropertiesSet(); Cache cache = manager.getCache("missing-cache"); @@ -48,32 +56,36 @@ public void missingCache() { @Test public void defaultCache() { - AerospikeCacheManager manager = new AerospikeCacheManager(client, - Arrays.asList("default-cache"), converter); + AerospikeCacheConfiguration aerospikeCacheConfiguration = new AerospikeCacheConfiguration(namespace, DEFAULT_SET_NAME); + Map aerospikeCacheConfigurationMap = new HashMap<>(); + aerospikeCacheConfigurationMap.put("default-cache", aerospikeCacheConfiguration); + AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration, aerospikeCacheConfigurationMap); manager.afterPropertiesSet(); - Cache cache = manager.lookupAerospikeCache("default-cache"); + Cache cache = manager.getCache("default-cache"); assertThat(cache).isNotNull().isInstanceOf(AerospikeCache.class); } @Test public void defaultCacheWithCustomizedSet() { - AerospikeCacheManager manager = new AerospikeCacheManager(client, - Arrays.asList("default-cache"), "custom-set", converter); + Map aerospikeCacheConfigurationMap = new HashMap<>(); + AerospikeCacheConfiguration aerospikeCacheConfiguration = new AerospikeCacheConfiguration(namespace, "custom-set"); + aerospikeCacheConfigurationMap.put("default-cache",aerospikeCacheConfiguration); + AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration, aerospikeCacheConfigurationMap); manager.afterPropertiesSet(); - Cache cache = manager.lookupAerospikeCache("default-cache"); + Cache cache = manager.getCache("default-cache"); assertThat(cache).isNotNull().isInstanceOf(AerospikeCache.class); } @Test public void transactionAwareCache() { - AerospikeCacheManager manager = new AerospikeCacheManager(client, converter); + AerospikeCacheConfiguration aerospikeCacheConfiguration = new AerospikeCacheConfiguration(namespace, DEFAULT_SET_NAME); + AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration); manager.setTransactionAware(true); manager.afterPropertiesSet(); Cache cache = manager.getCache("transaction-aware-cache"); assertThat(cache).isNotNull().isInstanceOf(TransactionAwareCacheDecorator.class); } - } diff --git a/src/test/java/org/springframework/data/aerospike/config/CommonTestConfig.java b/src/test/java/org/springframework/data/aerospike/config/CommonTestConfig.java index bbb51ab4e..8a8d8998d 100644 --- a/src/test/java/org/springframework/data/aerospike/config/CommonTestConfig.java +++ b/src/test/java/org/springframework/data/aerospike/config/CommonTestConfig.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.aerospike.config; import com.aerospike.client.AerospikeClient; @@ -7,11 +23,17 @@ import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.aerospike.BaseIntegrationTests; +import org.springframework.data.aerospike.cache.AerospikeCacheConfiguration; import org.springframework.data.aerospike.cache.AerospikeCacheManager; import org.springframework.data.aerospike.cache.AerospikeCacheManagerIntegrationTests.CachingComponent; import org.springframework.data.aerospike.convert.MappingAerospikeConverter; import org.springframework.data.aerospike.query.QueryEngineTestDataPopulator; +import java.util.HashMap; +import java.util.Map; + /** * @author Taras Danylchuk */ @@ -24,8 +46,21 @@ public class CommonTestConfig { protected String namespace; @Bean + @Primary public CacheManager cacheManager(AerospikeClient aerospikeClient, MappingAerospikeConverter aerospikeConverter) { - return new AerospikeCacheManager(aerospikeClient, aerospikeConverter); + AerospikeCacheConfiguration defaultCacheConfiguration = new AerospikeCacheConfiguration(namespace, BaseIntegrationTests.DEFAULT_SET_NAME); + AerospikeCacheConfiguration aerospikeCacheConfiguration = new AerospikeCacheConfiguration(namespace, "different-set"); + AerospikeCacheConfiguration configurationWithTTL = new AerospikeCacheConfiguration(namespace,BaseIntegrationTests.DEFAULT_SET_NAME, 2); + Map aerospikeCacheConfigurationMap = new HashMap<>(); + aerospikeCacheConfigurationMap.put("DIFFERENT-EXISTING-CACHE", aerospikeCacheConfiguration); + aerospikeCacheConfigurationMap.put("CACHE-WITH-TTL", configurationWithTTL); + return new AerospikeCacheManager(aerospikeClient, aerospikeConverter, defaultCacheConfiguration, aerospikeCacheConfigurationMap); + } + + @Bean + public CacheManager anotherCacheManager(AerospikeClient aerospikeClient, MappingAerospikeConverter aerospikeConverter) { + AerospikeCacheConfiguration defaultCacheConfiguration = new AerospikeCacheConfiguration(namespace, BaseIntegrationTests.DEFAULT_SET_NAME); + return new AerospikeCacheManager(aerospikeClient, aerospikeConverter, defaultCacheConfiguration); } @Bean