Skip to content

Commit aee69db

Browse files
authored
feat(spring): add removeCache method to cache manager (#357)
1 parent 9768666 commit aee69db

File tree

6 files changed

+159
-32
lines changed

6 files changed

+159
-32
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.github.xanthic.cache.springjdk17;
2+
3+
import org.springframework.cache.Cache;
4+
5+
record CacheWrapper(Cache cache, boolean custom) {
6+
CacheWrapper(Cache cache) {
7+
this(cache, false);
8+
}
9+
}

spring-java17/src/main/java/io/github/xanthic/cache/springjdk17/XanthicSpringCacheManager.java

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import io.github.xanthic.cache.core.CacheApi;
44
import io.github.xanthic.cache.core.CacheApiSpec;
5-
import lombok.Getter;
65
import org.jetbrains.annotations.NotNull;
6+
import org.jetbrains.annotations.VisibleForTesting;
77
import org.springframework.cache.Cache;
88
import org.springframework.cache.CacheManager;
99
import org.springframework.lang.Nullable;
1010

1111
import java.util.Collection;
12+
import java.util.Collections;
13+
import java.util.HashSet;
1214
import java.util.Map;
1315
import java.util.Set;
1416
import java.util.concurrent.ConcurrentHashMap;
@@ -21,9 +23,7 @@
2123
*/
2224
public class XanthicSpringCacheManager implements CacheManager {
2325

24-
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
25-
@Getter
26-
private final Set<String> customCacheNames = ConcurrentHashMap.newKeySet();
26+
private final Map<String, CacheWrapper> cacheMap = new ConcurrentHashMap<>();
2727
private final Consumer<CacheApiSpec<Object, Object>> spec;
2828
private final boolean dynamic;
2929

@@ -49,7 +49,7 @@ public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @N
4949
if (cacheNames != null) {
5050
this.dynamic = false;
5151
for (String name : cacheNames) {
52-
this.cacheMap.put(name, createCache(name, this.spec));
52+
this.cacheMap.put(name, new CacheWrapper(createCache(name, this.spec)));
5353
}
5454
} else {
5555
this.dynamic = true;
@@ -60,16 +60,32 @@ public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @N
6060
@Nullable
6161
public Cache getCache(@NotNull String name) {
6262
// Optimistic lock-free lookup to avoid contention: https://github.com/spring-projects/spring-framework/issues/30066
63-
Cache optimistic = cacheMap.get(name);
64-
if (optimistic != null || !dynamic)
65-
return optimistic;
63+
CacheWrapper optimistic = cacheMap.get(name);
64+
if (optimistic != null)
65+
return optimistic.cache();
66+
if (!dynamic)
67+
return null;
6668

67-
return this.cacheMap.computeIfAbsent(name, cacheName -> createCache(cacheName, this.spec));
69+
return this.cacheMap.computeIfAbsent(name, cacheName -> new CacheWrapper(createCache(cacheName, this.spec))).cache();
6870
}
6971

7072
@Override
7173
public @NotNull Collection<String> getCacheNames() {
72-
return this.cacheMap.keySet();
74+
return Collections.unmodifiableSet(cacheMap.keySet());
75+
}
76+
77+
@NotNull
78+
@Deprecated
79+
@VisibleForTesting
80+
public Set<String> getCustomCacheNames() {
81+
// unfortunately O(n) so this manager can be lock-free, but this operation should be rare in practice
82+
Set<String> names = new HashSet<>();
83+
cacheMap.forEach((k, v) -> {
84+
if (v.custom()) {
85+
names.add(k);
86+
}
87+
});
88+
return Collections.unmodifiableSet(names);
7389
}
7490

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

84-
this.cacheMap.put(name, createCache(name, spec));
85-
this.customCacheNames.add(name);
100+
this.cacheMap.put(name, new CacheWrapper(createCache(name, spec), true));
101+
}
102+
103+
/**
104+
* Removes a named cache from this cache manager.
105+
*
106+
* @param name the name of the cache
107+
*/
108+
public void removeCache(String name) {
109+
if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow removal of existing caches.");
110+
111+
this.cacheMap.remove(name);
86112
}
87113

88114
private Cache createCache(String name, Consumer<CacheApiSpec<Object, Object>> spec) {

spring-java17/src/test/java/io/github/xanthic/cache/springjdk17/SpringCacheTest.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,42 @@ public void putGetClearTest() {
6363

6464
@Test
6565
@DisplayName("Tests the registration and usage of a custom cache")
66+
@SuppressWarnings("deprecation")
6667
public void registerCustomCacheTest() {
6768
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
68-
xanthicSpringCacheManager.registerCache("my-custom-cache", spec -> {
69+
String name = "my-custom-cache";
70+
xanthicSpringCacheManager.registerCache(name, spec -> {
6971
spec.maxSize(1L);
7072
});
7173

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

7577
// cache available
76-
Cache cache = cacheManager.getCache("my-custom-cache");
77-
Assertions.assertNotNull(cache, "my-custom-cache should not be null");
78+
Cache cache = cacheManager.getCache(name);
79+
Assertions.assertNotNull(cache, name + " should not be null");
80+
81+
// remove cache
82+
xanthicSpringCacheManager.removeCache(name);
83+
Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should no longer be present");
84+
}
85+
86+
@Test
87+
@DisplayName("Tests that dynamic caches can be removed from the manager")
88+
@SuppressWarnings("deprecation")
89+
public void removeDynamicTest() {
90+
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
91+
92+
// create dynamic cache
93+
String name = "my-dynamic-cache";
94+
Cache cache = xanthicSpringCacheManager.getCache(name);
95+
Assertions.assertNotNull(cache, name + " should not be null");
96+
Assertions.assertTrue(cacheManager.getCacheNames().contains(name), name + " should be present in the manager");
97+
Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should not be present as a custom cache");
98+
99+
// remove cache
100+
xanthicSpringCacheManager.removeCache(name);
101+
Assertions.assertFalse(cacheManager.getCacheNames().contains(name), name + " should no longer be present in the manager");
78102
}
79103

80104
@Test
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.github.xanthic.cache.spring;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.Value;
5+
import lombok.experimental.Accessors;
6+
import org.springframework.cache.Cache;
7+
8+
@Value
9+
@Accessors(fluent = true)
10+
@RequiredArgsConstructor
11+
class CacheWrapper {
12+
Cache cache;
13+
boolean custom;
14+
15+
CacheWrapper(Cache cache) {
16+
this(cache, false);
17+
}
18+
}

spring/src/main/java/io/github/xanthic/cache/spring/XanthicSpringCacheManager.java

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import io.github.xanthic.cache.core.CacheApi;
44
import io.github.xanthic.cache.core.CacheApiSpec;
5-
import lombok.Getter;
65
import org.jetbrains.annotations.NotNull;
6+
import org.jetbrains.annotations.VisibleForTesting;
77
import org.springframework.cache.Cache;
88
import org.springframework.cache.CacheManager;
99
import org.springframework.lang.Nullable;
1010

1111
import java.util.Collection;
12+
import java.util.Collections;
13+
import java.util.HashSet;
1214
import java.util.Map;
1315
import java.util.Set;
1416
import java.util.concurrent.ConcurrentHashMap;
@@ -21,9 +23,7 @@
2123
*/
2224
public class XanthicSpringCacheManager implements CacheManager {
2325

24-
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
25-
@Getter
26-
private final Set<String> customCacheNames = ConcurrentHashMap.newKeySet();
26+
private final Map<String, CacheWrapper> cacheMap = new ConcurrentHashMap<>();
2727
private final Consumer<CacheApiSpec<Object, Object>> spec;
2828
private final boolean dynamic;
2929

@@ -49,7 +49,7 @@ public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @N
4949
if (cacheNames != null) {
5050
this.dynamic = false;
5151
for (String name : cacheNames) {
52-
this.cacheMap.put(name, createCache(name, this.spec));
52+
this.cacheMap.put(name, new CacheWrapper(createCache(name, this.spec)));
5353
}
5454
} else {
5555
this.dynamic = true;
@@ -60,16 +60,32 @@ public XanthicSpringCacheManager(Consumer<CacheApiSpec<Object, Object>> spec, @N
6060
@Nullable
6161
public Cache getCache(@NotNull String name) {
6262
// Optimistic lock-free lookup to avoid contention: https://github.com/spring-projects/spring-framework/issues/30066
63-
Cache optimistic = cacheMap.get(name);
64-
if (optimistic != null || !dynamic)
65-
return optimistic;
63+
CacheWrapper optimistic = cacheMap.get(name);
64+
if (optimistic != null)
65+
return optimistic.cache();
66+
if (!dynamic)
67+
return null;
6668

67-
return this.cacheMap.computeIfAbsent(name, cacheName -> createCache(cacheName, this.spec));
69+
return this.cacheMap.computeIfAbsent(name, cacheName -> new CacheWrapper(createCache(cacheName, this.spec))).cache();
6870
}
6971

7072
@Override
7173
public @NotNull Collection<String> getCacheNames() {
72-
return this.cacheMap.keySet();
74+
return Collections.unmodifiableSet(cacheMap.keySet());
75+
}
76+
77+
@NotNull
78+
@Deprecated
79+
@VisibleForTesting
80+
public Set<String> getCustomCacheNames() {
81+
// unfortunately O(n) so this manager can be lock-free, but this operation should be rare in practice
82+
Set<String> names = new HashSet<>();
83+
cacheMap.forEach((k, v) -> {
84+
if (v.custom()) {
85+
names.add(k);
86+
}
87+
});
88+
return Collections.unmodifiableSet(names);
7389
}
7490

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

84-
this.cacheMap.put(name, createCache(name, spec));
85-
this.customCacheNames.add(name);
100+
this.cacheMap.put(name, new CacheWrapper(createCache(name, spec), true));
101+
}
102+
103+
/**
104+
* Removes a named cache from this cache manager.
105+
*
106+
* @param name the name of the cache
107+
*/
108+
public void removeCache(String name) {
109+
if (!this.dynamic) throw new IllegalStateException("CacheManager has a fixed set of cache keys and does not allow removal of existing caches.");
110+
111+
this.cacheMap.remove(name);
86112
}
87113

88114
private Cache createCache(String name, Consumer<CacheApiSpec<Object, Object>> spec) {

spring/src/test/java/io/github/xanthic/cache/spring/SpringCacheTest.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,42 @@ public void putGetClearTest() {
5050

5151
@Test
5252
@DisplayName("Tests the registration and usage of a custom cache")
53+
@SuppressWarnings("deprecation")
5354
public void registerCustomCacheTest() {
5455
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
55-
xanthicSpringCacheManager.registerCache("my-custom-cache", spec -> {
56+
String name = "my-custom-cache";
57+
xanthicSpringCacheManager.registerCache(name, spec -> {
5658
spec.maxSize(1L);
5759
});
5860

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

6264
// cache available
63-
Cache cache = cacheManager.getCache("my-custom-cache");
64-
Assertions.assertNotNull(cache, "my-custom-cache should not be null");
65+
Cache cache = cacheManager.getCache(name);
66+
Assertions.assertNotNull(cache, name + " should not be null");
67+
68+
// remove cache
69+
xanthicSpringCacheManager.removeCache(name);
70+
Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should no longer be present");
71+
}
72+
73+
@Test
74+
@DisplayName("Tests that dynamic caches can be removed from the manager")
75+
@SuppressWarnings("deprecation")
76+
public void removeDynamicTest() {
77+
XanthicSpringCacheManager xanthicSpringCacheManager = (XanthicSpringCacheManager) cacheManager;
78+
79+
// create dynamic cache
80+
String name = "my-dynamic-cache";
81+
Cache cache = xanthicSpringCacheManager.getCache(name);
82+
Assertions.assertNotNull(cache, name + " should not be null");
83+
Assertions.assertTrue(cacheManager.getCacheNames().contains(name), name + " should be present in the manager");
84+
Assertions.assertFalse(xanthicSpringCacheManager.getCustomCacheNames().contains(name), name + " should not be present as a custom cache");
85+
86+
// remove cache
87+
xanthicSpringCacheManager.removeCache(name);
88+
Assertions.assertFalse(cacheManager.getCacheNames().contains(name), name + " should no longer be present in the manager");
6589
}
6690

6791
@Test

0 commit comments

Comments
 (0)