diff --git a/README.md b/README.md
index 0f7db98..a219fef 100644
--- a/README.md
+++ b/README.md
@@ -188,30 +188,31 @@ The data compression feature is not compatible with all vertx-rest-storage featu
The following configuration values are available:
-| Property | Type | Default value | Description |
-|:----------------------------------------|:-------|:-------------------------|:--------------------------------------------------------------------------------------------------------------------------------------|
-| root | common | . | The prefix for the directory or redis key |
-| storageType | common | filesystem | The storage implementation to use. Choose between filesystem or redis |
-| port | common | 8989 | The port the mod listens to when HTTP API is enabled. |
-| httpRequestHandlerEnabled | common | true | When set to _false_, the storage is accessible throught the event bus only. |
-| httpRequestHandlerAuthenticationEnabled | common | false | Enable / disable authentication for the HTTP API |
-| httpRequestHandlerUsername | common | | The username for the HTTP API authentication |
-| httpRequestHandlerPassword | common | | The password for the HTTP API authentication |
-| prefix | common | / | The part of the URL path before this handler (aka "context path" in JEE terminology) |
-| storageAddress | common | resource-storage | The eventbus address the mod listens to. |
-| editorConfig | common | | Additional configuration values for the editor |
-| confirmCollectionDelete | common | false | When set to _true_, an additional _recursive=true_ url parameter has to be set to delete collections |
-| redisHost | redis | localhost | The host where redis is running on |
-| redisPort | redis | 6379 | The port where redis is running on |
-| expirablePrefix | redis | rest-storage:expirable | The prefix for expirable data redis keys |
-| resourcesPrefix | redis | rest-storage:resources | The prefix for resources redis keys |
-| collectionsPrefix | redis | rest-storage:collections | The prefix for collections redis keys |
-| deltaResourcesPrefix | redis | delta:resources | The prefix for delta resources redis keys |
-| deltaEtagsPrefix | redis | delta:etags | The prefix for delta etags redis keys |
-| lockPrefix | redis | rest-storage:locks | The prefix for lock redis keys |
-| resourceCleanupAmount | redis | 100000 | The maximum amount of resources to clean in a single cleanup run |
-| rejectStorageWriteOnLowMemory | redis | false | When set to _true_, PUT requests with the x-importance-level header can be rejected when memory gets low |
-| freeMemoryCheckIntervalMs | redis | 60000 | The interval in milliseconds to calculate the actual memory usage |
+| Property | Type | Default value | Description |
+|:----------------------------------------|:-------|:-------------------------|:------------------------------------------------------------------------------------------------------------------------------|
+| root | common | . | The prefix for the directory or redis key |
+| storageType | common | filesystem | The storage implementation to use. Choose between filesystem or redis |
+| port | common | 8989 | The port the mod listens to when HTTP API is enabled. |
+| httpRequestHandlerEnabled | common | true | When set to _false_, the storage is accessible throught the event bus only. |
+| httpRequestHandlerAuthenticationEnabled | common | false | Enable / disable authentication for the HTTP API |
+| httpRequestHandlerUsername | common | | The username for the HTTP API authentication |
+| httpRequestHandlerPassword | common | | The password for the HTTP API authentication |
+| prefix | common | / | The part of the URL path before this handler (aka "context path" in JEE terminology) |
+| storageAddress | common | resource-storage | The eventbus address the mod listens to. |
+| editorConfig | common | | Additional configuration values for the editor |
+| confirmCollectionDelete | common | false | When set to _true_, an additional _recursive=true_ url parameter has to be set to delete collections |
+| redisHost | redis | localhost | The host where redis is running on |
+| redisPort | redis | 6379 | The port where redis is running on |
+| expirablePrefix | redis | rest-storage:expirable | The prefix for expirable data redis keys |
+| resourcesPrefix | redis | rest-storage:resources | The prefix for resources redis keys |
+| collectionsPrefix | redis | rest-storage:collections | The prefix for collections redis keys |
+| deltaResourcesPrefix | redis | delta:resources | The prefix for delta resources redis keys |
+| deltaEtagsPrefix | redis | delta:etags | The prefix for delta etags redis keys |
+| lockPrefix | redis | rest-storage:locks | The prefix for lock redis keys |
+| resourceCleanupAmount | redis | 100000 | The maximum amount of resources to clean in a single cleanup run |
+| resourceCleanupIntervalSec | redis | | The interval (in seconds) how often to peform the storage cleanup. When set to _null_ no periodic storage cleanup is peformed |
+| rejectStorageWriteOnLowMemory | redis | false | When set to _true_, PUT requests with the x-importance-level header can be rejected when memory gets low |
+| freeMemoryCheckIntervalMs | redis | 60000 | The interval in milliseconds to calculate the actual memory usage |
### Configuration util
diff --git a/pom.xml b/pom.xml
index c913215..5f7dedf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.swisspush
rest-storage
- 3.0.15-SNAPSHOT
+ 3.0.16-SNAPSHOT
rest-storage
Persistence for REST resources in the filesystem or a redis database
@@ -415,7 +415,7 @@
1.10.19
4.3.0
1.6.5
- 2.9.0
+ 3.7.0
UTF8
https://oss.sonatype.org/content/repositories/snapshots/
diff --git a/src/main/java/org/swisspush/reststorage/DefaultRedisProvider.java b/src/main/java/org/swisspush/reststorage/DefaultRedisProvider.java
index fda415d..1167a27 100644
--- a/src/main/java/org/swisspush/reststorage/DefaultRedisProvider.java
+++ b/src/main/java/org/swisspush/reststorage/DefaultRedisProvider.java
@@ -6,6 +6,8 @@
import io.vertx.redis.client.Redis;
import io.vertx.redis.client.RedisAPI;
import io.vertx.redis.client.RedisOptions;
+import org.apache.commons.lang.StringUtils;
+import org.swisspush.reststorage.redis.RedisProvider;
import org.swisspush.reststorage.util.ModuleConfiguration;
import java.util.concurrent.atomic.AtomicReference;
@@ -19,7 +21,7 @@ public class DefaultRedisProvider implements RedisProvider {
private final Vertx vertx;
- private ModuleConfiguration configuration;
+ private final ModuleConfiguration configuration;
private RedisAPI redisAPI;
@@ -62,9 +64,8 @@ private Future setupRedisClient(){
private Future connectToRedis() {
Promise promise = Promise.promise();
- String protocol = configuration.isRedisEnableTls() ? "rediss://" : "redis://";
Redis.createClient(vertx, new RedisOptions()
- .setConnectionString(protocol + configuration.getRedisHost() + ":" + configuration.getRedisPort())
+ .setConnectionString(createConnectString())
.setPassword((configuration.getRedisAuth() == null ? "" : configuration.getRedisAuth()))
.setMaxPoolSize(configuration.getMaxRedisConnectionPoolSize())
.setMaxPoolWaiting(configuration.getMaxQueueWaiting())
@@ -79,4 +80,16 @@ private Future connectToRedis() {
return promise.future();
}
+
+ private String createConnectString() {
+ StringBuilder connectionStringBuilder = new StringBuilder();
+ connectionStringBuilder.append(configuration.isRedisEnableTls() ? "rediss://" : "redis://");
+ String redisUser = configuration.getRedisUser();
+ String redisPassword = configuration.getRedisPassword();
+ if (StringUtils.isNotEmpty(redisUser) && StringUtils.isNotEmpty(redisPassword)) {
+ connectionStringBuilder.append(configuration.getRedisUser()).append(":").append(redisPassword).append("@");
+ }
+ connectionStringBuilder.append(configuration.getRedisHost()).append(":").append(configuration.getRedisPort());
+ return connectionStringBuilder.toString();
+ }
}
diff --git a/src/main/java/org/swisspush/reststorage/FileSystemStorage.java b/src/main/java/org/swisspush/reststorage/FileSystemStorage.java
index 22e9d2d..7e213c6 100644
--- a/src/main/java/org/swisspush/reststorage/FileSystemStorage.java
+++ b/src/main/java/org/swisspush/reststorage/FileSystemStorage.java
@@ -27,7 +27,7 @@ public class FileSystemStorage implements Storage {
private final int rootLen;
private final FileSystemDirLister fileSystemDirLister;
- private Logger log = LoggerFactory.getLogger(FileSystemStorage.class);
+ private final Logger log = LoggerFactory.getLogger(FileSystemStorage.class);
public FileSystemStorage(Vertx vertx, String root) {
this.vertx = vertx;
diff --git a/src/main/java/org/swisspush/reststorage/MimeTypeResolver.java b/src/main/java/org/swisspush/reststorage/MimeTypeResolver.java
index 1f02414..9f208f2 100644
--- a/src/main/java/org/swisspush/reststorage/MimeTypeResolver.java
+++ b/src/main/java/org/swisspush/reststorage/MimeTypeResolver.java
@@ -10,7 +10,7 @@ public class MimeTypeResolver {
private Map mimeTypes = new HashMap<>();
- private String defaultMimeType;
+ private final String defaultMimeType;
public MimeTypeResolver(String defaultMimeType) {
this.defaultMimeType = defaultMimeType;
diff --git a/src/main/java/org/swisspush/reststorage/ModuleConfigurationAuthentication.java b/src/main/java/org/swisspush/reststorage/ModuleConfigurationAuthentication.java
index a7dd47b..fcc7849 100644
--- a/src/main/java/org/swisspush/reststorage/ModuleConfigurationAuthentication.java
+++ b/src/main/java/org/swisspush/reststorage/ModuleConfigurationAuthentication.java
@@ -34,7 +34,7 @@ private User(String name, String password) {
}
}
- private User user;
+ private final User user;
public ModuleConfigurationAuthentication(ModuleConfiguration configuration) {
Objects.requireNonNull(configuration);
diff --git a/src/main/java/org/swisspush/reststorage/RedisRestStorageRunner.java b/src/main/java/org/swisspush/reststorage/RedisRestStorageRunner.java
index d294dcc..7304a33 100644
--- a/src/main/java/org/swisspush/reststorage/RedisRestStorageRunner.java
+++ b/src/main/java/org/swisspush/reststorage/RedisRestStorageRunner.java
@@ -14,7 +14,8 @@ public class RedisRestStorageRunner {
public static void main(String[] args) {
ModuleConfiguration modConfig = new ModuleConfiguration()
- .storageType(ModuleConfiguration.StorageType.redis);
+ .storageType(ModuleConfiguration.StorageType.redis)
+ .resourceCleanupIntervalSec(10);
Vertx.vertx().deployVerticle(new RestStorageMod(), new DeploymentOptions().setConfig(modConfig.asJsonObject()), event ->
LoggerFactory.getLogger(RedisRestStorageRunner.class).info("rest-storage started"));
diff --git a/src/main/java/org/swisspush/reststorage/RestStorageMod.java b/src/main/java/org/swisspush/reststorage/RestStorageMod.java
index dda25eb..ae3a6bc 100644
--- a/src/main/java/org/swisspush/reststorage/RestStorageMod.java
+++ b/src/main/java/org/swisspush/reststorage/RestStorageMod.java
@@ -5,11 +5,13 @@
import io.vertx.core.http.HttpServerRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.swisspush.reststorage.redis.RedisProvider;
+import org.swisspush.reststorage.redis.RedisStorage;
import org.swisspush.reststorage.util.ModuleConfiguration;
public class RestStorageMod extends AbstractVerticle {
- private Logger log = LoggerFactory.getLogger(RestStorageMod.class);
+ private final Logger log = LoggerFactory.getLogger(RestStorageMod.class);
private RedisProvider redisProvider;
diff --git a/src/main/java/org/swisspush/reststorage/lock/Lock.java b/src/main/java/org/swisspush/reststorage/lock/Lock.java
new file mode 100644
index 0000000..aef427b
--- /dev/null
+++ b/src/main/java/org/swisspush/reststorage/lock/Lock.java
@@ -0,0 +1,36 @@
+package org.swisspush.reststorage.lock;
+
+import io.vertx.core.Future;
+
+/**
+ * Cluster wide locks allow you to obtain exclusive locks across the cluster.
+ * This is useful when you want to do something or access a resource on only one node of a cluster at any one time.
+ *
+ * @author https://github.com/mcweba [Marc-Andre Weber]
+ */
+public interface Lock {
+ /**
+ * Try to acquire a lock.
+ * The token
parameter value must be unique across all clients and all lock requests. The lockExpiryMs
+ * parameter defines the expiry of the lock.
+ * When not manually released, the lock will be released automatically when expired.
+ *
+ * @param lock The name of the lock to acquire
+ * @param token A unique token to define the owner of the lock
+ * @param lockExpiryMs The lock expiry in milliseconds
+ * @return Returns a Future holding a Boolean value whether the lock could be successfully acquired or not
+ */
+ Future acquireLock(String lock, String token, long lockExpiryMs);
+
+ /**
+ * Try to release a lock.
+ * The token
parameter value is used to verify that only the owner of the lock can release it.
+ * The token
parameter value also prevents the original owner of an already expired lock to release a lock
+ * which has been acquired by another client.
+ *
+ * @param lock The name of the lock to release
+ * @param token A unique token to verify if the owner of the lock tries to release the lock
+ * @return Returns a Promise holding a Boolean value whether the lock could be successfully released or not
+ */
+ Future releaseLock(String lock, String token);
+}
diff --git a/src/main/java/org/swisspush/reststorage/lock/impl/RedisBasedLock.java b/src/main/java/org/swisspush/reststorage/lock/impl/RedisBasedLock.java
new file mode 100644
index 0000000..e8609cf
--- /dev/null
+++ b/src/main/java/org/swisspush/reststorage/lock/impl/RedisBasedLock.java
@@ -0,0 +1,83 @@
+package org.swisspush.reststorage.lock.impl;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Promise;
+import io.vertx.core.json.JsonArray;
+import io.vertx.redis.client.Command;
+import io.vertx.redis.client.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.swisspush.reststorage.redis.RedisProvider;
+import org.swisspush.reststorage.lock.Lock;
+import org.swisspush.reststorage.lock.lua.LockLuaScripts;
+import org.swisspush.reststorage.lock.lua.LuaScriptState;
+import org.swisspush.reststorage.lock.lua.ReleaseLockRedisCommand;
+import org.swisspush.reststorage.redis.RedisUtils;
+import org.swisspush.reststorage.util.FailedAsyncResult;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of the {@link Lock} interface based on a redis database.
+ *
+ * @author https://github.com/mcweba [Marc-Andre Weber]
+ */
+public class RedisBasedLock implements Lock {
+
+ private final Logger log = LoggerFactory.getLogger(RedisBasedLock.class);
+
+ public static final String STORAGE_PREFIX = "rest-storage-lock:";
+
+ private final LuaScriptState releaseLockLuaScriptState;
+ private final RedisProvider redisProvider;
+
+ public RedisBasedLock(RedisProvider redisProvider) {
+ this.redisProvider = redisProvider;
+ this.releaseLockLuaScriptState = new LuaScriptState(LockLuaScripts.LOCK_RELEASE, redisProvider, false);
+ }
+
+ private void redisSetWithOptions(String key, String value, boolean nx, long px, Handler> handler) {
+ JsonArray options = new JsonArray();
+ options.add("PX").add(px);
+ if (nx) {
+ options.add("NX");
+ }
+ redisProvider.redis().onSuccess(redisAPI -> redisAPI.send(Command.SET, RedisUtils.toPayload(key, value, options).toArray(new String[0]))
+ .onComplete(handler)).onFailure(throwable -> handler.handle(new FailedAsyncResult<>(throwable)));
+ }
+
+ @Override
+ public Future acquireLock(String lock, String token, long lockExpiryMs) {
+ Promise promise = Promise.promise();
+ redisSetWithOptions(buildLockKey(lock), token, true, lockExpiryMs, event -> {
+ if (event.succeeded()) {
+ if (event.result() != null) {
+ promise.complete("OK".equalsIgnoreCase(event.result().toString()));
+ } else {
+ promise.complete(false);
+ }
+ } else {
+ promise.fail(event.cause().getMessage());
+ }
+ });
+ return promise.future();
+ }
+
+ @Override
+ public Future releaseLock(String lock, String token) {
+ Promise promise = Promise.promise();
+ List keys = Collections.singletonList(buildLockKey(lock));
+ List arguments = Collections.singletonList(token);
+ ReleaseLockRedisCommand cmd = new ReleaseLockRedisCommand(releaseLockLuaScriptState,
+ keys, arguments, redisProvider, log, promise);
+ cmd.exec(0);
+ return promise.future();
+ }
+
+ private String buildLockKey(String lock) {
+ return STORAGE_PREFIX + lock;
+ }
+}
diff --git a/src/main/java/org/swisspush/reststorage/lock/lua/LockLuaScripts.java b/src/main/java/org/swisspush/reststorage/lock/lua/LockLuaScripts.java
new file mode 100644
index 0000000..eb0d776
--- /dev/null
+++ b/src/main/java/org/swisspush/reststorage/lock/lua/LockLuaScripts.java
@@ -0,0 +1,21 @@
+package org.swisspush.reststorage.lock.lua;
+
+
+/**
+ * @author https://github.com/mcweba [Marc-Andre Weber]
+ */
+public enum LockLuaScripts implements LuaScript {
+
+ LOCK_RELEASE("lock_release.lua");
+
+ private final String file;
+
+ LockLuaScripts(String file) {
+ this.file = file;
+ }
+
+ @Override
+ public String getFilename() {
+ return file;
+ }
+}
diff --git a/src/main/java/org/swisspush/reststorage/lock/lua/LuaScript.java b/src/main/java/org/swisspush/reststorage/lock/lua/LuaScript.java
new file mode 100644
index 0000000..acf5b8e
--- /dev/null
+++ b/src/main/java/org/swisspush/reststorage/lock/lua/LuaScript.java
@@ -0,0 +1,8 @@
+package org.swisspush.reststorage.lock.lua;
+
+/**
+ * @author https://github.com/mcweba [Marc-Andre Weber]
+ */
+public interface LuaScript {
+ String getFilename();
+}
diff --git a/src/main/java/org/swisspush/reststorage/lock/lua/LuaScriptState.java b/src/main/java/org/swisspush/reststorage/lock/lua/LuaScriptState.java
new file mode 100644
index 0000000..b80da46
--- /dev/null
+++ b/src/main/java/org/swisspush/reststorage/lock/lua/LuaScriptState.java
@@ -0,0 +1,140 @@
+package org.swisspush.reststorage.lock.lua;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.swisspush.reststorage.redis.RedisProvider;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+
+/**
+ * Created by webermarca on 01.07.2016.
+ */
+public class LuaScriptState {
+ private final LuaScript luaScriptType;
+ /**
+ * the script itself
+ */
+ private String script;
+ /**
+ * if the script logs to the redis log
+ */
+ private boolean logoutput = false;
+ /**
+ * the sha, over which the script can be accessed in redis
+ */
+ private String sha;
+
+ private final RedisProvider redisProvider;
+
+ private final Logger log = LoggerFactory.getLogger(LuaScriptState.class);
+
+ public LuaScriptState(LuaScript luaScriptType, RedisProvider redisProvider, boolean logoutput) {
+ this.luaScriptType = luaScriptType;
+ this.redisProvider = redisProvider;
+ this.logoutput = logoutput;
+ this.composeLuaScript(luaScriptType);
+ this.loadLuaScript(new RedisCommandDoNothing(), 0);
+ }
+
+ /**
+ * Reads the script from the classpath and removes logging output if logoutput is false.
+ * The script is stored in the class member script.
+ *
+ * @param luaScriptType
+ */
+ private void composeLuaScript(LuaScript luaScriptType) {
+ log.info("read the lua script for script type: {} with logoutput: {}", luaScriptType, logoutput);
+ this.script = readLuaScriptFromClasspath(luaScriptType);
+ this.sha = DigestUtils.sha1Hex(this.script);
+ }
+
+ private String readLuaScriptFromClasspath(LuaScript luaScriptType) {
+ BufferedReader in = new BufferedReader(new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream(luaScriptType.getFilename())));
+ StringBuilder sb;
+ try {
+ sb = new StringBuilder();
+ String line;
+ while ((line = in.readLine()) != null) {
+ if (!logoutput && line.contains("redis.log(redis.LOG_NOTICE,")) {
+ continue;
+ }
+ sb.append(line).append("\n");
+ }
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ try {
+ in.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Load the get script into redis and store the sha in the class member sha.
+ *
+ * @param redisCommand the redis command that should be executed, after the script is loaded.
+ * @param executionCounter a counter to control recursion depth
+ */
+ public void loadLuaScript(final RedisCommand redisCommand, int executionCounter) {
+ final int executionCounterIncr = ++executionCounter;
+ // check first if the lua script already exists in the store
+ redisProvider.redis().onSuccess(redisAPI -> redisAPI.script(Arrays.asList("exists", sha), resultArray -> {
+ if (resultArray.failed()) {
+ log.error("Error checking whether lua script exists", resultArray.cause());
+ return;
+ }
+ Long exists = resultArray.result().get(0).toLong();
+ // if script already
+ if (Long.valueOf(1).equals(exists)) {
+ log.debug("RedisStorage script already exists in redis cache: {}", luaScriptType);
+ redisCommand.exec(executionCounterIncr);
+ } else {
+ log.info("load lua script for script type: {} logutput: {}", luaScriptType, logoutput);
+ redisAPI.script(Arrays.asList("load",script), stringAsyncResult -> {
+ String newSha = stringAsyncResult.result().toString();
+ log.info("got sha from redis for lua script: {}: {}", luaScriptType, newSha);
+ if (!newSha.equals(sha)) {
+ log.warn("the sha calculated by myself: {} doesn't match with the sha from redis: {}. " +
+ "We use the sha from redis", sha, newSha);
+ }
+ sha = newSha;
+ log.info("execute redis command for script type: {} with new sha: {}", luaScriptType, sha);
+ redisCommand.exec(executionCounterIncr);
+ });
+ }
+ })).onFailure(throwable -> log.error("Redis: Error checking whether lua script exists", throwable));
+ }
+
+ public String getScript() {
+ return script;
+ }
+
+ public void setScript(String script) {
+ this.script = script;
+ }
+
+ public boolean getLogoutput() {
+ return logoutput;
+ }
+
+ public void setLogoutput(boolean logoutput) {
+ this.logoutput = logoutput;
+ }
+
+ public String getSha() {
+ return sha;
+ }
+
+ public void setSha(String sha) {
+ this.sha = sha;
+ }
+
+}
diff --git a/src/main/java/org/swisspush/reststorage/lock/lua/RedisCommand.java b/src/main/java/org/swisspush/reststorage/lock/lua/RedisCommand.java
new file mode 100644
index 0000000..67d14dc
--- /dev/null
+++ b/src/main/java/org/swisspush/reststorage/lock/lua/RedisCommand.java
@@ -0,0 +1,8 @@
+package org.swisspush.reststorage.lock.lua;
+
+/**
+ * @author https://github.com/mcweba [Marc-Andre Weber]
+ */
+public interface RedisCommand {
+ void exec(int executionCounter);
+}
diff --git a/src/main/java/org/swisspush/reststorage/lock/lua/RedisCommandDoNothing.java b/src/main/java/org/swisspush/reststorage/lock/lua/RedisCommandDoNothing.java
new file mode 100644
index 0000000..1f5da71
--- /dev/null
+++ b/src/main/java/org/swisspush/reststorage/lock/lua/RedisCommandDoNothing.java
@@ -0,0 +1,12 @@
+package org.swisspush.reststorage.lock.lua;
+
+/**
+ * @author https://github.com/mcweba [Marc-Andre Weber]
+ */
+public class RedisCommandDoNothing implements RedisCommand{
+
+ @Override
+ public void exec(int executionCounter) {
+ // do nothing here
+ }
+}
diff --git a/src/main/java/org/swisspush/reststorage/lock/lua/ReleaseLockRedisCommand.java b/src/main/java/org/swisspush/reststorage/lock/lua/ReleaseLockRedisCommand.java
new file mode 100644
index 0000000..d939ebc
--- /dev/null
+++ b/src/main/java/org/swisspush/reststorage/lock/lua/ReleaseLockRedisCommand.java
@@ -0,0 +1,61 @@
+package org.swisspush.reststorage.lock.lua;
+
+import io.vertx.core.Promise;
+import org.slf4j.Logger;
+import org.swisspush.reststorage.redis.RedisProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author https://github.com/mcweba [Marc-Andre Weber]
+ */
+public class ReleaseLockRedisCommand implements RedisCommand {
+
+ private final LuaScriptState luaScriptState;
+ private final List keys;
+ private final List arguments;
+ private final Promise promise;
+ private final RedisProvider redisProvider;
+ private final Logger log;
+
+ public ReleaseLockRedisCommand(LuaScriptState luaScriptState, List keys, List arguments,
+ RedisProvider redisProvider, Logger log, final Promise promise) {
+ this.luaScriptState = luaScriptState;
+ this.keys = keys;
+ this.arguments = arguments;
+ this.redisProvider = redisProvider;
+ this.log = log;
+ this.promise = promise;
+ }
+
+ @Override
+ public void exec(int executionCounter) {
+ List args = new ArrayList<>();
+ args.add(luaScriptState.getSha());
+ args.add(String.valueOf(keys.size()));
+ args.addAll(keys);
+ args.addAll(arguments);
+
+ redisProvider.redis().onSuccess(redisAPI -> redisAPI.evalsha(args, event -> {
+ if (event.succeeded()) {
+ Long unlocked = event.result().toLong();
+ promise.complete(unlocked > 0);
+ } else {
+ String message = event.cause().getMessage();
+ if (message != null && message.startsWith("NOSCRIPT")) {
+ log.warn("ReleaseLockRedisCommand script couldn't be found, reload it");
+ log.warn("amount the script got loaded: " + executionCounter);
+ if (executionCounter > 10) {
+ promise.fail("amount the script got loaded is higher than 10, we abort");
+ } else {
+ luaScriptState.loadLuaScript(new ReleaseLockRedisCommand(luaScriptState, keys,
+ arguments, redisProvider, log, promise), executionCounter);
+ }
+ } else {
+ promise.fail("ReleaseLockRedisCommand request failed with message: " + message);
+ }
+ }
+ })).onFailure(throwable -> promise.fail("Redis: ReleaseLockRedisCommand request failed with error: " + throwable.getMessage()));
+ }
+}
diff --git a/src/main/java/org/swisspush/reststorage/RedisProvider.java b/src/main/java/org/swisspush/reststorage/redis/RedisProvider.java
similarity index 85%
rename from src/main/java/org/swisspush/reststorage/RedisProvider.java
rename to src/main/java/org/swisspush/reststorage/redis/RedisProvider.java
index df6240f..2c9ea6b 100644
--- a/src/main/java/org/swisspush/reststorage/RedisProvider.java
+++ b/src/main/java/org/swisspush/reststorage/redis/RedisProvider.java
@@ -1,4 +1,4 @@
-package org.swisspush.reststorage;
+package org.swisspush.reststorage.redis;
import io.vertx.core.Future;
import io.vertx.redis.client.RedisAPI;
diff --git a/src/main/java/org/swisspush/reststorage/RedisStorage.java b/src/main/java/org/swisspush/reststorage/redis/RedisStorage.java
similarity index 93%
rename from src/main/java/org/swisspush/reststorage/RedisStorage.java
rename to src/main/java/org/swisspush/reststorage/redis/RedisStorage.java
index dec2c3d..99009c6 100644
--- a/src/main/java/org/swisspush/reststorage/RedisStorage.java
+++ b/src/main/java/org/swisspush/reststorage/redis/RedisStorage.java
@@ -1,1194 +1,1247 @@
-package org.swisspush.reststorage;
-
-import io.vertx.core.*;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.json.DecodeException;
-import io.vertx.core.json.JsonArray;
-import io.vertx.core.json.JsonObject;
-import io.vertx.core.streams.ReadStream;
-import io.vertx.core.streams.WriteStream;
-import io.vertx.redis.client.Response;
-import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.lang.text.StrSubstitutor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.swisspush.reststorage.util.GZIPUtil;
-import org.swisspush.reststorage.util.LockMode;
-import org.swisspush.reststorage.util.ModuleConfiguration;
-import org.swisspush.reststorage.util.ResourceNameUtil;
-
-import java.io.*;
-import java.nio.charset.StandardCharsets;
-import java.text.DecimalFormat;
-import java.util.*;
-import java.util.stream.Stream;
-
-public class RedisStorage implements Storage {
-
- private Logger log = LoggerFactory.getLogger(RedisStorage.class);
-
- // set to very high value = Wed Nov 16 5138 09:46:39
- private static final String MAX_EXPIRE_IN_MILLIS = "99999999999999";
- private final String EMPTY = "";
- private static final float MAX_PERCENTAGE = 100.0f;
- private static final float MIN_PERCENTAGE = 0.0f;
- private static final int CLEANUP_BULK_SIZE = 200;
-
- private String redisResourcesPrefix;
- private String redisCollectionsPrefix;
- private String redisDeltaResourcesPrefix;
- private String redisDeltaEtagsPrefix;
- private String expirableSet;
- private long cleanupResourcesAmount;
- private String redisLockPrefix;
- private Vertx vertx;
-
- private RedisProvider redisProvider;
- private Map luaScripts = new HashMap<>();
- private DecimalFormat decimalFormat;
-
- private Optional currentMemoryUsageOptional = Optional.empty();
-
- public RedisStorage(Vertx vertx, ModuleConfiguration config, RedisProvider redisProvider) {
- this.expirableSet = config.getExpirablePrefix();
- this.redisResourcesPrefix = config.getResourcesPrefix();
- this.redisCollectionsPrefix = config.getCollectionsPrefix();
- this.redisDeltaResourcesPrefix = config.getDeltaResourcesPrefix();
- this.redisDeltaEtagsPrefix = config.getDeltaEtagsPrefix();
- this.cleanupResourcesAmount = config.getResourceCleanupAmount();
- this.redisLockPrefix = config.getLockPrefix();
-
- this.vertx = vertx;
- this.redisProvider = redisProvider;
- this.decimalFormat = new DecimalFormat();
- this.decimalFormat.setMaximumFractionDigits(1);
-
- // load all the lua scripts
- LuaScriptState luaGetScriptState = new LuaScriptState(LuaScript.GET, false);
- luaGetScriptState.loadLuaScript(new RedisCommandDoNothing(), 0);
- luaScripts.put(LuaScript.GET, luaGetScriptState);
-
- LuaScriptState luaStorageExpandScriptState = new LuaScriptState(LuaScript.STORAGE_EXPAND, false);
- luaStorageExpandScriptState.loadLuaScript(new RedisCommandDoNothing(), 0);
- luaScripts.put(LuaScript.STORAGE_EXPAND, luaStorageExpandScriptState);
-
- LuaScriptState luaPutScriptState = new LuaScriptState(LuaScript.PUT, false);
- luaPutScriptState.loadLuaScript(new RedisCommandDoNothing(), 0);
- luaScripts.put(LuaScript.PUT, luaPutScriptState);
-
- LuaScriptState luaDeleteScriptState = new LuaScriptState(LuaScript.DELETE, false);
- luaDeleteScriptState.loadLuaScript(new RedisCommandDoNothing(), 0);
- luaScripts.put(LuaScript.DELETE, luaDeleteScriptState);
-
- LuaScriptState luaCleanupScriptState = new LuaScriptState(LuaScript.CLEANUP, false);
- luaCleanupScriptState.loadLuaScript(new RedisCommandDoNothing(), 0);
- luaScripts.put(LuaScript.CLEANUP, luaCleanupScriptState);
-
- if (config.isRejectStorageWriteOnLowMemory()) {
- calculateCurrentMemoryUsage().onComplete(optionalAsyncResult -> currentMemoryUsageOptional = optionalAsyncResult.result());
- startPeriodicMemoryUsageUpdate(config.getFreeMemoryCheckIntervalMs());
- }
- }
-
- private void startPeriodicMemoryUsageUpdate(long intervalMs) {
- vertx.setPeriodic(intervalMs, updateMemoryUsage -> calculateCurrentMemoryUsage().onComplete(optionalAsyncResult -> currentMemoryUsageOptional = optionalAsyncResult.result()));
- }
-
- public Future> calculateCurrentMemoryUsage() {
- Promise> promise = Promise.promise();
-
- redisProvider.redis().onSuccess(redisAPI -> redisAPI.info(Collections.singletonList("memory"), memoryInfo -> {
- if (memoryInfo.failed()) {
- log.error("Unable to get memory information from redis", memoryInfo.cause());
- promise.complete(Optional.empty());
- return;
- }
-
- long totalSystemMemory;
- try {
- Optional totalSystemMemoryOpt = memoryInfo.result().toString()
- .lines()
- .filter(source -> source.startsWith("total_system_memory:"))
- .findAny();
- if (totalSystemMemoryOpt.isEmpty()) {
- log.warn("No 'total_system_memory' section received from redis. Unable to calculate the current memory usage");
- promise.complete(Optional.empty());
- return;
- }
- totalSystemMemory = Long.parseLong(totalSystemMemoryOpt.get().split(":")[1]);
- if (totalSystemMemory == 0L) {
- log.warn("'total_system_memory' value 0 received from redis. Unable to calculate the current memory usage");
- promise.complete(Optional.empty());
- return;
- }
-
- } catch (NumberFormatException ex) {
- logPropertyWarning("total_system_memory", ex);
- promise.complete(Optional.empty());
- return;
- }
-
- long usedMemory;
- try {
- Optional usedMemoryOpt = memoryInfo.result().toString()
- .lines()
- .filter(source -> source.startsWith("used_memory:"))
- .findAny();
- if (usedMemoryOpt.isEmpty()) {
- log.warn("No 'used_memory' section received from redis. Unable to calculate the current memory usage");
- promise.complete(Optional.empty());
- return;
- }
- usedMemory = Long.parseLong(usedMemoryOpt.get().split(":")[1]);
- } catch (NumberFormatException ex) {
- logPropertyWarning("used_memory", ex);
- promise.complete(Optional.empty());
- return;
- }
-
- float currentMemoryUsagePercentage = ((float) usedMemory / totalSystemMemory) * 100;
- if (currentMemoryUsagePercentage > MAX_PERCENTAGE) {
- currentMemoryUsagePercentage = MAX_PERCENTAGE;
- } else if (currentMemoryUsagePercentage < MIN_PERCENTAGE) {
- currentMemoryUsagePercentage = MIN_PERCENTAGE;
- }
- log.info("Current memory usage is {}%", decimalFormat.format(currentMemoryUsagePercentage));
- promise.complete(Optional.of(currentMemoryUsagePercentage));
- }))
- .onFailure(event -> {
- log.error("Unable to get memory information from redis", event);
- promise.complete(Optional.empty());
- });
- return promise.future();
- }
-
- private void logPropertyWarning(String property, Exception ex) {
- log.warn("No or invalid '{}' value received from redis. Unable to calculate the current memory usage. " +
- "Exception: {}", property, ex.toString());
- }
-
- private enum LuaScript {
- GET("get.lua"), STORAGE_EXPAND("storageExpand.lua"), PUT("put.lua"), DELETE("del.lua"), CLEANUP("cleanup.lua");
-
- private String file;
-
- LuaScript(String file) {
- this.file = file;
- }
-
- public String getFile() {
- return file;
- }
- }
-
- /**
- * Holds the state of a lua script.
- */
- private class LuaScriptState {
-
- private LuaScript luaScriptType;
- /**
- * the script itself
- */
- private String script;
- /**
- * if the script logs to the redis log
- */
- private boolean logoutput = false;
- /**
- * the sha, over which the script can be accessed in redis
- */
- private String sha;
-
- private LuaScriptState(LuaScript luaScriptType, boolean logoutput) {
- this.luaScriptType = luaScriptType;
- this.logoutput = logoutput;
- this.composeLuaScript(luaScriptType);
- this.loadLuaScript(new RedisCommandDoNothing(), 0);
- }
-
- /**
- * Reads the script from the classpath and removes logging output if logoutput is false.
- * The script is stored in the class member script.
- *
- * @param luaScriptType
- */
- private void composeLuaScript(LuaScript luaScriptType) {
- log.info("read the lua script for script type: {} with logoutput: {}", luaScriptType, logoutput);
-
- // It is not possible to evalsha or eval inside lua scripts,
- // so we wrap the cleanupscript around the deletescript manually to avoid code duplication.
- // we have to comment the return, so that the cleanup script doesn't terminate
- if (LuaScript.CLEANUP.equals(luaScriptType)) {
- Map values = new HashMap<>();
- values.put("delscript", readLuaScriptFromClasspath(LuaScript.DELETE).replaceAll("return", "--return"));
- StrSubstitutor sub = new StrSubstitutor(values, "--%(", ")");
- this.script = sub.replace(readLuaScriptFromClasspath(LuaScript.CLEANUP));
- } else {
- this.script = readLuaScriptFromClasspath(luaScriptType);
- }
- this.sha = DigestUtils.sha1Hex(this.script);
- }
-
- private String readLuaScriptFromClasspath(LuaScript luaScriptType) {
- BufferedReader in = new BufferedReader(new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream(luaScriptType.getFile())));
- StringBuilder sb;
- try {
- sb = new StringBuilder();
- String line;
- while ((line = in.readLine()) != null) {
- if (!logoutput && line.contains("redis.log(redis.LOG_NOTICE,")) {
- continue;
- }
- sb.append(line).append("\n");
- }
-
- } catch (IOException e) {
- throw new RuntimeException(e);
- } finally {
- try {
- in.close();
- } catch (IOException e) {
- // Ignore
- }
- }
- return sb.toString();
- }
-
- /**
- * Rereads the lua script, eg. if the loglevel changed.
- */
- public void recomposeLuaScript() {
- this.composeLuaScript(luaScriptType);
- }
-
- /**
- * Load the get script into redis and store the sha in the class member sha.
- *
- * @param redisCommand the redis command that should be executed, after the script is loaded.
- * @param executionCounter a counter to control recursion depth
- */
- public void loadLuaScript(final RedisCommand redisCommand, int executionCounter) {
- final int executionCounterIncr = ++executionCounter;
-
- redisProvider.redis().onSuccess(redisAPI -> {
- // check first if the lua script already exists in the store
- redisAPI.script(Arrays.asList("exists", sha), resultArray -> {
- if (resultArray.failed()) {
- log.error("Error checking whether lua script exists", resultArray.cause());
- return;
- }
- Long exists = resultArray.result().get(0).toLong();
- // if script already
- if (Long.valueOf(1).equals(exists)) {
- log.debug("RedisStorage script already exists in redis cache: " + luaScriptType);
- redisCommand.exec(executionCounterIncr);
- } else {
- log.info("load lua script for script type: {} logoutput: {}", luaScriptType, logoutput);
- redisAPI.script(Arrays.asList("load", script), stringAsyncResult -> {
- if (stringAsyncResult.failed()) {
- log.error("Loading of lua script {} failed", luaScriptType);
- return;
- }
- String newSha = stringAsyncResult.result().toString();
- log.info("got sha from redis for lua script: {}: {}", luaScriptType, newSha);
- if (!newSha.equals(sha)) {
- log.warn("the sha calculated by myself: {} doesn't match with the sha from redis: {}. " +
- "We use the sha from redis", sha, newSha);
- }
- sha = newSha;
- log.info("execute redis command for script type: {} with new sha: {}", luaScriptType, sha);
- redisCommand.exec(executionCounterIncr);
- });
- }
- });
- })
- .onFailure(event -> log.error("Error checking whether lua script exists", event));
- }
-
- public String getScript() {
- return script;
- }
-
- public void setScript(String script) {
- this.script = script;
- }
-
- public boolean getLogoutput() {
- return logoutput;
- }
-
- public void setLogoutput(boolean logoutput) {
- this.logoutput = logoutput;
- }
-
- public String getSha() {
- return sha;
- }
-
- public void setSha(String sha) {
- this.sha = sha;
- }
- }
-
- /**
- * The interface for a redis command.
- */
- private interface RedisCommand {
- void exec(int executionCounter);
- }
-
- /**
- * A dummy that can be passed if no RedisCommand should be executed.
- */
- private static class RedisCommandDoNothing implements RedisCommand {
-
- @Override
- public void exec(int executionCounter) {
- // do nothing here
- }
- }
-
- /**
- * If the loglevel is trace and the logoutput in luaScriptState is false, then reload the script with logoutput and execute the RedisCommand.
- * If the loglevel is not trace and the logoutput in luaScriptState is true, then reload the script without logoutput and execute the RedisCommand.
- * If the loglevel is matching the luaScriptState, just execute the RedisCommand.
- *
- * @param luaScript the type of lua script
- * @param redisCommand the redis command to execute
- */
- private void reloadScriptIfLoglevelChangedAndExecuteRedisCommand(LuaScript luaScript, RedisCommand redisCommand, int executionCounter) {
- boolean logoutput = log.isTraceEnabled();
- LuaScriptState luaScriptState = luaScripts.get(luaScript);
- // if the loglevel didn't change, execute the command and return
- if (logoutput == luaScriptState.getLogoutput()) {
- redisCommand.exec(executionCounter);
- return;
- // if the loglevel changed, set the new loglevel into the luaScriptState, recompose the script and provide the redisCommand as parameter to execute
- } else if (logoutput && !luaScriptState.getLogoutput()) {
- luaScriptState.setLogoutput(true);
- luaScriptState.recomposeLuaScript();
-
- } else if (!logoutput && luaScriptState.getLogoutput()) {
- luaScriptState.setLogoutput(false);
- luaScriptState.recomposeLuaScript();
- }
- luaScriptState.loadLuaScript(redisCommand, executionCounter);
- }
-
- public class ByteArrayReadStream implements ReadStream {
-
- ByteArrayInputStream content;
- int size;
- boolean paused;
- int position;
- Handler endHandler;
- Handler handler;
-
- public ByteArrayReadStream(byte[] byteArray) {
- size = byteArray.length;
- content = new ByteArrayInputStream(byteArray);
- }
-
- private void doRead() {
- vertx.runOnContext(v -> {
- if (!paused) {
- if (position < size) {
- int toRead = 8192;
- if (position + toRead > size) {
- toRead = size - position;
- }
- byte[] bytes = new byte[toRead];
- content.read(bytes, 0, toRead);
- handler.handle(Buffer.buffer(bytes));
- position += toRead;
- doRead();
- } else {
- endHandler.handle(null);
- }
- }
- });
- }
-
- public ByteArrayReadStream resume() {
- paused = false;
- doRead();
- return this;
- }
-
- @Override
- public ReadStream fetch(long amount) {
- return null;
- }
-
- @Override
- public ByteArrayReadStream pause() {
- paused = true;
- return this;
- }
-
- @Override
- public ByteArrayReadStream exceptionHandler(Handler handler) {
- return this;
- }
-
- @Override
- public ReadStream handler(Handler handler) {
- this.handler = handler;
- doRead();
- return this;
- }
-
- @Override
- public ByteArrayReadStream endHandler(Handler endHandler) {
- this.endHandler = endHandler;
- return this;
- }
- }
-
- @Override
- public Optional getCurrentMemoryUsage() {
- return currentMemoryUsageOptional;
- }
-
- @Override
- public void get(String path, String etag, int offset, int limit, final Handler handler) {
- final String key = encodePath(path);
- List keys = Collections.singletonList(key);
- List arguments = Arrays.asList(
- redisResourcesPrefix,
- redisCollectionsPrefix,
- expirableSet,
- String.valueOf(System.currentTimeMillis()),
- MAX_EXPIRE_IN_MILLIS,
- String.valueOf(offset),
- String.valueOf(limit),
- etag
- );
- reloadScriptIfLoglevelChangedAndExecuteRedisCommand(LuaScript.GET, new Get(keys, arguments, handler), 0);
- }
-
- /**
- * The Get Command Execution.
- * If the get script cannot be found under the sha in luaScriptState, reload the script.
- * To avoid infinite recursion, we limit the recursion.
- */
- private class Get implements RedisCommand {
-
- private List keys;
- private List arguments;
- private Handler handler;
-
- public Get(List keys, List arguments, final Handler handler) {
- this.keys = keys;
- this.arguments = arguments;
- this.handler = handler;
- }
-
- public void exec(final int executionCounter) {
- List args = toPayload(luaScripts.get(LuaScript.GET).getSha(), keys.size(), keys, arguments);
- redisProvider.redis().onSuccess(redisAPI -> redisAPI.evalsha(args, event -> {
- if (event.succeeded()) {
- Response values = event.result();
- if (log.isTraceEnabled()) {
- log.trace("RedisStorage get result: {}", values);
- }
- if ("notModified".equals(values.toString())) {
- notModified(handler);
- } else if ("notFound".equals(values.toString())) {
- notFound(handler);
- } else {
- handleJsonArrayValues(values, handler, "0".equals(arguments.get(5)) &&
- "-1".equals(arguments.get(6)));
- }
- } else {
- String message = event.cause().getMessage();
- if (message != null && message.startsWith("NOSCRIPT")) {
- log.warn("get script couldn't be found, reload it");
- log.warn("amount the script got loaded: {}", executionCounter);
- if (executionCounter > 10) {
- log.error("amount the script got loaded is higher than 10, we abort");
- } else {
- luaScripts.get(LuaScript.GET).loadLuaScript(new Get(keys, arguments, handler), executionCounter);
- }
- } else {
- log.error("GET request failed with message: {}", message);
- }
- }
- }))
- .onFailure(event -> log.error("Redis: GET request failed", event));
- }
- }
-
- @Override
- public void storageExpand(String path, String etag, List subResources, Handler handler) {
- final String key = encodePath(path);
- List keys = Collections.singletonList(key);
- List arguments = Arrays.asList(
- redisResourcesPrefix,
- redisCollectionsPrefix,
- expirableSet,
- String.valueOf(System.currentTimeMillis()),
- MAX_EXPIRE_IN_MILLIS,
- StringUtils.join(subResources, ";"),
- String.valueOf(subResources.size())
- );
- reloadScriptIfLoglevelChangedAndExecuteRedisCommand(LuaScript.STORAGE_EXPAND, new StorageExpand(keys, arguments, handler, etag), 0);
- }
-
- /**
- * The StorageExpand Command Execution.
- * If the get script cannot be found under the sha in luaScriptState, reload the script.
- * To avoid infinite recursion, we limit the recursion.
- */
- private class StorageExpand implements RedisCommand {
-
- private List keys;
- private List arguments;
- private Handler handler;
- private String etag;
-
- public StorageExpand(List keys, List arguments, final Handler handler, String etag) {
- this.keys = keys;
- this.arguments = arguments;
- this.handler = handler;
- this.etag = etag;
- }
-
- public void exec(final int executionCounter) {
- List args = toPayload(luaScripts.get(LuaScript.STORAGE_EXPAND).getSha(), keys.size(), keys, arguments);
-
- redisProvider.redis().onSuccess(redisAPI -> redisAPI.evalsha(args, event -> {
- if (event.succeeded()) {
- String value = event.result().toString();
- if (log.isTraceEnabled()) {
- log.trace("RedisStorage get result: {}", value);
- }
- if ("compressionNotSupported".equalsIgnoreCase(value)) {
- error(handler, "Collections having compressed resources are not supported in storage expand");
- return;
- }
- if ("notFound".equalsIgnoreCase(value)) {
- notFound(handler);
- return;
- }
- JsonObject expandResult = new JsonObject();
-
- JsonArray resultArr = new JsonArray(value);
-
- for (Object resultEntry : resultArr) {
- JsonArray entries = (JsonArray) resultEntry;
- String subResourceName = ResourceNameUtil.resetReplacedColonsAndSemiColons(entries.getString(0));
- String subResourceValue = entries.getString(1);
- if (subResourceValue.startsWith("[") && subResourceValue.endsWith("]")) {
- expandResult.put(subResourceName, extractSortedJsonArray(subResourceValue));
- } else {
- try {
- expandResult.put(subResourceName, new JsonObject(subResourceValue));
- } catch (DecodeException ex) {
- invalid(handler, "Error decoding invalid json resource '" + subResourceName + "'");
- return;
- }
- }
- }
-
- byte[] finalExpandedContent = decodeBinary(expandResult.encode());
- String calcDigest = DigestUtils.sha1Hex(finalExpandedContent);
-
- if (calcDigest.equals(etag)) {
- notModified(handler);
- } else {
- DocumentResource r = new DocumentResource();
- r.readStream = new ByteArrayReadStream(finalExpandedContent);
- r.length = finalExpandedContent.length;
- r.etag = calcDigest;
- r.closeHandler = event1 -> {
- // nothing to close
- };
- handler.handle(r);
- }
- } else {
- String message = event.cause().getMessage();
- if (message != null && message.startsWith("NOSCRIPT")) {
- log.warn("storageExpand script couldn't be found, reload it");
- log.warn("amount the script got loaded: {}", executionCounter);
- if (executionCounter > 10) {
- log.error("amount the script got loaded is higher than 10, we abort");
- } else {
- luaScripts.get(LuaScript.STORAGE_EXPAND).loadLuaScript(
- new StorageExpand(keys, arguments, handler, etag), executionCounter);
- }
- } else {
- log.error("StorageExpand request failed with message: {}", message);
- }
- }
- }))
- .onFailure(event -> log.error("Redis: StorageExpand request failed", event));
- }
- }
-
- private JsonArray extractSortedJsonArray(String arrayString) {
- String arrayContent = arrayString.replaceAll("\\[", EMPTY).replaceAll("\\]", EMPTY)
- .replaceAll("\"", EMPTY).replaceAll("\\\\", EMPTY);
- String[] splitted = StringUtils.split(arrayContent, ",");
- List resources = new ArrayList<>();
- List collections = new ArrayList<>();
- for (String split : splitted) {
- if (split.endsWith("/")) {
- collections.add(split);
- } else {
- resources.add(split);
- }
- }
- Collections.sort(collections);
- collections.addAll(resources);
- return new JsonArray(new ArrayList