Skip to content

Commit 3c55e65

Browse files
authored
Merge pull request #3877 from evolvedbinary/bugfix/bound-lazy-cache-creation
Lazily created caches should be bounded
2 parents 04e5a8b + 8853dbc commit 3c55e65

File tree

6 files changed

+133
-17
lines changed

6 files changed

+133
-17
lines changed

exist-distribution/src/main/config/conf.xml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,31 @@
989989
<!--
990990
Extension Modules
991991
-->
992-
<module uri="http://exist-db.org/xquery/cache" class="org.exist.xquery.modules.cache.CacheModule"/>
992+
<module uri="http://exist-db.org/xquery/cache" class="org.exist.xquery.modules.cache.CacheModule">
993+
<!--
994+
If a named Cache does not exist it can be created lazily on-demand
995+
when an operation is first performed upon it.
996+
997+
- enableLazyCreation
998+
true to enable lazy creation, false to disable.
999+
When false, if the Cache is not explicitly created before
1000+
it is used then an error is raised.
1001+
1002+
- lazy.*
1003+
These settings are as documented for the Cache Module
1004+
and control the config that is used when lazily creating
1005+
a Cache.
1006+
Valid settings are:
1007+
- lazy.maximumSize
1008+
- lazy.expireAfterAccess
1009+
- lazy.putGroup
1010+
- lazy.getGroup
1011+
- lazy.removeGroup
1012+
- lazy.clearGroup
1013+
-->
1014+
<parameter name="enableLazyCreation" value="true"/>
1015+
<parameter name="lazy.maximumSize" value="128"/>
1016+
</module>
9931017
<module uri="http://exist-db.org/xquery/compression" class="org.exist.xquery.modules.compression.CompressionModule"/>
9941018
<module uri="http://exist-db.org/xquery/counter" class="org.exist.xquery.modules.counter.CounterModule"/>
9951019
<module uri="http://exist-db.org/xquery/cqlparser" class="org.exist.xquery.modules.cqlparser.CQLParserModule"/>

extensions/modules/cache/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
<artifactId>caffeine</artifactId>
5858
</dependency>
5959

60+
<dependency>
61+
<groupId>org.apache.logging.log4j</groupId>
62+
<artifactId>log4j-api</artifactId>
63+
</dependency>
64+
6065
<dependency>
6166
<groupId>xml-apis</groupId>
6267
<artifactId>xml-apis</artifactId>

