package org.swisspush.reststorage; import io.vertx.core.Handler; import io.vertx.core.Vertx; 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.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.core.streams.ReadStream; import io.vertx.core.streams.WriteStream; import io.vertx.redis.RedisClient; import io.vertx.redis.RedisOptions; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.text.StrSubstitutor; 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.util.*; public class RedisStorage implements Storage { private Logger log = LoggerFactory.getLogger(RedisStorage.class); // set to very high value = Sat Nov 20 2286 17:46:39 private static final String MAX_EXPIRE_IN_MILLIS = "9999999999999"; private final String EMPTY = ""; 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 RedisClient redisClient; private Map<LuaScript,LuaScriptState> luaScripts = new HashMap<>(); public RedisStorage(Vertx vertx, ModuleConfiguration config) { 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.redisClient = RedisClient.create(vertx, new RedisOptions().setHost(config.getRedisHost()).setPort(config.getRedisPort())); // 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); } 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: " + luaScriptType + " with logoutput: " + 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<String, String> 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; // check first if the lua script already exists in the store redisClient.scriptExists(this.sha, resultArray -> { if(resultArray.failed()){ log.error("Error checking whether lua script exists", resultArray.cause()); return; } Long exists = resultArray.result().getLong(0); // 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: " + luaScriptType + " logutput: " + logoutput); redisClient.scriptLoad(script, stringAsyncResult -> { String newSha = stringAsyncResult.result(); log.info("got sha from redis for lua script: " + luaScriptType + ": " + newSha); if(!newSha.equals(sha)) { log.warn("the sha calculated by myself: " + sha + " doesn't match with the sha from redis: " + newSha + ". We use the sha from redis"); } sha = newSha; log.info("execute redis command for script type: " + luaScriptType + " with new sha: " + sha); redisCommand.exec(executionCounterIncr); }); } }); } 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 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<Buffer> { ByteArrayInputStream content; int size; boolean paused; int position; Handler<Void> endHandler; Handler<Buffer> 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 ByteArrayReadStream pause() { paused = true; return this; } @Override public ByteArrayReadStream exceptionHandler(Handler<Throwable> handler) { return this; } @Override public ReadStream<Buffer> handler(Handler<Buffer> handler) { this.handler = handler; doRead(); return this; } @Override public ByteArrayReadStream endHandler(Handler<Void> endHandler) { this.endHandler = endHandler; return this; } } @Override public void get(String path, String etag, int offset, int limit, final Handler<Resource> handler) { final String key = encodePath(path); List<String> keys = Collections.singletonList(key); List<String> 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<String> keys; private List<String> arguments; private Handler<Resource> handler; public Get(List<String> keys, List<String> arguments, final Handler<Resource> handler) { this.keys = keys; this.arguments = arguments; this.handler = handler; } public void exec(final int executionCounter) { redisClient.evalsha(luaScripts.get(LuaScript.GET).getSha(), keys, arguments, event -> { if(event.succeeded()){ JsonArray values = event.result(); if (log.isTraceEnabled()) { log.trace("RedisStorage get result: " + values); } if("notModified".equals(values.getString(0))){ notModified(handler); } else if ("notFound".equals(values.getString(0))) { 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: " + String.valueOf(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); } } }); } } @Override public void storageExpand(String path, String etag, List<String> subResources, Handler<Resource> handler) { final String key = encodePath(path); List<String> keys = Collections.singletonList(key); List<String> 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<String> keys; private List<String> arguments; private Handler<Resource> handler; private String etag; public StorageExpand(List<String> keys, List<String> arguments, final Handler<Resource> handler, String etag) { this.keys = keys; this.arguments = arguments; this.handler = handler; this.etag = etag; } public void exec(final int executionCounter) { redisClient.evalsha(luaScripts.get(LuaScript.STORAGE_EXPAND).getSha(), keys, arguments, event -> { if(event.succeeded()){ Object value = event.result().getValue(0); if (log.isTraceEnabled()) { log.trace("RedisStorage get result: " + value); } if("compressionNotSupported".equalsIgnoreCase((String) value)){ error(handler, "Collections having compressed resources are not supported in storage expand"); return; } if("notFound".equalsIgnoreCase((String) value)){ notFound(handler); return; } JsonObject expandResult = new JsonObject(); JsonArray resultArr = new JsonArray((String) 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: " + String.valueOf(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); } } }); } } private JsonArray extractSortedJsonArray(String arrayString){ String arrayContent = arrayString.replaceAll("\\[", EMPTY).replaceAll("\\]", EMPTY).replaceAll("\"", EMPTY).replaceAll("\\\\", EMPTY); String[] splitted = StringUtils.split(arrayContent, ","); List<String> resources = new ArrayList<>(); List<String> 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<Object>(collections)); } private void handleJsonArrayValues(JsonArray values, Handler<Resource> handler, boolean allowEmptyReturn){ String type = values.getString(0); if("TYPE_RESOURCE".equals(type)){ String valueStr = values.getString(1); DocumentResource r = new DocumentResource(); byte[] content = decodeBinary(valueStr); if(!values.hasNull(3)){ // data is compressed GZIPUtil.decompressResource(vertx, content, decompressedResult -> { if(decompressedResult.succeeded()) { r.readStream = new ByteArrayReadStream(decompressedResult.result()); r.length = decompressedResult.result().length; r.etag = values.getString(2); r.closeHandler = event -> { // nothing to close }; handler.handle(r); } else { error(handler, "Error during decompression of resource: " + decompressedResult.cause().getMessage()); } }); } else { r.readStream = new ByteArrayReadStream(content); r.length = content.length; r.etag = values.getString(2); r.closeHandler = event -> { // nothing to close }; handler.handle(r); } } else if("TYPE_COLLECTION".equals(type)) { CollectionResource r = new CollectionResource(); Set<Resource> items = new HashSet<>(); for (Object value : values) { String member = (String) value; if (!"TYPE_COLLECTION".equals(member)) { if (member.endsWith(":")) { member = member.replaceAll(":$", ""); CollectionResource c = new CollectionResource(); c.name = member; items.add(c); } else { DocumentResource d = new DocumentResource(); d.name = member; items.add(d); } } } if(allowEmptyReturn && items.size()==0) { notFound(handler); } else { r.items = new ArrayList<>(items); Collections.sort(r.items); handler.handle(r); } } else { notFound(handler); } } class ByteArrayWriteStream implements WriteStream<Buffer> { private ByteArrayOutputStream bos = new ByteArrayOutputStream(); public byte[] getBytes() { return bos.toByteArray(); } @Override public ByteArrayWriteStream setWriteQueueMaxSize(int maxSize) { return this; } @Override public boolean writeQueueFull() { return false; } @Override public ByteArrayWriteStream drainHandler(Handler<Void> handler) { return this; } @Override public ByteArrayWriteStream exceptionHandler(Handler<Throwable> handler) { return this; } @Override public WriteStream<Buffer> write(Buffer data) { try { bos.write(data.getBytes()); } catch (IOException e) { throw new RuntimeException(e); } return this; } @Override public void end() { try { bos.close(); } catch (IOException e) { throw new RuntimeException(e); } } } private String initEtagValue(String providedEtag){ if(!isEmpty(providedEtag)){ return providedEtag; } return UUID.randomUUID().toString(); } @Override public void put(String path, final String etag, final boolean merge, final long expire, final String lockOwner, final LockMode lockMode, final long lockExpire, final Handler<Resource> handler) { put(path, etag, merge, expire, lockOwner, lockMode, lockExpire, false, handler); } @Override public void put(String path, final String etag, final boolean merge, final long expire, final Handler<Resource> handler) { put(path, etag, merge, expire, "", LockMode.SILENT, 0, handler); } @Override public void put(String path, String etag, boolean merge, long expire, String lockOwner, LockMode lockMode, long lockExpire, boolean storeCompressed, Handler<Resource> handler) { final String key = encodePath(path); final DocumentResource d = new DocumentResource(); final ByteArrayWriteStream stream = new ByteArrayWriteStream(); final String etagValue = initEtagValue(etag); d.writeStream = stream; d.closeHandler = event -> { String expireInMillis = MAX_EXPIRE_IN_MILLIS; if (expire > -1) { expireInMillis = String.valueOf(System.currentTimeMillis() + (expire * 1000)); } String lockExpireInMillis = String.valueOf(System.currentTimeMillis() + (lockExpire * 1000)); List<String> keys = Collections.singletonList(key); if (storeCompressed) { String finalExpireInMillis = expireInMillis; GZIPUtil.compressResource(vertx, stream.getBytes(), compressResourceResult -> { if(compressResourceResult.succeeded()) { List<String> arg = Arrays.asList( redisResourcesPrefix, redisCollectionsPrefix, expirableSet, merge ? "true" : "false", finalExpireInMillis, MAX_EXPIRE_IN_MILLIS, encodeBinary(compressResourceResult.result()), etagValue, redisLockPrefix, lockOwner, lockMode.text(), lockExpireInMillis, storeCompressed ? "1" : "0" ); reloadScriptIfLoglevelChangedAndExecuteRedisCommand(LuaScript.PUT, new Put(d, keys, arg, handler), 0); } else { error(handler, "Error during compression of resource"); } }); } else { List<String> arguments = Arrays.asList( redisResourcesPrefix, redisCollectionsPrefix, expirableSet, merge ? "true" : "false", expireInMillis, MAX_EXPIRE_IN_MILLIS, encodeBinary(stream.getBytes()), etagValue, redisLockPrefix, lockOwner, lockMode.text(), lockExpireInMillis, storeCompressed ? "1" : "0" ); reloadScriptIfLoglevelChangedAndExecuteRedisCommand(LuaScript.PUT, new Put(d, keys, arguments, handler), 0); } }; handler.handle(d); } /** * The Put 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 Put implements RedisCommand { private DocumentResource d; private List<String> keys; private List<String> arguments; private Handler<Resource> handler; public Put(DocumentResource d, List<String> keys, List<String> arguments, Handler<Resource> handler) { this.d = d; this.keys = keys; this.arguments = arguments; this.handler = handler; } public void exec(final int executionCounter) { redisClient.evalsha(luaScripts.get(LuaScript.PUT).getSha(), keys, arguments, event -> { if(event.succeeded()){ String result = event.result().getString(0); if (log.isTraceEnabled()) { log.trace("RedisStorage successfull put. Result: " + result); } if(result != null && result.startsWith("existingCollection")){ CollectionResource c = new CollectionResource(); handler.handle(c); } else if(result != null && result.startsWith("existingResource")){ DocumentResource d = new DocumentResource(); d.exists = false; handler.handle(d); } else if("notModified".equals(result)){ notModified(handler); } else if(LockMode.REJECT.text().equals(result)) { rejected(handler); } else { d.endHandler.handle(null); } } else { String message = event.cause().getMessage(); if(message != null && message.startsWith("NOSCRIPT")) { log.warn("put script couldn't be found, reload it"); log.warn("amount the script got loaded: " + String.valueOf(executionCounter)); if(executionCounter > 10) { log.error("amount the script got loaded is higher than 10, we abort"); } else { luaScripts.get(LuaScript.PUT).loadLuaScript(new Put(d, keys, arguments, handler), executionCounter); } } else if (message != null && d.errorHandler != null){ log.error("PUT request failed with message: " + message); d.errorHandler.handle(message); } } }); } } @Override public void delete(String path, final Handler<Resource> handler) { delete(path, "", LockMode.SILENT, 0, handler); } @Override public void delete(String path, String lockOwner, LockMode lockMode, long lockExpire, final Handler<Resource> handler ) { final String key = encodePath(path); List<String> keys = Collections.singletonList(key); String lockExpireInMillis = String.valueOf(System.currentTimeMillis() + (lockExpire * 1000)); List<String> arguments = Arrays.asList( redisResourcesPrefix, redisCollectionsPrefix, redisDeltaResourcesPrefix, redisDeltaEtagsPrefix, expirableSet, String.valueOf(System.currentTimeMillis()), MAX_EXPIRE_IN_MILLIS, redisLockPrefix, lockOwner, lockMode.text(), lockExpireInMillis ); reloadScriptIfLoglevelChangedAndExecuteRedisCommand(LuaScript.DELETE, new Delete(keys, arguments, handler), 0); } /** * The Delete 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 Delete implements RedisCommand { private List<String> keys; private List<String> arguments; private Handler<Resource> handler; public Delete(List<String> keys, List<String> arguments, final Handler<Resource> handler) { this.keys = keys; this.arguments = arguments; this.handler = handler; } public void exec(final int executionCounter) { redisClient.evalsha(luaScripts.get(LuaScript.DELETE).getSha(), keys, arguments, event -> { if(event.cause() != null && event.cause().getMessage().startsWith("NOSCRIPT")) { log.warn("delete script couldn't be found, reload it"); log.warn("amount the script got loaded: " + String.valueOf(executionCounter)); if(executionCounter > 10) { log.error("amount the script got loaded is higher than 10, we abort"); } else { luaScripts.get(LuaScript.DELETE).loadLuaScript(new Delete(keys, arguments, handler), executionCounter); } return; } String result = null; if(event.result() != null){ result = event.result().getString(0); } if (log.isTraceEnabled()) { log.trace("RedisStorage delete result: " + result); } if ("notFound".equals(result)) { notFound(handler); return; } else if(LockMode.REJECT.text().equals(result)) { rejected(handler); return; } Resource r = new Resource(); handler.handle(r); }); } } /** * Cleans up the outdated resources recursive. * If the script which is refered over the luaScriptState.sha, the execution is aborted and the script is reloaded. * * @param handler the handler to execute * @param cleanedLastRun how many resources were cleaned in the last run * @param maxdel max resources to clean * @param bulkSize how many resources should be cleaned in one run */ public void cleanupRecursive(final Handler<DocumentResource> handler, final long cleanedLastRun, final long maxdel, final int bulkSize) { List<String> arguments = Arrays.asList( redisResourcesPrefix, redisCollectionsPrefix, redisDeltaResourcesPrefix, redisDeltaEtagsPrefix, expirableSet, "0", MAX_EXPIRE_IN_MILLIS, String.valueOf(System.currentTimeMillis()), String.valueOf(bulkSize) ); redisClient.evalsha(luaScripts.get(LuaScript.CLEANUP).getSha(), Collections.emptyList(), arguments, event -> { if (log.isTraceEnabled()) { log.trace("RedisStorage cleanup resources succeeded: " + event.succeeded()); } if(event.failed() && event.cause() != null && event.cause().getMessage().startsWith("NOSCRIPT")) { log.warn("the cleanup script is not loaded. Load it and exit. The Cleanup will success the next time"); luaScripts.get(LuaScript.CLEANUP).loadLuaScript(new RedisCommandDoNothing(), 0); return; } long cleanedThisRun = 0; if(event.succeeded() && event.result().getLong(0) != null){ cleanedThisRun = event.result().getLong(0); } if (log.isTraceEnabled()) { log.trace("RedisStorage cleanup resources cleanded this run: " + cleanedThisRun); } final long cleaned = cleanedLastRun + cleanedThisRun; if (cleanedThisRun != 0 && cleaned < maxdel) { if (log.isTraceEnabled()) { log.trace("RedisStorage cleanup resources call recursive next bulk"); } cleanupRecursive(handler, cleaned, maxdel, bulkSize); } else { redisClient.zcount(expirableSet, 0, System.currentTimeMillis(), longAsyncResult -> { Long result = longAsyncResult.result(); if (log.isTraceEnabled()) { log.trace("RedisStorage cleanup resources zcount on expirable set: " + result); } int resToCleanLeft = 0; if (result != null && result.intValue() >= 0) { resToCleanLeft = result.intValue(); } JsonObject retObj = new JsonObject(); retObj.put("cleanedResources", cleaned); retObj.put("expiredResourcesLeft", resToCleanLeft); DocumentResource r = new DocumentResource(); byte[] content = decodeBinary(retObj.toString()); r.readStream = new ByteArrayReadStream(content); r.length = content.length; r.closeHandler = event1 -> { // nothing to close }; handler.handle(r); }); } }); } private String encodePath(String path) { if (path.equals("/")) { path = ""; } return ResourceNameUtil.replaceColonsAndSemiColons(path).replaceAll("/", ":"); } private String encodeBinary(byte[] bytes) { try { return new String(bytes, "ISO-8859-1"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private byte[] decodeBinary(String s) { try { return s.getBytes("ISO-8859-1"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private void notFound(Handler<Resource> handler) { Resource r = new Resource(); r.exists = false; handler.handle(r); } private void notModified(Handler<Resource> handler){ Resource r = new Resource(); r.modified = false; handler.handle(r); } private void rejected(Handler<Resource> handler) { Resource r = new Resource(); r.rejected = true; handler.handle(r); } private void invalid(Handler<Resource> handler, String invalidMessage){ Resource r = new Resource(); r.invalid = true; r.invalidMessage = invalidMessage; handler.handle(r); } private void error(Handler<Resource> handler, String errorMessage){ Resource r = new Resource(); r.error = true; r.errorMessage = errorMessage; handler.handle(r); } @Override public void cleanup(Handler<DocumentResource> handler, String cleanupResourcesAmountStr) { long cleanupResourcesAmountUsed = cleanupResourcesAmount; if (log.isTraceEnabled()) { log.trace("RedisStorage cleanup resources, cleanupResourcesAmount: " + cleanupResourcesAmountUsed); } try { cleanupResourcesAmountUsed = Long.parseLong(cleanupResourcesAmountStr); } catch (Exception e) { // do nothing } cleanupRecursive(handler, 0, cleanupResourcesAmountUsed, CLEANUP_BULK_SIZE); } private boolean isEmpty(CharSequence cs) { return cs == null || cs.length() == 0; } }