Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.xanthic.cache.springjdk17;

import org.springframework.cache.Cache;

record CacheWrapper(Cache cache, boolean custom) {
CacheWrapper(Cache cache) {
this(cache, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import io.github.xanthic.cache.core.CacheApi;
import io.github.xanthic.cache.core.CacheApiSpec;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.lang.Nullable;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -21,9 +23,7 @@
*/
public class XanthicSpringCacheManager implements CacheManager {

private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
@Getter
private final Set<String> customCacheNames = ConcurrentHashMap.newKeySet();
private final Map<String, CacheWrapper> cacheMap = new ConcurrentHashMap<>();
private final Consumer<CacheApiSpec<Object, Object>> spec;
private final boolean dynamic;

Expand All @@ -49,7 +49,7 @@ public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @N
if (cacheNames != null) {
this.dynamic = false;
for (String name : cacheNames) {
this.cacheMap.put(name, createCache(name, this.spec));
this.cacheMap.put(name, new CacheWrapper(createCache(name, this.spec)));
}
} else {
this.dynamic = true;
Expand All @@ -60,16 +60,32 @@ public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @N
@Nullable
public Cache getCache(@NotNull String name) {
// Optimistic lock-free lookup to avoid contention: https://github.com/spring-projects/spring-framework/issues/30066
Cache optimistic = cacheMap.get(name);
if (optimistic != null || !dynamic)
return optimistic;
CacheWrapper optimistic = cacheMap.get(name);
if (optimistic != null)
return optimistic.cache();
if (!dynamic)
return null;

return this.cacheMap.computeIfAbsent(name, cacheName -> createCache(cacheName, this.spec));
return this.cacheMap.computeIfAbsent(name, cacheName -> new CacheWrapper(createCache(cacheName, this.spec))).cache();
}

@Override
public @NotNull Collection<String> getCacheNames() {
return this.cacheMap.keySet();
return Collections.unmodifiableSet(cacheMap.keySet());
}

@NotNull
@Deprecated
@VisibleForTesting
public Set<String> getCustomCacheNames() {
// unfortunately O(n) so this manager can be lock-free, but this operation should be rare in practice
Set<String> names = new HashSet<>();
cacheMap.forEach((k, v) -> {
if (v.custom()) {
names.add(k);
}
});
return Collections.unmodifiableSet(names);
}

/**
Expand All @@ -81,8 +97,18 @@ public Cache getCache(@NotNull String name) {
public void registerCache(String name, Consumer<CacheApiSpec<Object, Object>> spec) {
if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow creation of new caches.");

this.cacheMap.put(name, createCache(name, spec));
this.customCacheNames.add(name);
this.cacheMap.put(name, new CacheWrapper(createCache(name, spec), true));
}

/**
* Removes a named cache from this cache manager.
*
* @param name the name of the cache
*/
public void removeCache(String name) {
if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow removal of existing caches.");

this.cacheMap.remove(name);
}

private Cache createCache(String name, Consumer<CacheApiSpec<Object, Object>> spec) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,42 @@ public void putGetClearTest() {

@Test
@DisplayName("Tests the registration and usage of a custom cache")
@SuppressWarnings("deprecation")
public void registerCustomCacheTest() {
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
xanthicSpringCacheManager.registerCache("my-custom-cache", spec -> {
String name = "my-custom-cache";
xanthicSpringCacheManager.registerCache(name, spec -> {
spec.maxSize(1L);
});

// registration check
Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains("my-custom-cache"), "getCustomCacheNames should contain my-custom-cache");
Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains(name), "getCustomCacheNames should contain " + name);

// cache available
Cache cache = cacheManager.getCache("my-custom-cache");
Assertions.assertNotNull(cache, "my-custom-cache should not be null");
Cache cache = cacheManager.getCache(name);
Assertions.assertNotNull(cache, name + " should not be null");

// remove cache
xanthicSpringCacheManager.removeCache(name);
Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should no longer be present");
}

@Test
@DisplayName("Tests that dynamic caches can be removed from the manager")
@SuppressWarnings("deprecation")
public void removeDynamicTest() {
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;

// create dynamic cache
String name = "my-dynamic-cache";
Cache cache = xanthicSpringCacheManager.getCache(name);
Assertions.assertNotNull(cache, name + " should not be null");
Assertions.assertTrue(cacheManager.getCacheNames().contains(name), name + " should be present in the manager");
Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should not be present as a custom cache");

// remove cache
xanthicSpringCacheManager.removeCache(name);
Assertions.assertFalse(cacheManager.getCacheNames().contains(name), name + " should no longer be present in the manager");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.xanthic.cache.spring;

import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.experimental.Accessors;
import org.springframework.cache.Cache;

@Value
@Accessors(fluent = true)
@RequiredArgsConstructor
class CacheWrapper {
Cache cache;
boolean custom;

CacheWrapper(Cache cache) {
this(cache, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import io.github.xanthic.cache.core.CacheApi;
import io.github.xanthic.cache.core.CacheApiSpec;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.lang.Nullable;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -21,9 +23,7 @@
*/
public class XanthicSpringCacheManager implements CacheManager {

private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
@Getter
private final Set<String> customCacheNames = ConcurrentHashMap.newKeySet();
private final Map<String, CacheWrapper> cacheMap = new ConcurrentHashMap<>();
private final Consumer<CacheApiSpec<Object, Object>> spec;
private final boolean dynamic;

Expand All @@ -49,7 +49,7 @@ public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @N
if (cacheNames != null) {
this.dynamic = false;
for (String name : cacheNames) {
this.cacheMap.put(name, createCache(name, this.spec));
this.cacheMap.put(name, new CacheWrapper(createCache(name, this.spec)));
}
} else {
this.dynamic = true;
Expand All @@ -60,16 +60,32 @@ public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @N
@Nullable
public Cache getCache(@NotNull String name) {
// Optimistic lock-free lookup to avoid contention: https://github.com/spring-projects/spring-framework/issues/30066
Cache optimistic = cacheMap.get(name);
if (optimistic != null || !dynamic)
return optimistic;
CacheWrapper optimistic = cacheMap.get(name);
if (optimistic != null)
return optimistic.cache();
if (!dynamic)
return null;

return this.cacheMap.computeIfAbsent(name, cacheName -> createCache(cacheName, this.spec));
return this.cacheMap.computeIfAbsent(name, cacheName -> new CacheWrapper(createCache(cacheName, this.spec))).cache();
}

@Override
public @NotNull Collection<String> getCacheNames() {
return this.cacheMap.keySet();
return Collections.unmodifiableSet(cacheMap.keySet());
}

@NotNull
@Deprecated
@VisibleForTesting
public Set<String> getCustomCacheNames() {
// unfortunately O(n) so this manager can be lock-free, but this operation should be rare in practice
Set<String> names = new HashSet<>();
cacheMap.forEach((k, v) -> {
if (v.custom()) {
names.add(k);
}
});
return Collections.unmodifiableSet(names);
}

/**
Expand All @@ -81,8 +97,18 @@ public Cache getCache(@NotNull String name) {
public void registerCache(String name, Consumer<CacheApiSpec<Object, Object>> spec) {
if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow creation of new caches.");

this.cacheMap.put(name, createCache(name, spec));
this.customCacheNames.add(name);
this.cacheMap.put(name, new CacheWrapper(createCache(name, spec), true));
}

/**
* Removes a named cache from this cache manager.
*
* @param name the name of the cache
*/
public void removeCache(String name) {
if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow removal of existing caches.");

this.cacheMap.remove(name);
}

private Cache createCache(String name, Consumer<CacheApiSpec<Object, Object>> spec) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,42 @@ public void putGetClearTest() {

@Test
@DisplayName("Tests the registration and usage of a custom cache")
@SuppressWarnings("deprecation")
public void registerCustomCacheTest() {
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
xanthicSpringCacheManager.registerCache("my-custom-cache", spec -> {
String name = "my-custom-cache";
xanthicSpringCacheManager.registerCache(name, spec -> {
spec.maxSize(1L);
});

// registration check
Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains("my-custom-cache"), "getCustomCacheNames should contain my-custom-cache");
Assertions.assertTrue(xanthicSpringCacheManager.getCustomCacheNames().contains(name), "getCustomCacheNames should contain " + name);

// cache available
Cache cache = cacheManager.getCache("my-custom-cache");
Assertions.assertNotNull(cache, "my-custom-cache should not be null");
Cache cache = cacheManager.getCache(name);
Assertions.assertNotNull(cache, name + " should not be null");

// remove cache
xanthicSpringCacheManager.removeCache(name);
Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should no longer be present");
}

@Test
@DisplayName("Tests that dynamic caches can be removed from the manager")
@SuppressWarnings("deprecation")
public void removeDynamicTest() {
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;

// create dynamic cache
String name = "my-dynamic-cache";
Cache cache = xanthicSpringCacheManager.getCache(name);
Assertions.assertNotNull(cache, name + " should not be null");
Assertions.assertTrue(cacheManager.getCacheNames().contains(name), name + " should be present in the manager");
Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should not be present as a custom cache");

// remove cache
xanthicSpringCacheManager.removeCache(name);
Assertions.assertFalse(cacheManager.getCacheNames().contains(name), name + " should no longer be present in the manager");
}

@Test
Expand Down
Loading