extensions/modules/cache/src/main/java/org/exist/xquery/modules/cache/CacheConfig.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ public class CacheConfig {
3434
private final Optional<Long> maximumSize;
3535
private final Optional<Long> expireAfterAccess;
3636

37-
public CacheConfig() {
38-
this(Optional.empty(), Optional.empty(), Optional.empty());
39-
}
40-
4137
/**
4238
* @param permissions Any restrictions on cache operations
4339
* @param maximumSize The maximimum number of entries in the cache

extensions/modules/cache/src/main/java/org/exist/xquery/modules/cache/CacheFunctions.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public class CacheFunctions extends BasicFunction {
6262
"Explicitly create a cache with a specific configuration",
6363
returns(Type.BOOLEAN, "true if the cache was created, false if the cache already exists"),
6464
FS_PARAM_CACHE_NAME,
65-
param("config", Type.MAP, "A map with configuration for the cache. At present cache LRU and permission groups may be specified, for operations on the cache. `maximumSize` is optional and specifies the maximum number of entries. `expireAfterAccess` is optional and specified the expiry period for infrequently accessed entries (in milliseconds). If a permission group is not specified for an operation, then permissions are not checked for that operation. Should have the format: map { \"maximumSize\": 1000, \"expireAfterAccess\": 120000, \"permissions\": map { \"put-group\": \"group1\", \"get-group\": \"group2\", \"remove-group\": \"group3\", \"clear-group\": \"group4\"} }")
65+
param("config", Type.MAP, "A map with configuration for the cache. At present cache LRU and permission groups may be specified, for operations on the cache. `maximumSize` is optional and specifies the maximum number of entries. `expireAfterAccess` is optional and specifies the expiry period for infrequently accessed entries (in milliseconds). If a permission group is not specified for an operation, then permissions are not checked for that operation. Should have the format: map { \"maximumSize\": 1000, \"expireAfterAccess\": 120000, \"permissions\": map { \"put-group\": \"group1\", \"get-group\": \"group2\", \"remove-group\": \"group3\", \"clear-group\": \"group4\"} }")
6666
);
6767

6868
private static final String FS_NAMES_NAME = "names";
@@ -159,7 +159,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
159159
cacheName = null;
160160
}
161161

162-
switch(getName().getLocalPart()) {
162+
switch (getName().getLocalPart()) {
163163

164164
case FS_CREATE_NAME:
165165
if(CacheModule.caches.containsKey(cacheName)) {
@@ -173,7 +173,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
173173
case FS_PUT_NAME:
174174
// lazy create cache if it doesn't exist
175175
if(!CacheModule.caches.containsKey(cacheName)) {
176-
createCache(cacheName, new CacheConfig());
176+
lazilyCreateCache(cacheName);
177177
}
178178
final String putKey = toMapKey(args[1]);
179179
final Sequence value = args[2];
@@ -182,30 +182,30 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
182182
case FS_LIST_NAME:
183183
// lazy create cache if it doesn't exist
184184
if(!CacheModule.caches.containsKey(cacheName)) {
185-
createCache(cacheName, new CacheConfig());
185+
lazilyCreateCache(cacheName);
186186
}
187187
final String[] keys = toMapKeys(args[1]);
188188
return list(cacheName, keys);
189189

190190
case FS_KEYS_NAME:
191191
// lazy create cache if it doesn't exist
192192
if(!CacheModule.caches.containsKey(cacheName)) {
193-
createCache(cacheName, new CacheConfig());
193+
lazilyCreateCache(cacheName);
194194
}
195195
return listKeys(cacheName);
196196

197197
case FS_GET_NAME:
198198
// lazy create cache if it doesn't exist
199199
if(!CacheModule.caches.containsKey(cacheName)) {
200-
createCache(cacheName, new CacheConfig());
200+
lazilyCreateCache(cacheName);
201201
}
202202
final String getKey = toMapKey(args[1]);
203203
return get(cacheName, getKey);
204204

205205
case FS_REMOVE_NAME:
206206
// lazy create cache if it doesn't exist
207207
if(!CacheModule.caches.containsKey(cacheName)) {
208-
createCache(cacheName, new CacheConfig());
208+
lazilyCreateCache(cacheName);
209209
}
210210
final String removeKey = toMapKey(args[1]);
211211
return remove(cacheName, removeKey);
@@ -292,6 +292,17 @@ private boolean createCache(final String cacheName, final CacheConfig config) {
292292
return newOrExisting.getConfig() == config;
293293
}
294294

295+
private void lazilyCreateCache(final String cacheName) throws XPathException {
296+
final CacheModule cacheModule = (CacheModule) getParentModule();
297+
final Optional<CacheConfig> maybeLazyCacheConfig = cacheModule.getLazyCacheConfig();
298+
299+
if (!maybeLazyCacheConfig.isPresent()) {
300+
throw new XPathException(this, LAZY_CREATION_DISABLED, "There is no such named cache: " + cacheName + ", and lazy creation of the cache has been disabled.");
301+
}
302+
303+
createCache(cacheName, maybeLazyCacheConfig.get());
304+
}
305+
295306
private Sequence cacheNames() throws XPathException {
296307
final Sequence result = new ValueSequence();
297308
for(final String cacheName : CacheModule.caches.keySet()) {

extensions/modules/cache/src/main/java/org/exist/xquery/modules/cache/CacheModule.java

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@
2323

2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.Optional;
2627
import java.util.concurrent.ConcurrentHashMap;
2728

29+
import org.apache.logging.log4j.LogManager;
30+
import org.apache.logging.log4j.Logger;
2831
import org.exist.dom.QName;
2932
import org.exist.xquery.*;
3033
import org.exist.xquery.value.FunctionParameterSequenceType;
3134
import org.exist.xquery.value.FunctionReturnSequenceType;
35+
3236
import static org.exist.xquery.FunctionDSL.functionDefs;
3337

3438
/**
@@ -42,11 +46,13 @@
4246
*/
4347
public class CacheModule extends AbstractInternalModule {
4448

45-
public final static String NAMESPACE_URI = "http://exist-db.org/xquery/cache";
49+
private static final Logger LOG = LogManager.getLogger(CacheModule.class);
50+
51+
public static final String NAMESPACE_URI = "http://exist-db.org/xquery/cache";
4652

47-
public final static String PREFIX = "cache";
48-
public final static String INCLUSION_DATE = "2009-03-04";
49-
public final static String RELEASED_IN_VERSION = "eXist-1.4";
53+
public static final String PREFIX = "cache";
54+
public static final String INCLUSION_DATE = "2009-03-04";
55+
public static final String RELEASED_IN_VERSION = "eXist-1.4";
5056

5157
public static final FunctionDef[] functions = functionDefs(
5258
CacheFunctions.class,
@@ -62,11 +68,26 @@ public class CacheModule extends AbstractInternalModule {
6268
CacheFunctions.FS_CLEANUP,
6369
CacheFunctions.FS_DESTROY);
6470

71+
private static final String PARAM_NAME_ENABLE_LAZY_CREATION = "enableLazyCreation";
72+
private static final String PARAM_NAME_LAZY_MAXIMUM_SIZE = "lazy.maximumSize";
73+
private static final String PARAM_NAME_LAZY_EXPIRE_AFTER_ACCESS = "lazy.expireAfterAccess";
74+
private static final String PARAM_NAME_LAZY_PUT_GROUP = "lazy.putGroup";
75+
private static final String PARAM_NAME_LAZY_GET_GROUP = "lazy.getGroup";
76+
private static final String PARAM_NAME_LAZY_REMOVE_GROUP = "lazy.removeGroup";
77+
private static final String PARAM_NAME_LAZY_CLEAR_GROUP = "lazy.clearGroup";
78+
79+
private static final long DEFAULT_LAZY_MAXIMUM_SIZE = 128; // 128 items
80+
private static final long DEFAULT_LAZY_EXPIRE_AFTER_ACCESS = 1000 * 60 * 5; // 5 minutes
6581

6682
static final Map<String, Cache> caches = new ConcurrentHashMap<>();
6783

84+
private final Optional<CacheConfig> lazyCacheConfig;
85+
6886
public CacheModule(final Map<String, List<?>> parameters) {
6987
super(functions, parameters);
88+
89+
// read parameters
90+
this.lazyCacheConfig = parseParameters(parameters);
7091
}
7192

7293
@Override
@@ -105,4 +126,60 @@ private CacheModuleErrorCode(final String code, final String description) {
105126

106127
static final ErrorCodes.ErrorCode INSUFFICIENT_PERMISSIONS = new CacheModuleErrorCode("insufficient-permissions", "The calling user does not have sufficient permissions to operate on the cache.");
107128
static final ErrorCodes.ErrorCode KEY_SERIALIZATION = new CacheModuleErrorCode("key-serialization", "Unable to serialize the provided key.");
129+
static final ErrorCodes.ErrorCode LAZY_CREATION_DISABLED = new CacheModuleErrorCode("lazy-creation-disabled", "There is no such named cache, and lazy creation of the cache has been disabled.");
130+
131+
private static Optional<CacheConfig> parseParameters(final Map<String, List<?>> parameters) {
132+
if (parameters == null || parameters.isEmpty()) {
133+
return Optional.empty();
134+
}
135+
136+
final boolean enableLazyCreation = getFirstString(parameters, PARAM_NAME_ENABLE_LAZY_CREATION)
137+
.map(Boolean::parseBoolean)
138+
.orElse(false);
139+
140+
if (!enableLazyCreation) {
141+
return Optional.empty();
142+
}
143+
144+
final Optional<String> putGroup = getFirstString(parameters, PARAM_NAME_LAZY_PUT_GROUP);
145+
final Optional<String> getGroup = getFirstString(parameters, PARAM_NAME_LAZY_GET_GROUP);
146+
final Optional<String> removeGroup = getFirstString(parameters, PARAM_NAME_LAZY_REMOVE_GROUP);
147+
final Optional<String> clearGroup = getFirstString(parameters, PARAM_NAME_LAZY_CLEAR_GROUP);
148+
final Optional<CacheConfig.Permissions> permissions = Optional.of(new CacheConfig.Permissions(putGroup, getGroup, removeGroup, clearGroup));
149+
150+
final Optional<Long> maximumSize = getFirstString(parameters, PARAM_NAME_LAZY_MAXIMUM_SIZE)
151+
.map(s -> {
152+
try {
153+
return Long.parseLong(s);
154+
} catch (final NumberFormatException e) {
155+
LOG.warn("Unable to set {} to: {}. Using default: ", PARAM_NAME_LAZY_MAXIMUM_SIZE, s, DEFAULT_LAZY_MAXIMUM_SIZE);
156+
return DEFAULT_LAZY_MAXIMUM_SIZE;
157+
}
158+
});
159+
160+
final Optional<Long> expireAfterAccess = getFirstString(parameters, PARAM_NAME_LAZY_EXPIRE_AFTER_ACCESS)
161+
.map(s -> {
162+
try {
163+
return Long.parseLong(s);
164+
} catch (final NumberFormatException e) {
165+
LOG.warn("Unable to set {} to: {}. Using default: ", PARAM_NAME_LAZY_EXPIRE_AFTER_ACCESS, s, DEFAULT_LAZY_EXPIRE_AFTER_ACCESS);
166+
return DEFAULT_LAZY_EXPIRE_AFTER_ACCESS;
167+
}
168+
});
169+
170+
171+
return Optional.of(new CacheConfig(permissions, maximumSize, expireAfterAccess));
172+
}
173+
174+
private static Optional<String> getFirstString(final Map<String, List<?>> parameters, final String paramName) {
175+
return Optional.ofNullable(parameters.get(paramName))
176+
.filter(l -> l.size() == 1)
177+
.map(l -> l.get(0))
178+
.filter(o -> o instanceof String)
179+
.map(o -> (String)o);
180+
}
181+
182+
Optional<CacheConfig> getLazyCacheConfig() {
183+
return lazyCacheConfig;
184+
}
108185
}

extensions/modules/cache/src/test/resources-filtered/conf.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,10 @@
744744
<builtin-modules>
745745

746746
<!-- Module under test -->
747-
<module uri="http://exist-db.org/xquery/cache" class="org.exist.xquery.modules.cache.CacheModule"/>
747+
<module uri="http://exist-db.org/xquery/cache" class="org.exist.xquery.modules.cache.CacheModule">
748+
<parameter name="enableLazyCreation" value="true"/>
749+
<parameter name="lazy.maximumSize" value="128"/>
750+
</module>
748751

749752

750753
<!-- Needed for XQSuite! -->

0 commit comments

Comments
 (0)