├── pubring.gpg ├── secring.gpg ├── .gitignore ├── src ├── test │ ├── resources │ │ ├── vertx-default-jul-logging.properties │ │ ├── log4j.xml │ │ ├── redis_info_persistance_loading_0 │ │ └── redis_info_persistance_loading_1 │ └── java │ │ └── org │ │ └── swisspush │ │ └── redisques │ │ ├── util │ │ ├── TestMemoryUsageProvider.java │ │ ├── MessageUtilTest.java │ │ ├── RedisQuesTimerTest.java │ │ └── StatuscodeTest.java │ │ ├── action │ │ ├── DeleteAllLocksActionTest.java │ │ ├── GetQueueItemsCountActionTest.java │ │ ├── BulkPutLocksActionTest.java │ │ ├── BulkDeleteLocksActionTest.java │ │ ├── GetQueuesItemsCountActionTest.java │ │ ├── GetQueuesActionTest.java │ │ ├── GetQueuesCountActionTest.java │ │ ├── GetQueuesSpeedActionTest.java │ │ ├── GetQueuesStatisticsActionTest.java │ │ ├── AddQueueItemActionTest.java │ │ ├── GetQueueItemsActionTest.java │ │ ├── ReplaceQueueItemActionTest.java │ │ ├── LockedEnqueueActionTest.java │ │ ├── AbstractQueueActionTest.java │ │ ├── GetQueueItemActionTest.java │ │ ├── GetLockActionTest.java │ │ ├── DeleteLockActionTest.java │ │ ├── PutLockActionTest.java │ │ ├── GetAllLocksActionTest.java │ │ ├── DeleteQueueItemActionTest.java │ │ └── DeleteAllQueueItemsActionTest.java │ │ └── RedisQuesProcessorDelayedExecutionTest.java └── main │ ├── java │ └── org │ │ └── swisspush │ │ └── redisques │ │ ├── queue │ │ ├── UnregisterConsumerType.java │ │ ├── QueueProcessingState.java │ │ └── KeyspaceHelper.java │ │ ├── util │ │ ├── MetricsPublisher.java │ │ ├── MemoryUsageProvider.java │ │ ├── RedisquesConfigurationProvider.java │ │ ├── RedisProvider.java │ │ ├── MetricTags.java │ │ ├── DebugInfo.java │ │ ├── FailedAsyncResult.java │ │ ├── RedisReadyProvider.java │ │ ├── EventBusMetricsPublisher.java │ │ ├── MetricMeter.java │ │ ├── MessageUtil.java │ │ ├── HandlerUtil.java │ │ ├── RedisQuesTimer.java │ │ ├── StatusCode.java │ │ ├── ResourcesUtils.java │ │ ├── RedisUtils.java │ │ ├── Result.java │ │ ├── LockUtil.java │ │ └── DefaultRedisReadyProvider.java │ │ ├── QueueState.java │ │ ├── lua │ │ ├── LuaScript.java │ │ ├── RedisCommand.java │ │ └── RedisCommandDoNothing.java │ │ ├── exception │ │ ├── ResourceExhaustionException.java │ │ ├── NoStacktraceException.java │ │ ├── WastefulRedisQuesExceptionFactory.java │ │ ├── RedisQuesExceptionFactory.java │ │ └── ThriftyRedisQuesExceptionFactory.java │ │ ├── lock │ │ ├── lua │ │ │ ├── LockLuaScripts.java │ │ │ └── ReleaseLockRedisCommand.java │ │ ├── Lock.java │ │ └── impl │ │ │ └── RedisBasedLock.java │ │ ├── action │ │ ├── QueueAction.java │ │ ├── GetConfigurationAction.java │ │ ├── UnsupportedAction.java │ │ ├── SetConfigurationAction.java │ │ ├── GetQueueItemAction.java │ │ ├── GetQueueItemsCountAction.java │ │ ├── GetAllLocksAction.java │ │ ├── ReplaceQueueItemAction.java │ │ ├── DeleteAllLocksAction.java │ │ ├── GetLockAction.java │ │ ├── BulkDeleteLocksAction.java │ │ ├── GetQueuesAction.java │ │ ├── GetQueuesCountAction.java │ │ ├── DeleteLockAction.java │ │ ├── AddQueueItemAction.java │ │ ├── PutLockAction.java │ │ ├── GetQueuesSpeedAction.java │ │ ├── BulkPutLocksAction.java │ │ ├── GetQueuesStatisticsAction.java │ │ ├── BulkDeleteQueuesAction.java │ │ ├── GetQueuesItemsCountAction.java │ │ ├── DeleteQueueItemAction.java │ │ ├── DeleteAllQueueItemsAction.java │ │ ├── GetQueueItemsAction.java │ │ ├── MonitorAction.java │ │ └── LockedEnqueueAction.java │ │ ├── RedisQuesRunner.java │ │ ├── handler │ │ ├── DeleteLockHandler.java │ │ ├── PutLockHandler.java │ │ ├── AddQueueItemHandler.java │ │ ├── GetQueuesCountHandler.java │ │ ├── GetQueueItemsCountHandler.java │ │ ├── GetLockHandler.java │ │ ├── GetQueueItemHandler.java │ │ ├── ReplaceQueueItemHandler.java │ │ ├── GetQueueItemsHandler.java │ │ ├── GetQueuesSpeedHandler.java │ │ ├── GetAllLocksHandler.java │ │ ├── GetQueuesHandler.java │ │ ├── GetQueuesStatisticsHandler.java │ │ └── RedisquesConfigurationAuthentication.java │ │ ├── metrics │ │ ├── LongTaskTimerSamplePair.java │ │ └── MetricsCollectorScheduler.java │ │ └── performance │ │ └── LongRingbuffer.java │ └── resources │ ├── lock_release.lua │ └── mod.json ├── LICENSE.txt ├── maybe-release.sh └── .github └── workflows └── maven.yml /pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swisspost/vertx-redisques/HEAD/pubring.gpg -------------------------------------------------------------------------------- /secring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swisspost/vertx-redisques/HEAD/secring.gpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.classpath 3 | /.project 4 | /.settings 5 | /build 6 | /.idea 7 | /*.iml 8 | -------------------------------------------------------------------------------- /src/test/resources/vertx-default-jul-logging.properties: -------------------------------------------------------------------------------- 1 | handlers=java.util.logging.ConsoleHandler 2 | java.util.logging.ConsoleHandler.level=FINEST 3 | 4 | .level=INFO -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/queue/UnregisterConsumerType.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.queue; 2 | 3 | public enum UnregisterConsumerType { 4 | FORCE, GRACEFUL, QUIET_FOR_SOMETIME 5 | } -------------------------------------------------------------------------------- /src/main/resources/lock_release.lua: -------------------------------------------------------------------------------- 1 | local lockKey = KEYS[1] 2 | local token = ARGV[1] 3 | 4 | if redis.call("get", lockKey) == token then 5 | return redis.call("del", lockKey) 6 | else 7 | return 0 8 | end -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/MetricsPublisher.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | public interface MetricsPublisher { 4 | 5 | void publishMetric(String name, long value); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/QueueState.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques; 2 | 3 | // State of each queue. Consuming means there is a message being processed. 4 | public enum QueueState { 5 | READY, CONSUMING 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/lua/LuaScript.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.lua; 2 | 3 | /** 4 | * @author https://github.com/mcweba [Marc-Andre Weber] 5 | */ 6 | public interface LuaScript { 7 | String getFilename(); 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This work is licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this file except in compliance with the License. 3 | 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/MemoryUsageProvider.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import java.util.Optional; 4 | 5 | public interface MemoryUsageProvider { 6 | 7 | Optional currentMemoryUsagePercentage(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/lua/RedisCommand.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.lua; 2 | 3 | /** 4 | * @author https://github.com/mcweba [Marc-Andre Weber] 5 | */ 6 | public interface RedisCommand { 7 | void exec(int executionCounter); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "org.swisspush.redisques.RedisQues", 3 | "description":"A highly scalable redis-persistent queuing system for Vert.x", 4 | "licenses": ["The Apache Software License Version 2.0"], 5 | "author": "Laurent Bovet", 6 | "homepage": "https://github.com/swisspush/vertx-redisques" 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/lua/RedisCommandDoNothing.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.lua; 2 | 3 | /** 4 | * @author https://github.com/mcweba [Marc-Andre Weber] 5 | */ 6 | public class RedisCommandDoNothing implements RedisCommand { 7 | 8 | @Override 9 | public void exec(int executionCounter) { 10 | // do nothing here 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /maybe-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | git fetch 4 | git reset --hard 5 | mvn -B -Prelease jgitflow:release-start jgitflow:release-finish 6 | rc=$? 7 | if [ $rc -eq 0 ] 8 | then 9 | echo 'Release done, will push' 10 | git tag 11 | git push --tags 12 | git checkout develop 13 | git push origin develop 14 | exit 0 15 | fi 16 | echo 'Release failed' 17 | exit $rc -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/RedisquesConfigurationProvider.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | 5 | public interface RedisquesConfigurationProvider { 6 | 7 | RedisquesConfiguration configuration(); 8 | 9 | Result updateConfiguration(JsonObject configuration, boolean validateOnly); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/RedisProvider.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.redis.client.RedisAPI; 5 | 6 | /** 7 | * Provider for {@link RedisAPI} 8 | * 9 | * @author Marc-André Weber 10 | */ 11 | public interface RedisProvider { 12 | Future redis(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/MetricTags.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | public enum MetricTags { 4 | 5 | IDENTIFIER("identifier"), 6 | QUEUE_NAME("queue_name"), 7 | CONSUMER_UID("consumer_uid"); 8 | 9 | private final String id; 10 | 11 | MetricTags(String id) { 12 | this.id = id; 13 | } 14 | 15 | public String getId() { 16 | return id; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/DebugInfo.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | public class DebugInfo { 4 | 5 | public static String __WHERE__() { return __WHERE__(1); } 6 | 7 | public static String __WHERE__(int numFramesToIgnore) { 8 | var frame = Thread.currentThread().getStackTrace()[2 + numFramesToIgnore]; 9 | return frame.getClassName() + "." + frame.getMethodName() + "():L" + frame.getLineNumber(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/exception/ResourceExhaustionException.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.exception; 2 | 3 | /** 4 | *

Thrown when something cannot be done right now because some 5 | * resource is exhausted.

6 | * 7 | *

For example not enough memory, connection pools or waiting queues 8 | * are full, etc.

9 | */ 10 | public class ResourceExhaustionException extends Exception { 11 | 12 | public ResourceExhaustionException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/lock/lua/LockLuaScripts.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.lock.lua; 2 | 3 | import org.swisspush.redisques.lua.LuaScript; 4 | 5 | /** 6 | * @author https://github.com/mcweba [Marc-Andre Weber] 7 | */ 8 | public enum LockLuaScripts implements LuaScript { 9 | 10 | LOCK_RELEASE("lock_release.lua"); 11 | 12 | private String file; 13 | 14 | LockLuaScripts(String file) { 15 | this.file = file; 16 | } 17 | 18 | @Override 19 | public String getFilename() { 20 | return file; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/QueueAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.eventbus.Message; 4 | import io.vertx.core.json.JsonObject; 5 | 6 | import static org.swisspush.redisques.util.RedisquesAPI.*; 7 | 8 | public interface QueueAction { 9 | void execute(Message event); 10 | 11 | default JsonObject createOkReply() { 12 | return new JsonObject().put(STATUS, OK); 13 | } 14 | 15 | default JsonObject createErrorReply() { 16 | return new JsonObject().put(STATUS, ERROR); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/FailedAsyncResult.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.AsyncResult; 4 | 5 | public class FailedAsyncResult implements AsyncResult { 6 | private final Throwable cause; 7 | 8 | public FailedAsyncResult(Throwable cause) { 9 | this.cause = cause; 10 | } 11 | 12 | @Override 13 | public Response result() { 14 | return null; 15 | } 16 | 17 | @Override 18 | public Throwable cause() { 19 | return cause; 20 | } 21 | 22 | @Override 23 | public boolean succeeded() { 24 | return false; 25 | } 26 | 27 | @Override 28 | public boolean failed() { 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/exception/NoStacktraceException.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.exception; 2 | 3 | /** 4 | * Basically same as in vertx, But adding the forgotten contructors. 5 | */ 6 | public class NoStacktraceException extends RuntimeException { 7 | 8 | public NoStacktraceException() { 9 | } 10 | 11 | public NoStacktraceException(String message) { 12 | super(message); 13 | } 14 | 15 | public NoStacktraceException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public NoStacktraceException(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | @Override 24 | public Throwable fillInStackTrace() { 25 | return this; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/RedisReadyProvider.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.redis.client.RedisAPI; 5 | 6 | /** 7 | * Provides the "ready state" of the Redis database. The connection to Redis may be already established, but Redis is not 8 | * yet ready to be used 9 | * 10 | * @author Marc-André Weber 11 | */ 12 | public interface RedisReadyProvider { 13 | 14 | /** 15 | * Get the "ready state" of the Redis database. 16 | * 17 | * @param redisAPI API to access redis database 18 | * @return An async boolean true when Redis can be used. Returns async boolean false otherwise or in case of an error 19 | */ 20 | Future ready(RedisAPI redisAPI); 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/util/TestMemoryUsageProvider.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import java.util.Optional; 4 | 5 | public class TestMemoryUsageProvider implements MemoryUsageProvider { 6 | 7 | private Optional currentMemoryUsagePercentage; 8 | 9 | public TestMemoryUsageProvider(Optional currentMemoryUsagePercentage) { 10 | this.currentMemoryUsagePercentage = currentMemoryUsagePercentage; 11 | } 12 | 13 | @Override 14 | public Optional currentMemoryUsagePercentage() { 15 | return currentMemoryUsagePercentage; 16 | } 17 | 18 | public void setCurrentMemoryUsage(Optional currentMemoryUsagePercentage) { 19 | this.currentMemoryUsagePercentage = currentMemoryUsagePercentage; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetConfigurationAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.eventbus.Message; 4 | import io.vertx.core.json.JsonObject; 5 | import org.swisspush.redisques.util.RedisquesConfigurationProvider; 6 | 7 | import static org.swisspush.redisques.util.RedisquesAPI.VALUE; 8 | 9 | public class GetConfigurationAction implements QueueAction { 10 | 11 | private final RedisquesConfigurationProvider configurationProvider; 12 | 13 | public GetConfigurationAction(RedisquesConfigurationProvider configurationProvider) { 14 | this.configurationProvider = configurationProvider; 15 | } 16 | 17 | @Override 18 | public void execute(Message event) { 19 | event.reply(createOkReply().put(VALUE, configurationProvider.configuration().asJsonObject())); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/EventBusMetricsPublisher.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.json.JsonObject; 5 | 6 | public class EventBusMetricsPublisher implements MetricsPublisher { 7 | 8 | private final Vertx vertx; 9 | private final String monitoringAddress; 10 | private final String prefix; 11 | 12 | public EventBusMetricsPublisher(Vertx vertx, String monitoringAddress, String prefix) { 13 | this.vertx = vertx; 14 | this.monitoringAddress = monitoringAddress; 15 | this.prefix = prefix; 16 | } 17 | 18 | @Override 19 | public void publishMetric(String name, long value) { 20 | vertx.eventBus().publish(monitoringAddress, 21 | new JsonObject().put("name", prefix + name).put("action", "set").put("n", value)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/redis_info_persistance_loading_0: -------------------------------------------------------------------------------- 1 | # Persistence 2 | loading:0 3 | async_loading:0 4 | current_cow_peak:0 5 | current_cow_size:0 6 | current_cow_size_age:0 7 | current_fork_perc:0.00 8 | current_save_keys_processed:0 9 | current_save_keys_total:0 10 | rdb_changes_since_last_save:108 11 | rdb_bgsave_in_progress:0 12 | rdb_last_save_time:1718713249 13 | rdb_last_bgsave_status:ok 14 | rdb_last_bgsave_time_sec:0 15 | rdb_current_bgsave_time_sec:-1 16 | rdb_saves:296 17 | rdb_last_cow_size:0 18 | rdb_last_load_keys_expired:0 19 | rdb_last_load_keys_loaded:0 20 | aof_enabled:0 21 | aof_rewrite_in_progress:0 22 | aof_rewrite_scheduled:0 23 | aof_last_rewrite_time_sec:-1 24 | aof_current_rewrite_time_sec:-1 25 | aof_last_bgrewrite_status:ok 26 | aof_rewrites:0 27 | aof_rewrites_consecutive_failures:0 28 | aof_last_write_status:ok 29 | aof_last_cow_size:0 30 | module_fork_in_progress:0 31 | module_fork_last_cow_size:0 32 | -------------------------------------------------------------------------------- /src/test/resources/redis_info_persistance_loading_1: -------------------------------------------------------------------------------- 1 | # Persistence 2 | loading:1 3 | async_loading:0 4 | current_cow_peak:0 5 | current_cow_size:0 6 | current_cow_size_age:0 7 | current_fork_perc:0.00 8 | current_save_keys_processed:0 9 | current_save_keys_total:0 10 | rdb_changes_since_last_save:108 11 | rdb_bgsave_in_progress:0 12 | rdb_last_save_time:1718713249 13 | rdb_last_bgsave_status:ok 14 | rdb_last_bgsave_time_sec:0 15 | rdb_current_bgsave_time_sec:-1 16 | rdb_saves:296 17 | rdb_last_cow_size:0 18 | rdb_last_load_keys_expired:0 19 | rdb_last_load_keys_loaded:0 20 | aof_enabled:0 21 | aof_rewrite_in_progress:0 22 | aof_rewrite_scheduled:0 23 | aof_last_rewrite_time_sec:-1 24 | aof_current_rewrite_time_sec:-1 25 | aof_last_bgrewrite_status:ok 26 | aof_rewrites:0 27 | aof_rewrites_consecutive_failures:0 28 | aof_last_write_status:ok 29 | aof_last_cow_size:0 30 | module_fork_in_progress:0 31 | module_fork_last_cow_size:0 32 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/queue/QueueProcessingState.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.queue; 2 | 3 | import org.swisspush.redisques.QueueState; 4 | 5 | public class QueueProcessingState { 6 | public QueueProcessingState(QueueState state, long timestampMillis) { 7 | this.setState(state); 8 | this.setLastConsumedTimestampMillis(timestampMillis); 9 | } 10 | 11 | private QueueState state; 12 | private long lastConsumedTimestampMillis; 13 | 14 | public QueueState getState() { 15 | return state; 16 | } 17 | 18 | public void setState(QueueState state) { 19 | this.state = state; 20 | } 21 | 22 | public long getLastConsumedTimestampMillis() { 23 | return lastConsumedTimestampMillis; 24 | } 25 | 26 | public void setLastConsumedTimestampMillis(long lastConsumedTimestampMillis) { 27 | this.lastConsumedTimestampMillis = lastConsumedTimestampMillis; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/UnsupportedAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.eventbus.Message; 4 | import io.vertx.core.json.JsonObject; 5 | import org.slf4j.Logger; 6 | 7 | import static org.swisspush.redisques.util.RedisquesAPI.*; 8 | 9 | public class UnsupportedAction implements QueueAction { 10 | 11 | protected final Logger log; 12 | 13 | public UnsupportedAction(Logger log) { 14 | this.log = log; 15 | } 16 | 17 | @Override 18 | public void execute(Message event) { 19 | final JsonObject body = event.body(); 20 | String message; 21 | if (null != body) { 22 | message = "QUEUE_ERROR: Unsupported operation received: " + body.getString(OPERATION); 23 | } else { 24 | message = "QUEUE_ERROR: Unsupported operation received"; 25 | } 26 | 27 | JsonObject reply = new JsonObject(); 28 | log.error(message); 29 | reply.put(STATUS, ERROR); 30 | reply.put(MESSAGE, message); 31 | event.reply(reply); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/RedisQuesRunner.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques; 2 | 3 | import io.vertx.core.DeploymentOptions; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.LoggerFactory; 7 | import org.swisspush.redisques.util.RedisquesConfiguration; 8 | 9 | /** 10 | * Deploys vertx-redisques to vert.x. 11 | * Used in the standalone scenario. 12 | * 13 | * @author Marc-André Weber 14 | */ 15 | public class RedisQuesRunner { 16 | 17 | public static void main(String[] args) { 18 | 19 | JsonObject configuration = RedisquesConfiguration.with() 20 | .httpRequestHandlerEnabled(true) 21 | .redisReconnectAttempts(-1) 22 | .redisPoolRecycleTimeoutMs(-1) 23 | .redisReadyCheckIntervalMs(5000) 24 | .build().asJsonObject(); 25 | 26 | Vertx.vertx().deployVerticle(new RedisQues(), new DeploymentOptions().setConfig(configuration), 27 | event -> LoggerFactory.getLogger(RedisQues.class).info("vertx-redisques started")); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/SetConfigurationAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.eventbus.Message; 4 | import io.vertx.core.json.JsonObject; 5 | import org.slf4j.Logger; 6 | import org.swisspush.redisques.util.RedisquesConfigurationProvider; 7 | import org.swisspush.redisques.util.Result; 8 | 9 | import static org.swisspush.redisques.util.RedisquesAPI.*; 10 | 11 | public class SetConfigurationAction implements QueueAction { 12 | 13 | protected final Logger log; 14 | private final RedisquesConfigurationProvider configurationProvider; 15 | 16 | public SetConfigurationAction(RedisquesConfigurationProvider configurationProvider, Logger log) { 17 | this.configurationProvider = configurationProvider; 18 | this.log = log; 19 | } 20 | 21 | @Override 22 | public void execute(Message event) { 23 | JsonObject configurationValues = event.body().getJsonObject(PAYLOAD); 24 | Result updateResult = configurationProvider.updateConfiguration(configurationValues, true); 25 | if(updateResult.isOk()) { 26 | event.reply(createOkReply()); 27 | } else { 28 | event.reply(createErrorReply().put(MESSAGE, updateResult.getErr())); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/MetricMeter.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | public enum MetricMeter { 4 | 5 | ENQUEUE_SUCCESS("redisques.enqueue.success", "Overall count of queue items to be enqueued successfully"), 6 | ENQUEUE_FAIL("redisques.enqueue.fail", "Overall count of queue items which could not be enqueued"), 7 | DEQUEUE("redisques.dequeue", "Overall count of queue items to be dequeued from the queues"), 8 | ACTIVE_QUEUES("redisques.active.queues", "Count of active queues"), 9 | MAX_QUEUE_SIZE("redisques.max.queue.size", "Amount of queue items of the biggest queue"), 10 | QUEUE_STATE_READY_SIZE("redisques.queue.state.ready.size", "Amount of queue in state ready"), 11 | QUEUE_STATE_CONSUMING_SIZE("redisques.queue.state.consuming.size", "Amount of queue in state consuming"), 12 | QUEUE_CONSUMER_COUNT("redisques.queue.consumers", "Count of consumer registered and alive for a redisques instance"), 13 | QUEUE_CONSUMER_LIFE_CYCLE("redisques.queue.consumer.life.cycle", "Count of consumer registered and alive for a redisques instance"); 14 | private final String id; 15 | private final String description; 16 | 17 | MetricMeter(String id, String description) { 18 | this.id = id; 19 | this.description = description; 20 | } 21 | 22 | public String getId() { 23 | return id; 24 | } 25 | 26 | public String getDescription() { 27 | return description; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/DeleteLockHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 10 | 11 | import static org.slf4j.LoggerFactory.getLogger; 12 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 13 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 14 | 15 | /** 16 | * Class DeleteLockHandler. 17 | * 18 | * @author baldim 19 | */ 20 | public class DeleteLockHandler implements Handler> { 21 | 22 | private static final Logger log = getLogger(DeleteLockHandler.class); 23 | private final Message event; 24 | private final RedisQuesExceptionFactory exceptionFactory; 25 | 26 | public DeleteLockHandler(Message event, RedisQuesExceptionFactory exceptionFactory) { 27 | this.event = event; 28 | this.exceptionFactory = exceptionFactory; 29 | } 30 | 31 | @Override 32 | public void handle(AsyncResult reply) { 33 | if (reply.succeeded()) { 34 | event.reply(new JsonObject().put(STATUS, OK)); 35 | } else { 36 | event.reply(exceptionFactory.newReplyException(null, reply.cause())); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/PutLockHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 10 | 11 | import static org.slf4j.LoggerFactory.getLogger; 12 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 13 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 14 | 15 | /** 16 | * Class PutLock. 17 | * 18 | * @author baldim 19 | */ 20 | public class PutLockHandler implements Handler> { 21 | 22 | private static final Logger log = getLogger(PutLockHandler.class); 23 | private final Message event; 24 | private final RedisQuesExceptionFactory exceptionFactory; 25 | 26 | public PutLockHandler(Message event, RedisQuesExceptionFactory exceptionFactory) { 27 | this.event = event; 28 | this.exceptionFactory = exceptionFactory; 29 | } 30 | 31 | @Override 32 | public void handle(AsyncResult reply) { 33 | if(reply.succeeded()){ 34 | event.reply(new JsonObject().put(STATUS, OK)); 35 | } else { 36 | event.reply(exceptionFactory.newReplyException(reply.cause().getMessage(), reply.cause())); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/MessageUtil.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.eventbus.Message; 4 | import io.vertx.core.json.JsonObject; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.Optional; 9 | import java.util.regex.Pattern; 10 | 11 | import static org.swisspush.redisques.util.RedisquesAPI.FILTER; 12 | import static org.swisspush.redisques.util.RedisquesAPI.PAYLOAD; 13 | 14 | /** 15 | * Util class to work with {@link Message}s 16 | * 17 | * @author Marc-André Weber 18 | */ 19 | public class MessageUtil { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(MessageUtil.class); 22 | 23 | public static Result, String> extractFilterPattern(Message event) { 24 | JsonObject payload = event.body().getJsonObject(PAYLOAD); 25 | if (payload == null || payload.getString(FILTER) == null) { 26 | return Result.ok(Optional.empty()); 27 | } 28 | String filterString = payload.getString(FILTER); 29 | try { 30 | Pattern pattern = Pattern.compile(filterString); 31 | return Result.ok(Optional.of(pattern)); 32 | } catch (Exception ex) { 33 | log.error("Interface doesn't allow to pass stack trace. Therefore simply log it now.", ex); 34 | return Result.err("Error while compile regex pattern. Cause: " + ex.getMessage()); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/AddQueueItemHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 10 | 11 | import static org.slf4j.LoggerFactory.getLogger; 12 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 13 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 14 | 15 | /** 16 | * Class AddQueueItemHandler. 17 | * 18 | * @author baldim, Marc-André Weber 19 | */ 20 | public class AddQueueItemHandler implements Handler> { 21 | 22 | private static final Logger log = getLogger(AddQueueItemHandler.class); 23 | private final Message event; 24 | private final RedisQuesExceptionFactory exceptionFactory; 25 | 26 | public AddQueueItemHandler(Message event, RedisQuesExceptionFactory exceptionFactory) { 27 | this.event = event; 28 | this.exceptionFactory = exceptionFactory; 29 | } 30 | 31 | @Override 32 | public void handle(AsyncResult reply) { 33 | if(reply.succeeded()){ 34 | event.reply(new JsonObject().put(STATUS, OK)); 35 | } else { 36 | event.reply(exceptionFactory.newReplyException(null, reply.cause())); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/HandlerUtil.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.redis.client.Response; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import java.util.regex.Pattern; 8 | 9 | public class HandlerUtil { 10 | 11 | /** 12 | * Apply the given filter pattern to the given JsonArray containing the list of queues/locks. 13 | * @param response list of JSON objects containing the queueName/lockName to be filtered 14 | * @param filterPattern The filter regex pattern to be matched against the given queues/locks list 15 | * @return The resulting filtered list of queues/locks. If there is no filter given, the full list 16 | * of queues/locks is returned. 17 | */ 18 | public static List filterByPattern(Response response, Optional filterPattern) { 19 | List queues = new ArrayList<>(); 20 | if (filterPattern != null && filterPattern.isPresent()) { 21 | Pattern pattern = filterPattern.get(); 22 | for (int i = 0; i < response.size(); i++) { 23 | String queue = response.get(i).toString(); 24 | if (pattern.matcher(queue).find()) { 25 | queues.add(queue); 26 | } 27 | } 28 | } else { 29 | for (int i = 0; i < response.size(); i++) { 30 | queues.add(response.get(i).toString()); 31 | } 32 | } 33 | return queues; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetQueuesCountHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | 10 | import static org.slf4j.LoggerFactory.getLogger; 11 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 12 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 13 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 14 | import static org.swisspush.redisques.util.RedisquesAPI.VALUE; 15 | 16 | /** 17 | * Class GetQueuesCountHandler. 18 | * 19 | * @author Marc-André Weber 20 | */ 21 | public class GetQueuesCountHandler implements Handler> { 22 | 23 | private static final Logger log = getLogger(GetQueuesCountHandler.class); 24 | private final Message event; 25 | 26 | public GetQueuesCountHandler(Message event) { 27 | this.event = event; 28 | } 29 | 30 | @Override 31 | public void handle(AsyncResult reply) { 32 | if(reply.succeeded()){ 33 | Long queueCount = reply.result().toLong(); 34 | event.reply(new JsonObject().put(STATUS, OK).put(VALUE, queueCount)); 35 | } else { 36 | log.warn("Concealed error", new Exception(reply.cause())); 37 | event.reply(new JsonObject().put(STATUS, ERROR)); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetQueueItemsCountHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | 10 | import static org.slf4j.LoggerFactory.getLogger; 11 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 12 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 13 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 14 | import static org.swisspush.redisques.util.RedisquesAPI.VALUE; 15 | 16 | /** 17 | * Class GetQueueItemsCountHandler. 18 | * 19 | * @author Marc-André Weber 20 | */ 21 | public class GetQueueItemsCountHandler implements Handler> { 22 | 23 | private static final Logger log = getLogger(GetQueueItemsCountHandler.class); 24 | private final Message event; 25 | 26 | public GetQueueItemsCountHandler(Message event) { 27 | this.event = event; 28 | } 29 | 30 | @Override 31 | public void handle(AsyncResult reply) { 32 | if(reply.succeeded()){ 33 | Long queueItemCount = reply.result().toLong(); 34 | event.reply(new JsonObject().put(STATUS, OK).put(VALUE, queueItemCount)); 35 | } else { 36 | log.warn("Concealed error", new Exception(reply.cause())); 37 | event.reply(new JsonObject().put(STATUS, ERROR)); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/metrics/LongTaskTimerSamplePair.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.metrics; 2 | 3 | import io.micrometer.core.instrument.LongTaskTimer; 4 | public class LongTaskTimerSamplePair { 5 | private LongTaskTimer longTaskTimer; 6 | private LongTaskTimer.Sample sample; 7 | 8 | 9 | public LongTaskTimerSamplePair(LongTaskTimer longTaskTimer, LongTaskTimer.Sample sample) { 10 | this.longTaskTimer = longTaskTimer; 11 | this.sample = sample; 12 | } 13 | 14 | public LongTaskTimer getLongTaskTimer() { 15 | return longTaskTimer; 16 | } 17 | 18 | public void setLongTaskTimer(LongTaskTimer longTaskTimer) { 19 | this.longTaskTimer = longTaskTimer; 20 | } 21 | 22 | public LongTaskTimer.Sample getSample() { 23 | return sample; 24 | } 25 | 26 | public void setSample(LongTaskTimer.Sample sample) { 27 | this.sample = sample; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "LongTaskTimerPair{" + 33 | "longTaskTimer=" + longTaskTimer + 34 | ", sample=" + sample + 35 | '}'; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object o) { 40 | if (this == o) return true; 41 | if (!(o instanceof LongTaskTimerSamplePair)) return false; 42 | LongTaskTimerSamplePair that = (LongTaskTimerSamplePair) o; 43 | return longTaskTimer.equals(that.longTaskTimer) && 44 | sample.equals(that.sample); 45 | } 46 | 47 | @Override 48 | public int hashCode() { 49 | return java.util.Objects.hash(longTaskTimer, sample); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/RedisQuesTimer.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.Promise; 5 | import io.vertx.core.Vertx; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.util.Random; 10 | 11 | /** 12 | * Utility class for the vertx timer functionalities. 13 | * 14 | * @author Marc-André Weber 15 | */ 16 | public class RedisQuesTimer { 17 | private final Vertx vertx; 18 | private final Random random; 19 | 20 | private final Logger log = LoggerFactory.getLogger(RedisQuesTimer.class); 21 | 22 | public RedisQuesTimer(Vertx vertx) { 23 | this.vertx = vertx; 24 | this.random = new Random(); 25 | } 26 | 27 | /** 28 | * Delay an operation by providing a delay in milliseconds. This method completes the {@link Future} any time 29 | * between immediately and the delay. When 0 provided as delay, the {@link Future} is resolved immediately. 30 | * 31 | * @param delayMs the delay in milliseconds 32 | * @return A {@link Future} which completes after the delay 33 | */ 34 | public Future executeDelayedMax(long delayMs) { 35 | Promise promise = Promise.promise(); 36 | 37 | if (delayMs > 0) { 38 | int delay = random.nextInt((int) (delayMs + 1)) + 1; 39 | log.debug("starting timer with a delay of {}ms", delay); 40 | vertx.setTimer(delay, delayed -> promise.complete()); 41 | } else { 42 | vertx.runOnContext(aVoid -> promise.complete()); 43 | } 44 | 45 | return promise.future(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetQueueItemAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetQueueItemHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.QueueConfiguration; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.List; 15 | 16 | import static org.swisspush.redisques.util.RedisquesAPI.*; 17 | 18 | public class GetQueueItemAction extends AbstractQueueAction { 19 | 20 | 21 | public GetQueueItemAction( 22 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 23 | List queueConfigurations, RedisQuesExceptionFactory exceptionFactory, 24 | QueueStatisticsCollector queueStatisticsCollector, Logger log 25 | ) { 26 | super(vertx, redisService, keyspaceHelper, 27 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 28 | } 29 | 30 | @Override 31 | public void execute(Message event) { 32 | String key = keyspaceHelper.getQueuesPrefix() + event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 33 | int index = event.body().getJsonObject(PAYLOAD).getInteger(INDEX); 34 | 35 | redisService.lindex(key, String.valueOf(index)).onComplete(response -> new GetQueueItemHandler(event, exceptionFactory).handle(response)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/exception/WastefulRedisQuesExceptionFactory.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.exception; 2 | 3 | import io.vertx.core.eventbus.ReplyException; 4 | import io.vertx.core.eventbus.ReplyFailure; 5 | 6 | /** 7 | * Trades speed for maintainability. For example invests more resources like 8 | * recording stack traces (which likely provocates more logs) to get easier 9 | * to debug error messages and better hints of what is happening. It also 10 | * keeps details like 'causes' and 'suppressed' exceptions. If an app needs 11 | * more error details it should use {@link WastefulRedisQuesExceptionFactory}. If none 12 | * of those fits the apps needs, it can provide its own implementation. 13 | */ 14 | class WastefulRedisQuesExceptionFactory implements RedisQuesExceptionFactory { 15 | 16 | WastefulRedisQuesExceptionFactory() { 17 | } 18 | 19 | public Exception newException(String message, Throwable cause) { 20 | return new Exception(message, cause); 21 | } 22 | 23 | @Override 24 | public RuntimeException newRuntimeException(String message, Throwable cause) { 25 | return new RuntimeException(message, cause); 26 | } 27 | 28 | @Override 29 | public ReplyException newReplyException(int failureCode, String msg, Throwable cause) { 30 | if (msg == null && cause != null) msg = cause.getMessage(); 31 | ReplyException ex = new ReplyException(ReplyFailure.RECIPIENT_FAILURE, failureCode, msg); 32 | if (cause != null) ex.initCause(cause); 33 | return ex; 34 | } 35 | 36 | @Override 37 | public ResourceExhaustionException newResourceExhaustionException(String msg, Throwable cause) { 38 | return new ResourceExhaustionException(msg, cause); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetQueueItemsCountAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetQueueItemsCountHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.QueueConfiguration; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.List; 15 | 16 | import static org.swisspush.redisques.util.RedisquesAPI.PAYLOAD; 17 | import static org.swisspush.redisques.util.RedisquesAPI.QUEUENAME; 18 | 19 | /** 20 | * Retrieve the number of items of the requested queue 21 | */ 22 | public class GetQueueItemsCountAction extends AbstractQueueAction { 23 | 24 | public GetQueueItemsCountAction( 25 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 26 | List queueConfigurations, RedisQuesExceptionFactory exceptionFactory, 27 | QueueStatisticsCollector queueStatisticsCollector, Logger log 28 | ) { 29 | super(vertx, redisService, keyspaceHelper, 30 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 31 | } 32 | 33 | @Override 34 | public void execute(Message event) { 35 | String queue = event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 36 | redisService.llen(keyspaceHelper.getQueuesPrefix() + queue).onComplete(response -> new GetQueueItemsCountHandler(event).handle(response)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/DeleteAllLocksActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.unit.TestContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.swisspush.redisques.util.RedisquesAPI.buildDeleteAllLocksOperation; 19 | 20 | /** 21 | * Tests for {@link DeleteAllLocksAction} class. 22 | * 23 | * @author Marc-André Weber 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class DeleteAllLocksActionTest extends AbstractQueueActionTest { 27 | 28 | @Before 29 | @Override 30 | public void setup() { 31 | super.setup(); 32 | action = new DeleteAllLocksAction(vertx, redisService, keyspaceHelper, 33 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 34 | } 35 | 36 | @Test 37 | public void testDeleteAllLocksWhenRedisIsNotReady(TestContext context){ 38 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 39 | when(message.body()).thenReturn(buildDeleteAllLocksOperation()); 40 | 41 | action.execute(message); 42 | 43 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\",\"message\":\"not ready\"}")))); 44 | verifyNoInteractions(redisAPI); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetQueueItemsCountActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.unit.TestContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetQueueItemsCountOperation; 19 | 20 | /** 21 | * Tests for {@link GetQueueItemsCountAction} class. 22 | * 23 | * @author Marc-André Weber 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class GetQueueItemsCountActionTest extends AbstractQueueActionTest { 27 | 28 | @Before 29 | @Override 30 | public void setup() { 31 | super.setup(); 32 | action = new GetQueueItemsCountAction(vertx, redisService, keyspaceHelper, 33 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 34 | } 35 | 36 | @Test 37 | public void testGetQueueItemsCountWhenRedisIsNotReady(TestContext context){ 38 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 39 | when(message.body()).thenReturn(buildGetQueueItemsCountOperation("q1")); 40 | 41 | action.execute(message); 42 | 43 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 44 | verifyNoInteractions(redisAPI); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/metrics/MetricsCollectorScheduler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.metrics; 2 | 3 | import io.vertx.core.Vertx; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class MetricsCollectorScheduler { 8 | 9 | private final MetricsCollector metricsCollector; 10 | private static final Logger log = LoggerFactory.getLogger(MetricsCollectorScheduler.class); 11 | 12 | public MetricsCollectorScheduler(Vertx vertx, MetricsCollector metricsCollector, long collectIntervalSec) { 13 | this.metricsCollector = metricsCollector; 14 | 15 | long collectIntervalMs = collectIntervalSec * 1000; 16 | 17 | vertx.setPeriodic(collectIntervalMs, event -> { 18 | updateActiveQueuesCount(); 19 | updateMaxQueueSize(); 20 | updateMyQueuesStateCount(); 21 | }); 22 | } 23 | 24 | private void updateActiveQueuesCount() { 25 | metricsCollector.updateActiveQueuesCount().onComplete(updateEvent -> { 26 | if(updateEvent.failed()) { 27 | log.warn("Failed to update active queues count", updateEvent.cause()); 28 | } 29 | }); 30 | } 31 | 32 | private void updateMaxQueueSize() { 33 | metricsCollector.updateMaxQueueSize().onComplete(updateEvent -> { 34 | if(updateEvent.failed()) { 35 | log.warn("Failed to update max queue size", updateEvent.cause()); 36 | } 37 | }); 38 | } 39 | 40 | private void updateMyQueuesStateCount() { 41 | metricsCollector.updateMyQueuesStateCount().onComplete(updateEvent -> { 42 | if(updateEvent.failed()) { 43 | log.warn("Failed to update queue state count", updateEvent.cause()); 44 | } 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetLockHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 10 | 11 | import static org.slf4j.LoggerFactory.getLogger; 12 | import static org.swisspush.redisques.util.RedisquesAPI.NO_SUCH_LOCK; 13 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 14 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 15 | import static org.swisspush.redisques.util.RedisquesAPI.VALUE; 16 | 17 | /** 18 | * Class GetLockHandler. 19 | * 20 | * @author baldim 21 | */ 22 | public class GetLockHandler implements Handler> { 23 | 24 | private static final Logger log = getLogger(GetLockHandler.class); 25 | private final Message event; 26 | private final RedisQuesExceptionFactory exceptionFactory; 27 | 28 | public GetLockHandler(Message event, RedisQuesExceptionFactory exceptionFactory) { 29 | this.event = event; 30 | this.exceptionFactory = exceptionFactory; 31 | } 32 | 33 | @Override 34 | public void handle(AsyncResult reply) { 35 | if (reply.succeeded()) { 36 | if (reply.result() != null) { 37 | event.reply(new JsonObject().put(STATUS, OK).put(VALUE, reply.result().toString())); 38 | } else { 39 | event.reply(new JsonObject().put(STATUS, NO_SUCH_LOCK)); 40 | } 41 | } else { 42 | event.reply(exceptionFactory.newReplyException("Operation GetLockAction failed", reply.cause())); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/BulkPutLocksActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.Mockito; 13 | import org.slf4j.Logger; 14 | import org.swisspush.redisques.util.QueueStatisticsCollector; 15 | 16 | import java.util.ArrayList; 17 | 18 | import static org.mockito.Mockito.*; 19 | import static org.swisspush.redisques.util.RedisquesAPI.buildBulkPutLocksOperation; 20 | 21 | /** 22 | * Tests for {@link BulkPutLocksAction} class. 23 | * 24 | * @author Marc-André Weber 25 | */ 26 | @RunWith(VertxUnitRunner.class) 27 | public class BulkPutLocksActionTest extends AbstractQueueActionTest { 28 | 29 | @Before 30 | @Override 31 | public void setup() { 32 | super.setup(); 33 | action = new BulkPutLocksAction(vertx, redisService, keyspaceHelper, 34 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 35 | } 36 | 37 | @Test 38 | public void testBulkPutLocksWhenRedisIsNotReady(TestContext context){ 39 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 40 | when(message.body()).thenReturn(buildBulkPutLocksOperation(new JsonArray().add("q1").add("q2").add("q3"), "geronimo")); 41 | 42 | action.execute(message); 43 | 44 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 45 | verifyNoInteractions(redisAPI); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetQueueItemHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 10 | 11 | import static org.slf4j.LoggerFactory.getLogger; 12 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 13 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 14 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 15 | import static org.swisspush.redisques.util.RedisquesAPI.VALUE; 16 | 17 | /** 18 | * Class GetQueueItemHandler. 19 | * 20 | * @author baldim, Marc-André Weber 21 | */ 22 | public class GetQueueItemHandler implements Handler> { 23 | 24 | private static final Logger log = getLogger(GetQueueItemHandler.class); 25 | private final Message event; 26 | private final RedisQuesExceptionFactory exceptionFactory; 27 | 28 | public GetQueueItemHandler(Message event, RedisQuesExceptionFactory exceptionFactory) { 29 | this.event = event; 30 | this.exceptionFactory = exceptionFactory; 31 | } 32 | 33 | @Override 34 | public void handle(AsyncResult reply) { 35 | if(reply.failed()) { 36 | event.reply(exceptionFactory.newReplyException("Operation GetQueueItemAction failed", reply.cause())); 37 | } else if (reply.result() != null) { 38 | event.reply(new JsonObject().put(STATUS, OK).put(VALUE, reply.result().toString())); 39 | } else { 40 | event.reply(new JsonObject().put(STATUS, ERROR)); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/lock/Lock.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.lock; 2 | 3 | import io.vertx.core.Future; 4 | 5 | /** 6 | * Cluster wide locks allow you to obtain exclusive locks across the cluster. 7 | * This is useful when you want to do something or access a resource on only one node of a cluster at any one time. 8 | * 9 | * @author https://github.com/mcweba [Marc-Andre Weber] 10 | */ 11 | public interface Lock { 12 | /** 13 | * Try to acquire a lock. 14 | * The token parameter value must be unique across all clients and all lock requests. The lockExpiryMs 15 | * parameter defines the expiry of the lock. 16 | * When not manually released, the lock will be released automatically when expired. 17 | * 18 | * @param lock The name of the lock to acquire 19 | * @param token A unique token to define the owner of the lock 20 | * @param lockExpiryMs The lock expiry in milliseconds 21 | * @return Returns a Future holding a Boolean value whether the lock could be successfully acquired or not 22 | */ 23 | Future acquireLock(String lock, String token, long lockExpiryMs); 24 | 25 | /** 26 | * Try to release a lock. 27 | * The token parameter value is used to verify that only the owner of the lock can release it. 28 | * The token parameter value also prevents the original owner of an already expired lock to release a lock 29 | * which has been acquired by another client. 30 | * 31 | * @param lock The name of the lock to release 32 | * @param token A unique token to verify if the owner of the lock tries to release the lock 33 | * @return Returns a Promise holding a Boolean value whether the lock could be successfully released or not 34 | */ 35 | Future releaseLock(String lock, String token); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/StatusCode.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | /** 4 | * Enum for HTTP status codes 5 | * 6 | * @author Marc-André Weber 7 | */ 8 | public enum StatusCode { 9 | OK(200, "OK"), 10 | ACCEPTED(202, "Accepted"), 11 | NOT_MODIFIED(304, "Not Modified"), 12 | BAD_REQUEST(400, "Bad Request"), 13 | FORBIDDEN(403, "Forbidden"), 14 | NOT_FOUND(404, "Not Found"), 15 | METHOD_NOT_ALLOWED(405, "Method Not Allowed"), 16 | CONFLICT(409, "Conflict"), 17 | TOO_MANY_REQUESTS(429, "Too Many Requests"), 18 | INTERNAL_SERVER_ERROR(500, "Internal Server Error"), 19 | SERVICE_UNAVAILABLE(503, "Service Unavailable"), 20 | TIMEOUT(504,"Gateway Timeout"), 21 | INSUFFICIENT_STORAGE(507, "Insufficient Storage"); 22 | 23 | private final int statusCode; 24 | private final String statusMessage; 25 | 26 | StatusCode(int statusCode, String statusMessage) { 27 | this.statusCode = statusCode; 28 | this.statusMessage = statusMessage; 29 | } 30 | 31 | public int getStatusCode() { 32 | return statusCode; 33 | } 34 | 35 | public String getStatusMessage() { 36 | return statusMessage; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return statusCode + " " + statusMessage; 42 | } 43 | 44 | /** 45 | * Returns the enum StatusCode which matches the specified http status code. 46 | * 47 | * @param code code 48 | * @return The matching StatusCode or null if none matches. 49 | */ 50 | public static StatusCode fromCode(int code) { 51 | for (StatusCode statuscode : values()) { 52 | if (statuscode.getStatusCode() == code) { 53 | return statuscode; 54 | } 55 | } 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/BulkDeleteLocksActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.Mockito; 13 | import org.slf4j.Logger; 14 | import org.swisspush.redisques.util.QueueStatisticsCollector; 15 | 16 | import java.util.ArrayList; 17 | 18 | import static org.mockito.Mockito.*; 19 | import static org.swisspush.redisques.util.RedisquesAPI.buildBulkDeleteLocksOperation; 20 | 21 | /** 22 | * Tests for {@link BulkDeleteLocksAction} class. 23 | * 24 | * @author Marc-André Weber 25 | */ 26 | @RunWith(VertxUnitRunner.class) 27 | public class BulkDeleteLocksActionTest extends AbstractQueueActionTest { 28 | 29 | @Before 30 | @Override 31 | public void setup() { 32 | super.setup(); 33 | action = new BulkDeleteLocksAction(vertx, redisService, keyspaceHelper, 34 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 35 | } 36 | 37 | @Test 38 | public void testBulkDeleteLocksWhenRedisIsNotReady(TestContext context){ 39 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 40 | when(message.body()).thenReturn(buildBulkDeleteLocksOperation(new JsonArray().add("q1").add("q3"))); 41 | 42 | action.execute(message); 43 | 44 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\",\"message\":\"not ready\"}")))); 45 | verifyNoInteractions(redisAPI); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetAllLocksAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetAllLocksHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.*; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.regex.Pattern; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.*; 18 | 19 | public class GetAllLocksAction extends AbstractQueueAction { 20 | 21 | public GetAllLocksAction( 22 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 23 | List queueConfigurations, RedisQuesExceptionFactory exceptionFactory, 24 | QueueStatisticsCollector queueStatisticsCollector, Logger log 25 | ) { 26 | super(vertx, redisService, keyspaceHelper, 27 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 28 | } 29 | 30 | @Override 31 | public void execute(Message event) { 32 | Result, String> result = MessageUtil.extractFilterPattern(event); 33 | if (result.isOk()) { 34 | redisService.hkeys(keyspaceHelper.getLocksKey()).onComplete(response -> new GetAllLocksHandler(exceptionFactory, event, result.getOk()).handle(response)) 35 | .onFailure(throwable -> handleFail(event, "Operation GetAllLocks failed", throwable)); 36 | } else { 37 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT).put(MESSAGE, result.getErr())); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/ReplaceQueueItemAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.ReplaceQueueItemHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.QueueConfiguration; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.List; 15 | 16 | import static org.swisspush.redisques.util.RedisquesAPI.*; 17 | 18 | public class ReplaceQueueItemAction extends AbstractQueueAction { 19 | 20 | public ReplaceQueueItemAction( 21 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 22 | List queueConfigurations, 23 | RedisQuesExceptionFactory exceptionFactory, 24 | QueueStatisticsCollector queueStatisticsCollector, Logger log 25 | ) { 26 | super(vertx, redisService, keyspaceHelper, queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 27 | } 28 | 29 | @Override 30 | public void execute(Message event) { 31 | String keyReplaceItem = keyspaceHelper.getQueuesPrefix() + event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 32 | int indexReplaceItem = event.body().getJsonObject(PAYLOAD).getInteger(INDEX); 33 | String bufferReplaceItem = event.body().getJsonObject(PAYLOAD).getString(BUFFER); 34 | redisService.lset(keyReplaceItem, String.valueOf(indexReplaceItem), 35 | bufferReplaceItem).onComplete(response -> new ReplaceQueueItemHandler(event, exceptionFactory).handle(response)); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/ResourcesUtils.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import com.google.common.base.Charsets; 4 | import com.google.common.io.Resources; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.net.URL; 9 | 10 | /** 11 | *

12 | * Utility class providing handy methods to deal with Resources. 13 | *

14 | * 15 | * @author Marc-André Weber 16 | */ 17 | public class ResourcesUtils { 18 | 19 | private static final Logger log = LoggerFactory.getLogger(ResourcesUtils.class); 20 | 21 | private ResourcesUtils() { 22 | // prevent instantiation 23 | } 24 | 25 | /** 26 | *

27 | * Loads the resource with the provided name from the classpath. When param {@code exceptionWhenNotFound} 28 | * set to true, a {@link RuntimeException} is thrown when the resource cannot be loaded. 29 | *

30 | * 31 | * @param resourceName the name of the resource to load 32 | * @param exceptionWhenNotFound throw a {@link RuntimeException} when the resource could not be loaded 33 | * @throws RuntimeException when {@code exceptionWhenNotFound} is set to true and resource cannot be loaded 34 | * @return The content of the resource or null 35 | */ 36 | public static String loadUtf8ResourceAsString(String resourceName, boolean exceptionWhenNotFound) { 37 | try { 38 | URL url = Resources.getResource(resourceName); 39 | return Resources.toString(url, Charsets.UTF_8); 40 | } catch (Exception e) { 41 | if(exceptionWhenNotFound){ 42 | throw new RuntimeException("Error loading required resource '"+resourceName+"'"); 43 | } else { 44 | log.error("Error loading resource '{}'", resourceName, e); 45 | return null; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/DeleteAllLocksAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.redis.client.Response; 7 | import org.slf4j.Logger; 8 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.QueueConfiguration; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.List; 15 | 16 | import static org.swisspush.redisques.util.RedisquesAPI.MESSAGE; 17 | 18 | public class DeleteAllLocksAction extends AbstractQueueAction { 19 | 20 | public DeleteAllLocksAction( 21 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 22 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log 23 | ) { 24 | super(vertx, redisService, keyspaceHelper, 25 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 26 | } 27 | 28 | @Override 29 | public void execute(Message event) { 30 | redisService.hkeys(keyspaceHelper.getLocksKey()).onComplete(locksResult -> { 31 | if (locksResult.succeeded()) { 32 | Response locks = locksResult.result(); 33 | deleteLocks(event, locks); 34 | } else { 35 | replyError(event, locksResult.cause()); 36 | } 37 | }); 38 | } 39 | 40 | private void replyError(Message event, Throwable ex) { 41 | if (log.isWarnEnabled()) log.warn("failed to delete all locks.", new Exception(ex)); 42 | event.reply(createErrorReply().put(MESSAGE, ex.getMessage())); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetLockAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import org.slf4j.Logger; 8 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 9 | import org.swisspush.redisques.handler.GetLockHandler; 10 | import org.swisspush.redisques.queue.KeyspaceHelper; 11 | import org.swisspush.redisques.queue.RedisService; 12 | import org.swisspush.redisques.util.QueueConfiguration; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.List; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.PAYLOAD; 18 | import static org.swisspush.redisques.util.RedisquesAPI.QUEUENAME; 19 | 20 | public class GetLockAction extends AbstractQueueAction { 21 | 22 | public GetLockAction( 23 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 24 | List queueConfigurations, RedisQuesExceptionFactory exceptionFactory, 25 | QueueStatisticsCollector queueStatisticsCollector, Logger log 26 | ) { 27 | super(vertx, redisService, keyspaceHelper, 28 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 29 | } 30 | 31 | @Override 32 | public void execute(Message event) { 33 | final JsonObject body = event.body(); 34 | if (body == null) { 35 | replyErrorMessageHandler(event).handle(new NullPointerException("Got msg with no body from event bus. address=" + 36 | event.address() + " replyAddress=" + event.replyAddress())); 37 | return; 38 | } 39 | redisService.hget(keyspaceHelper.getLocksKey(), body.getJsonObject(PAYLOAD).getString(QUEUENAME)) 40 | .onComplete(response -> new GetLockHandler(event, exceptionFactory).handle(response)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetQueuesItemsCountActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.unit.TestContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 14 | import org.swisspush.redisques.util.QueueStatisticsCollector; 15 | 16 | import java.util.ArrayList; 17 | import java.util.concurrent.Semaphore; 18 | 19 | import static org.mockito.Mockito.*; 20 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetQueuesItemsCountOperation; 21 | 22 | /** 23 | * Tests for {@link GetQueuesItemsCountAction} class. 24 | * 25 | * @author Marc-André Weber 26 | */ 27 | @RunWith(VertxUnitRunner.class) 28 | public class GetQueuesItemsCountActionTest extends AbstractQueueActionTest { 29 | 30 | @Before 31 | @Override 32 | public void setup() { 33 | super.setup(); 34 | action = new GetQueuesItemsCountAction(vertx, redisService, keyspaceHelper, 35 | new ArrayList<>(), Mockito.mock(RedisQuesExceptionFactory.class), Mockito.mock(Semaphore.class), 36 | Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 37 | } 38 | 39 | @Test 40 | public void testGetQueuesItemsCountWhenRedisIsNotReady(TestContext context){ 41 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 42 | when(message.body()).thenReturn(buildGetQueuesItemsCountOperation(null)); 43 | 44 | action.execute(message); 45 | 46 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 47 | verifyNoInteractions(redisAPI); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/ReplaceQueueItemHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 10 | 11 | import static org.slf4j.LoggerFactory.getLogger; 12 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 13 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 14 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 15 | 16 | /** 17 | * Class ReplaceQueueItemHandler. 18 | * 19 | * @author baldim, Marc-André Weber 20 | */ 21 | public class ReplaceQueueItemHandler implements Handler> { 22 | 23 | private static final Logger log = getLogger(ReplaceQueueItemHandler.class); 24 | private final Message event; 25 | private final RedisQuesExceptionFactory exceptionFactory; 26 | 27 | public ReplaceQueueItemHandler(Message event, RedisQuesExceptionFactory exceptionFactory) { 28 | this.event = event; 29 | this.exceptionFactory = exceptionFactory; 30 | } 31 | 32 | @Override 33 | public void handle(AsyncResult reply) { 34 | if(reply.succeeded()){ 35 | event.reply(new JsonObject().put(STATUS, OK)); 36 | } else if(checkRedisErrorCodes(reply.cause().getMessage())) { 37 | event.reply(new JsonObject().put(STATUS, ERROR)); 38 | } else { 39 | event.reply(exceptionFactory.newReplyException("Operation ReplaceQueueItemAction failed", reply.cause())); 40 | } 41 | } 42 | 43 | private boolean checkRedisErrorCodes(String message) { 44 | if(message == null) { 45 | return false; 46 | } 47 | return message.contains("index out of range"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/BulkDeleteLocksAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import io.vertx.redis.client.impl.types.MultiType; 9 | import io.vertx.redis.client.impl.types.SimpleStringType; 10 | import org.slf4j.Logger; 11 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 12 | import org.swisspush.redisques.queue.KeyspaceHelper; 13 | import org.swisspush.redisques.queue.RedisService; 14 | import org.swisspush.redisques.util.QueueConfiguration; 15 | import org.swisspush.redisques.util.QueueStatisticsCollector; 16 | 17 | import java.util.List; 18 | 19 | import static org.swisspush.redisques.util.RedisquesAPI.*; 20 | 21 | public class BulkDeleteLocksAction extends AbstractQueueAction { 22 | 23 | public BulkDeleteLocksAction(Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 24 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log) { 25 | super(vertx, redisService, keyspaceHelper, queueConfigurations, 26 | exceptionFactory, queueStatisticsCollector, log); 27 | } 28 | 29 | @Override 30 | public void execute(Message event) { 31 | JsonArray jsonArray = event.body().getJsonObject(PAYLOAD).getJsonArray(LOCKS); 32 | if (jsonArray != null) { 33 | MultiType locks = MultiType.create(jsonArray.size(), false); 34 | for (int j = 0; j < jsonArray.size(); j++) { 35 | Response response = SimpleStringType.create(jsonArray.getString(j)); 36 | locks.add(response); 37 | } 38 | deleteLocks(event, locks); 39 | } else { 40 | event.reply(createErrorReply().put(MESSAGE, "No locks to delete provided")); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/RedisUtils.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | import io.vertx.core.json.JsonArray; 3 | import io.vertx.core.json.JsonObject; 4 | 5 | import java.util.*; 6 | import java.util.stream.Stream; 7 | 8 | /** 9 | * Useful utilities for Redis 10 | * 11 | */ 12 | public final class RedisUtils { 13 | 14 | private RedisUtils() {} 15 | 16 | /** 17 | * from https://github.com/vert-x3/vertx-redis-client/blob/3.9/src/main/java/io/vertx/redis/impl/RedisClientImpl.java#L94 18 | * 19 | * @param parameters 20 | * @return 21 | */ 22 | public static List toPayload(Object... parameters) { 23 | List result = new ArrayList<>(parameters.length); 24 | 25 | for (Object param : parameters) { 26 | // unwrap 27 | if (param instanceof JsonArray) { 28 | param = ((JsonArray) param).getList(); 29 | } 30 | // unwrap 31 | if (param instanceof JsonObject) { 32 | param = ((JsonObject) param).getMap(); 33 | } 34 | 35 | if (param instanceof Collection) { 36 | ((Collection) param).stream().filter(Objects::nonNull).forEach(o -> result.add(o.toString())); 37 | } else if (param instanceof Map) { 38 | for (Map.Entry pair : ((Map) param).entrySet()) { 39 | result.add(pair.getKey().toString()); 40 | result.add(pair.getValue().toString()); 41 | } 42 | } else if (param instanceof Stream) { 43 | ((Stream) param).forEach(e -> { 44 | if (e instanceof Object[]) { 45 | Collections.addAll(result, (String[]) e); 46 | } else { 47 | result.add(e.toString()); 48 | } 49 | }); 50 | } else if (param != null) { 51 | result.add(param.toString()); 52 | } 53 | } 54 | return result; 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetQueuesAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetQueuesHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.*; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.regex.Pattern; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.*; 18 | 19 | public class GetQueuesAction extends AbstractQueueAction { 20 | 21 | public GetQueuesAction( 22 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 23 | List queueConfigurations, RedisQuesExceptionFactory exceptionFactory, 24 | QueueStatisticsCollector queueStatisticsCollector, Logger log 25 | ) { 26 | super(vertx, redisService, keyspaceHelper, 27 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 28 | } 29 | 30 | @Override 31 | public void execute(Message event) { 32 | Result, String> result = MessageUtil.extractFilterPattern(event); 33 | getQueues(event, false, result); 34 | } 35 | 36 | protected void getQueues(Message event, boolean countOnly, Result, String> filterPatternResult) { 37 | if (filterPatternResult.isErr()) { 38 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT).put(MESSAGE, filterPatternResult.getErr())); 39 | } else { 40 | redisService.zrangebyscore(keyspaceHelper.getQueuesKey(), String.valueOf(getMaxAgeTimestamp()), "+inf") 41 | .onComplete(response -> new GetQueuesHandler(event, filterPatternResult.getOk(), countOnly).handle(response)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetQueuesCountAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetQueuesCountHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.*; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.regex.Pattern; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.*; 18 | 19 | public class GetQueuesCountAction extends GetQueuesAction { 20 | 21 | public GetQueuesCountAction( 22 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 23 | List queueConfigurations, RedisQuesExceptionFactory exceptionFactory, 24 | QueueStatisticsCollector queueStatisticsCollector, Logger log 25 | ) { 26 | super(vertx, redisService, keyspaceHelper, 27 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 28 | } 29 | 30 | @Override 31 | public void execute(Message event) { 32 | Result, String> result = MessageUtil.extractFilterPattern(event); 33 | if (result.isErr()) { 34 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT).put(MESSAGE, result.getErr())); 35 | return; 36 | } 37 | 38 | /* 39 | * to filter values we have to use "getQueues" operation 40 | */ 41 | if (result.getOk().isPresent()) { 42 | getQueues(event, true, result); 43 | } else { 44 | redisService.zcount(keyspaceHelper.getQueuesKey(), String.valueOf(getMaxAgeTimestamp()), 45 | String.valueOf(Double.MAX_VALUE)).onComplete(response -> new GetQueuesCountHandler(event).handle(response)); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetQueueItemsHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonArray; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.redis.client.Response; 9 | import org.slf4j.Logger; 10 | 11 | import static org.slf4j.LoggerFactory.getLogger; 12 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 13 | import static org.swisspush.redisques.util.RedisquesAPI.INFO; 14 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 15 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 16 | import static org.swisspush.redisques.util.RedisquesAPI.VALUE; 17 | 18 | /** 19 | * Class GetQueueItemsHandler. 20 | * 21 | * @author baldim, Marc-André Weber 22 | */ 23 | public class GetQueueItemsHandler implements Handler> { 24 | 25 | private static final Logger log = getLogger(GetQueueItemsHandler.class); 26 | private final Message event; 27 | private final Long queueItemCount; 28 | 29 | public GetQueueItemsHandler(Message event, Long queueItemCount) { 30 | this.event = event; 31 | this.queueItemCount = queueItemCount; 32 | } 33 | 34 | @Override 35 | public void handle(AsyncResult reply) { 36 | if (reply.succeeded()) { 37 | Response result = reply.result(); 38 | JsonArray countInfo = new JsonArray(); 39 | if (result != null) { 40 | countInfo.add(result.size()); 41 | } 42 | countInfo.add(queueItemCount); 43 | JsonArray values = new JsonArray(); 44 | for (Response res : result) { 45 | values.add(res.toString()); 46 | } 47 | event.reply(new JsonObject().put(STATUS, OK).put(VALUE, values).put(INFO, countInfo)); 48 | } else { 49 | log.warn("Concealed error", new Exception(reply.cause())); 50 | event.reply(new JsonObject().put(STATUS, ERROR)); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetQueuesActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.unit.TestContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetQueuesOperation; 19 | 20 | /** 21 | * Tests for {@link GetQueuesAction} class. 22 | * 23 | * @author Marc-André Weber 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class GetQueuesActionTest extends AbstractQueueActionTest { 27 | 28 | @Before 29 | @Override 30 | public void setup() { 31 | super.setup(); 32 | action = new GetQueuesAction(vertx, redisService, keyspaceHelper, 33 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 34 | } 35 | 36 | @Test 37 | public void testGetQueuesWhenRedisIsNotReady(TestContext context){ 38 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 39 | when(message.body()).thenReturn(buildGetQueuesOperation(null)); 40 | 41 | action.execute(message); 42 | 43 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 44 | verifyNoInteractions(redisAPI); 45 | } 46 | 47 | @Test 48 | public void testGetQueuesWithFilterWhenRedisIsNotReady(TestContext context){ 49 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 50 | when(message.body()).thenReturn(buildGetQueuesOperation("abc")); 51 | 52 | action.execute(message); 53 | 54 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 55 | verifyNoInteractions(redisAPI); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/DeleteLockAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.DeleteLockHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.QueueConfiguration; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.Arrays; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | import static org.swisspush.redisques.util.RedisquesAPI.PAYLOAD; 19 | import static org.swisspush.redisques.util.RedisquesAPI.QUEUENAME; 20 | 21 | public class DeleteLockAction extends AbstractQueueAction { 22 | 23 | public DeleteLockAction( 24 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 25 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log 26 | ) { 27 | super(vertx, redisService, keyspaceHelper, 28 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 29 | } 30 | 31 | @Override 32 | public void execute(Message event) { 33 | String queueName = event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 34 | redisService.exists(Collections.singletonList(keyspaceHelper.getQueuesPrefix() + queueName)).onComplete(event1 -> { 35 | if (event1.failed()) { 36 | log.warn("Concealed error", exceptionFactory.newException(event1.cause())); 37 | } 38 | 39 | if (event1.succeeded() && event1.result() != null && event1.result().toInteger() == 1) { 40 | notifyConsumer(queueName); 41 | } 42 | 43 | redisService.hdel(Arrays.asList(keyspaceHelper.getLocksKey(), queueName)).onComplete(response -> new DeleteLockHandler(event, exceptionFactory).handle(response)); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/AddQueueItemAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.AddQueueItemHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.QueueConfiguration; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.List; 15 | 16 | import static org.swisspush.redisques.util.RedisquesAPI.*; 17 | 18 | public class AddQueueItemAction extends AbstractQueueAction { 19 | 20 | public AddQueueItemAction( 21 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 22 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log 23 | ) { 24 | super(vertx, redisService, keyspaceHelper, queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 25 | } 26 | 27 | @Override 28 | public void execute(Message event) { 29 | String queueName = event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 30 | String key = keyspaceHelper.getQueuesPrefix() + queueName; 31 | String valueAddItem = event.body().getJsonObject(PAYLOAD).getString(BUFFER); 32 | redisService.rpush(key, valueAddItem).onComplete(rpushResult -> { 33 | if (rpushResult.succeeded()) { 34 | processTrimRequestByState(queueName).onComplete(asyncResult -> { 35 | if (asyncResult.failed()) { 36 | log.warn("Failed to do the trim for {}", queueName); 37 | } 38 | new AddQueueItemHandler(event, exceptionFactory).handle(rpushResult); 39 | }); 40 | } else { 41 | handleFail(event, "Operation AddQueueItemAction failed", rpushResult.cause()); 42 | } 43 | }); 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/PutLockAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | import org.slf4j.Logger; 8 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 9 | import org.swisspush.redisques.handler.PutLockHandler; 10 | import org.swisspush.redisques.queue.KeyspaceHelper; 11 | import org.swisspush.redisques.queue.RedisService; 12 | import org.swisspush.redisques.util.QueueConfiguration; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.List; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.*; 18 | 19 | public class PutLockAction extends AbstractQueueAction { 20 | 21 | public PutLockAction( 22 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 23 | List queueConfigurations, 24 | RedisQuesExceptionFactory exceptionFactory, 25 | QueueStatisticsCollector queueStatisticsCollector, Logger log 26 | ) { 27 | super(vertx, redisService, keyspaceHelper, 28 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 29 | } 30 | 31 | @Override 32 | public void execute(Message event) { 33 | JsonObject lockInfo = extractLockInfo(event.body().getJsonObject(PAYLOAD).getString(REQUESTED_BY)); 34 | if (lockInfo != null) { 35 | JsonArray lockNames = new JsonArray().add(event.body().getJsonObject(PAYLOAD).getString(QUEUENAME)); 36 | if (!jsonArrayContainsStringsOnly(lockNames)) { 37 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT).put(MESSAGE, "Lock must be a string value")); 38 | return; 39 | } 40 | redisService.hmset(buildLocksItems(keyspaceHelper.getLocksKey(), lockNames, lockInfo)).onComplete(response -> 41 | new PutLockHandler(event, exceptionFactory).handle(response)); 42 | } else { 43 | event.reply(createErrorReply().put(MESSAGE, "Property '" + REQUESTED_BY + "' missing")); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetQueuesCountActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.unit.TestContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetQueuesCountOperation; 19 | 20 | /** 21 | * Tests for {@link GetQueuesCountAction} class. 22 | * 23 | * @author Marc-André Weber 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class GetQueuesCountActionTest extends AbstractQueueActionTest { 27 | 28 | @Before 29 | @Override 30 | public void setup() { 31 | super.setup(); 32 | action = new GetQueuesCountAction(vertx, redisService, keyspaceHelper, 33 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 34 | } 35 | 36 | @Test 37 | public void testGetQueuesCountWhenRedisIsNotReady(TestContext context){ 38 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 39 | when(message.body()).thenReturn(buildGetQueuesCountOperation(null)); 40 | 41 | action.execute(message); 42 | 43 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 44 | verifyNoInteractions(redisAPI); 45 | } 46 | 47 | @Test 48 | public void testGetQueuesCountWithFilterWhenRedisIsNotReady(TestContext context){ 49 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 50 | when(message.body()).thenReturn(buildGetQueuesCountOperation("abc")); 51 | 52 | action.execute(message); 53 | 54 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 55 | verifyNoInteractions(redisAPI); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetQueuesSpeedActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.unit.TestContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetQueuesSpeedOperation; 19 | 20 | /** 21 | * Tests for {@link GetQueuesSpeedAction} class. 22 | * 23 | * @author Marc-André Weber 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class GetQueuesSpeedActionTest extends AbstractQueueActionTest { 27 | 28 | @Before 29 | @Override 30 | public void setup() { 31 | super.setup(); 32 | action = new GetQueuesSpeedAction(vertx, redisService, keyspaceHelper, 33 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 34 | } 35 | 36 | @Test 37 | public void testGetQueuesSpeedWhenRedisIsNotReady(TestContext context){ 38 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 39 | when(message.body()).thenReturn(buildGetQueuesSpeedOperation(null)); 40 | 41 | action.execute(message); 42 | 43 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 44 | verifyNoInteractions(redisAPI); 45 | } 46 | 47 | @Test 48 | public void testGetQueuesSpeedWithFilterWhenRedisIsNotReady(TestContext context){ 49 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 50 | when(message.body()).thenReturn(buildGetQueuesSpeedOperation("abc")); 51 | 52 | action.execute(message); 53 | 54 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 55 | verifyNoInteractions(redisAPI); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetQueuesSpeedHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.swisspush.redisques.util.HandlerUtil; 11 | import org.swisspush.redisques.util.QueueStatisticsCollector; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.regex.Pattern; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 18 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 19 | 20 | /** 21 | * Retrieves in it's AsyncResult handler the speed summary of the queues matching the given filter 22 | * and returns the same in the event completion. 23 | */ 24 | public class GetQueuesSpeedHandler implements Handler> { 25 | 26 | private static final Logger log = LoggerFactory.getLogger(GetQueuesSpeedHandler.class); 27 | private final Message event; 28 | private final Optional filterPattern; 29 | private final QueueStatisticsCollector queueStatisticsCollector; 30 | 31 | public GetQueuesSpeedHandler( 32 | Message event, 33 | Optional filterPattern, 34 | QueueStatisticsCollector queueStatisticsCollector 35 | ) { 36 | this.event = event; 37 | this.filterPattern = filterPattern; 38 | this.queueStatisticsCollector = queueStatisticsCollector; 39 | } 40 | 41 | @Override 42 | public void handle(AsyncResult handleQueues) { 43 | if (handleQueues.succeeded()) { 44 | // apply the given filter in order to have only the queues for which we ar interested 45 | List queues = HandlerUtil 46 | .filterByPattern(handleQueues.result(), filterPattern); 47 | queueStatisticsCollector.getQueuesSpeed(event, queues); 48 | } else { 49 | log.warn("Concealed error", new Exception(handleQueues.cause())); 50 | event.reply(new JsonObject().put(STATUS, ERROR)); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/exception/RedisQuesExceptionFactory.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.exception; 2 | 3 | import io.vertx.core.eventbus.ReplyException; 4 | 5 | 6 | /** 7 | * Applies dependency inversion for exception instantiation. 8 | * 9 | * This class did arise because we had different use cases in different 10 | * applications. One of them has the need to perform fine-grained error 11 | * reporting. Whereas in the other application this led to performance issues. 12 | * So now through this abstraction, both applications can choose the behavior 13 | * they need. 14 | * 15 | * If dependency-injection gets applied properly, an app can even provide its 16 | * custom implementation to fine-tune the exact behavior even further. 17 | */ 18 | public interface RedisQuesExceptionFactory { 19 | 20 | public default Exception newException(String message) { return newException(message, null); } 21 | 22 | public default Exception newException(Throwable cause) { return newException(null, cause); } 23 | 24 | public Exception newException(String message, Throwable cause); 25 | 26 | public default RuntimeException newRuntimeException(String message) { return newRuntimeException(message, null); } 27 | 28 | public default RuntimeException newRuntimeException(Throwable cause) { return newRuntimeException(null, cause); } 29 | 30 | public RuntimeException newRuntimeException(String message, Throwable cause); 31 | 32 | default ReplyException newReplyException(String msg, Throwable cause) { 33 | return newReplyException(0, msg, cause); 34 | } 35 | 36 | public ReplyException newReplyException(int failureCode, String msg, Throwable cause); 37 | 38 | 39 | /** 40 | * See {@link ThriftyRedisQuesExceptionFactory}. 41 | */ 42 | public static RedisQuesExceptionFactory newThriftyExceptionFactory() { 43 | return new ThriftyRedisQuesExceptionFactory(); 44 | } 45 | 46 | /** 47 | * See {@link WastefulRedisQuesExceptionFactory}. 48 | */ 49 | public static RedisQuesExceptionFactory newWastefulExceptionFactory() { 50 | return new WastefulRedisQuesExceptionFactory(); 51 | } 52 | 53 | ResourceExhaustionException newResourceExhaustionException(String msg, Throwable cause); 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetQueuesStatisticsActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.unit.TestContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetQueuesStatisticsOperation; 19 | 20 | /** 21 | * Tests for {@link GetQueuesStatisticsAction} class. 22 | * 23 | * @author Marc-André Weber 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class GetQueuesStatisticsActionTest extends AbstractQueueActionTest { 27 | 28 | @Before 29 | @Override 30 | public void setup() { 31 | super.setup(); 32 | action = new GetQueuesStatisticsAction(vertx, redisService, keyspaceHelper, 33 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 34 | } 35 | 36 | @Test 37 | public void testGetQueuesStatisticsWhenRedisIsNotReady(TestContext context){ 38 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 39 | when(message.body()).thenReturn(buildGetQueuesStatisticsOperation(null)); 40 | 41 | action.execute(message); 42 | 43 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 44 | verifyNoInteractions(redisAPI); 45 | } 46 | 47 | @Test 48 | public void testGetQueuesStatisticsFilterWhenRedisIsNotReady(TestContext context){ 49 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 50 | when(message.body()).thenReturn(buildGetQueuesStatisticsOperation("abc")); 51 | 52 | action.execute(message); 53 | 54 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 55 | verifyNoInteractions(redisAPI); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetAllLocksHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonArray; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.redis.client.Response; 9 | import org.slf4j.Logger; 10 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 11 | import org.swisspush.redisques.util.HandlerUtil; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.regex.Pattern; 16 | 17 | import static org.slf4j.LoggerFactory.getLogger; 18 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 19 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 20 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 21 | import static org.swisspush.redisques.util.RedisquesAPI.VALUE; 22 | 23 | /** 24 | * Class GetAllLocksHandler. 25 | * 26 | * @author baldim 27 | */ 28 | public class GetAllLocksHandler implements Handler> { 29 | 30 | private static final Logger log = getLogger(GetAllLocksHandler.class); 31 | private final Message event; 32 | private final Optional filterPattern; 33 | private final RedisQuesExceptionFactory exceptionFactory; 34 | 35 | public GetAllLocksHandler(RedisQuesExceptionFactory exceptionFactory, Message event, 36 | Optional filterPattern) { 37 | this.exceptionFactory = exceptionFactory; 38 | this.event = event; 39 | this.filterPattern = filterPattern; 40 | } 41 | 42 | @Override 43 | public void handle(AsyncResult reply) { 44 | if (reply.succeeded() && reply.result() != null) { 45 | JsonObject result = new JsonObject(); 46 | Response locks = reply.result(); 47 | List filteredLocks = HandlerUtil.filterByPattern(locks, filterPattern); 48 | result.put("locks", new JsonArray(filteredLocks)); 49 | event.reply(new JsonObject().put(STATUS, OK).put(VALUE, result)); 50 | } else { 51 | if( reply.failed() ) log.warn("Concealed error", exceptionFactory.newException(reply.cause())); 52 | event.reply(new JsonObject().put(STATUS, ERROR)); 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | pull_request: 10 | branches: [ "master", "develop" ] 11 | 12 | jobs: 13 | build_maven: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up JDK 11 19 | uses: actions/setup-java@v3 20 | with: 21 | distribution: 'temurin' 22 | java-version: '11' 23 | server-id: central 24 | server-username: CI_DEPLOY_USERNAME # env variable for username in deploy 25 | server-password: CI_DEPLOY_PASSWORD # env variable for token in deploy 26 | gpg-private-key: ${{ secrets.CI_GPG_PRIVATE_KEY }} # Value of the GPG private key to import 27 | gpg-passphrase: CI_GPG_PASSPHRASE # env variable for GPG private key passphrase 28 | 29 | - name: Start Redis 30 | uses: supercharge/redis-github-action@1.4.0 31 | with: 32 | redis-version: 4 33 | 34 | - name: Set Git user 35 | run: | 36 | git config --global user.name "github-actions[bot]" 37 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 38 | 39 | - name: Install, unit test, integration test 40 | run: mvn install -Dmaven.javadoc.skip=true -B -V 41 | 42 | - name: Upload coverage reports to Codecov 43 | uses: codecov/codecov-action@v3 44 | 45 | - name: Release to maven central 46 | if: github.ref_name == 'master' && github.event_name != 'pull_request' && github.repository == 'swisspost/vertx-redisques' 47 | run: | 48 | curl -s https://get.sdkman.io | bash 49 | source "$HOME/.sdkman/bin/sdkman-init.sh" 50 | 51 | chmod +x ./maybe-release.sh 52 | ./maybe-release.sh 53 | env: 54 | CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }} 55 | CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }} 56 | CI_GPG_PASSPHRASE: ${{ secrets.CI_GPG_PASSPHRASE }} 57 | - name: After release 58 | run: bash <(curl -s https://codecov.io/bash) 59 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetQueuesSpeedAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetQueuesSpeedHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.*; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.regex.Pattern; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.*; 18 | 19 | /** 20 | * Retrieve the summarized queue speed of the requested queues 21 | */ 22 | public class GetQueuesSpeedAction extends AbstractQueueAction { 23 | 24 | public GetQueuesSpeedAction( 25 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 26 | List queueConfigurations, RedisQuesExceptionFactory exceptionFactory, 27 | QueueStatisticsCollector queueStatisticsCollector, Logger log 28 | ) { 29 | super(vertx, redisService, keyspaceHelper, 30 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 31 | } 32 | 33 | @Override 34 | public void execute(Message event) { 35 | Result, String> filterPattern = MessageUtil.extractFilterPattern(event); 36 | getQueuesSpeed(event, filterPattern); 37 | } 38 | 39 | /** 40 | * Retrieve the summarized queue speed of the requested queues filtered by the 41 | * given filter pattern. 42 | */ 43 | private void getQueuesSpeed(Message event, 44 | Result, String> filterPattern) { 45 | if (filterPattern.isErr()) { 46 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT) 47 | .put(MESSAGE, filterPattern.getErr())); 48 | } else { 49 | redisService.zrangebyscore(keyspaceHelper.getQueuesKey(), String.valueOf(getMaxAgeTimestamp()), "+inf").onComplete(response -> 50 | new GetQueuesSpeedHandler(event, filterPattern.getOk(), 51 | queueStatisticsCollector).handle(response)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetQueuesHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonArray; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.redis.client.Response; 9 | import org.slf4j.Logger; 10 | import org.swisspush.redisques.util.HandlerUtil; 11 | 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.regex.Pattern; 15 | 16 | import static org.slf4j.LoggerFactory.getLogger; 17 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 18 | import static org.swisspush.redisques.util.RedisquesAPI.OK; 19 | import static org.swisspush.redisques.util.RedisquesAPI.QUEUES; 20 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 21 | import static org.swisspush.redisques.util.RedisquesAPI.VALUE; 22 | 23 | /** 24 | * @author Marc-André Weber 25 | */ 26 | public class GetQueuesHandler implements Handler> { 27 | 28 | private static final Logger log = getLogger(GetQueuesHandler.class); 29 | private final Message event; 30 | private final Optional filterPattern; 31 | private final boolean countOnly; 32 | 33 | public GetQueuesHandler(Message event, Optional filterPattern, boolean countOnly) { 34 | this.event = event; 35 | this.filterPattern = filterPattern; 36 | this.countOnly = countOnly; 37 | } 38 | 39 | @Override 40 | public void handle(AsyncResult reply) { 41 | if(reply.succeeded()){ 42 | JsonObject jsonRes = new JsonObject(); 43 | Response queues = reply.result(); 44 | List filteredQueues = HandlerUtil.filterByPattern(queues, filterPattern); 45 | jsonRes.put(QUEUES, new JsonArray(filteredQueues)); 46 | if(countOnly){ 47 | event.reply(new JsonObject().put(STATUS, OK).put(VALUE, jsonRes.getJsonArray(QUEUES).size())); 48 | } else { 49 | event.reply(new JsonObject().put(STATUS, OK).put(VALUE, jsonRes)); 50 | } 51 | } else { 52 | log.warn("Concealed error", new Exception(reply.cause())); 53 | event.reply(new JsonObject().put(STATUS, ERROR)); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/GetQueuesStatisticsHandler.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.swisspush.redisques.util.HandlerUtil; 11 | import org.swisspush.redisques.util.QueueStatisticsCollector; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.regex.Pattern; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.ERROR; 18 | import static org.swisspush.redisques.util.RedisquesAPI.STATUS; 19 | 20 | /** 21 | * Retrieves in it's AsyncResult handler for all given queue names the queue statistics information 22 | * and returns the same in the event completion. 23 | */ 24 | public class GetQueuesStatisticsHandler implements Handler> { 25 | 26 | private static final Logger log = LoggerFactory.getLogger(GetQueuesStatisticsHandler.class); 27 | private final Message event; 28 | private final Optional filterPattern; 29 | private final QueueStatisticsCollector queueStatisticsCollector; 30 | 31 | public GetQueuesStatisticsHandler( 32 | Message event, 33 | Optional filterPattern, 34 | QueueStatisticsCollector queueStatisticsCollector 35 | ) { 36 | this.event = event; 37 | this.filterPattern = filterPattern; 38 | this.queueStatisticsCollector = queueStatisticsCollector; 39 | } 40 | 41 | @Override 42 | public void handle(AsyncResult handleQueues) { 43 | if (handleQueues.succeeded()) { 44 | List queues = HandlerUtil 45 | .filterByPattern(handleQueues.result(), filterPattern); 46 | queueStatisticsCollector.getQueueStatistics(queues) 47 | .onFailure(ex -> { 48 | log.error("", ex); 49 | event.reply(new JsonObject().put(STATUS, ERROR)); 50 | }) 51 | .onSuccess(event::reply); 52 | } else { 53 | log.warn("Concealed error", new Exception(handleQueues.cause())); 54 | event.reply(new JsonObject().put(STATUS, ERROR)); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/BulkPutLocksAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | import org.slf4j.Logger; 8 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 9 | import org.swisspush.redisques.handler.PutLockHandler; 10 | import org.swisspush.redisques.queue.KeyspaceHelper; 11 | import org.swisspush.redisques.queue.RedisService; 12 | import org.swisspush.redisques.util.QueueConfiguration; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.List; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.*; 18 | 19 | public class BulkPutLocksAction extends AbstractQueueAction { 20 | 21 | public BulkPutLocksAction( 22 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 23 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log 24 | ) { 25 | super(vertx, redisService, keyspaceHelper, 26 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 27 | } 28 | 29 | @Override 30 | public void execute(Message event) { 31 | JsonArray locks = event.body().getJsonObject(PAYLOAD).getJsonArray(LOCKS); 32 | if (locks == null || locks.isEmpty()) { 33 | event.reply(createErrorReply().put(MESSAGE, "No locks to put provided")); 34 | return; 35 | } 36 | 37 | JsonObject lockInfo = extractLockInfo(event.body().getJsonObject(PAYLOAD).getString(REQUESTED_BY)); 38 | if (lockInfo == null) { 39 | event.reply(createErrorReply().put(MESSAGE, "Property '" + REQUESTED_BY + "' missing")); 40 | return; 41 | } 42 | 43 | if (!jsonArrayContainsStringsOnly(locks)) { 44 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT).put(MESSAGE, "Locks must be string values")); 45 | return; 46 | } 47 | redisService.hmset(buildLocksItems(keyspaceHelper.getLocksKey(), locks, lockInfo)).onComplete(response -> 48 | new PutLockHandler(event, exceptionFactory).handle(response)) 49 | .onFailure(ex -> replyErrorMessageHandler(event).handle(ex)); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetQueuesStatisticsAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetQueuesStatisticsHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.*; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.regex.Pattern; 16 | 17 | import static org.swisspush.redisques.util.RedisquesAPI.*; 18 | 19 | /** 20 | * Retrieve the queue statistics info of the requested queues 21 | */ 22 | public class GetQueuesStatisticsAction extends AbstractQueueAction { 23 | 24 | public GetQueuesStatisticsAction( 25 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, 26 | List queueConfigurations, RedisQuesExceptionFactory exceptionFactory, 27 | QueueStatisticsCollector queueStatisticsCollector, Logger log 28 | ) { 29 | super(vertx, redisService, keyspaceHelper, 30 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 31 | } 32 | 33 | @Override 34 | public void execute(Message event) { 35 | Result, String> filterPattern = MessageUtil.extractFilterPattern(event); 36 | getQueuesStatistics(event, filterPattern); 37 | } 38 | 39 | /** 40 | * Retrieve the queue statistics info of the requested queues filtered by the 41 | * given filter pattern. 42 | */ 43 | private void getQueuesStatistics(Message event, 44 | Result, String> filterPattern) { 45 | if (filterPattern.isErr()) { 46 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT) 47 | .put(MESSAGE, filterPattern.getErr())); 48 | } else { 49 | redisService.zrangebyscore(keyspaceHelper.getQueuesKey(), String.valueOf(getMaxAgeTimestamp()), "+inf").onComplete(response -> 50 | new GetQueuesStatisticsHandler(event, filterPattern.getOk(), 51 | queueStatisticsCollector).handle(response)); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/BulkDeleteQueuesAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | import org.slf4j.Logger; 8 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.QueueConfiguration; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.List; 15 | 16 | import static org.swisspush.redisques.util.RedisquesAPI.*; 17 | 18 | public class BulkDeleteQueuesAction extends AbstractQueueAction { 19 | 20 | public BulkDeleteQueuesAction( 21 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 22 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log 23 | ) { 24 | super(vertx, redisService, keyspaceHelper, 25 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 26 | } 27 | 28 | @Override 29 | public void execute(Message event) { 30 | JsonArray queues = event.body().getJsonObject(PAYLOAD).getJsonArray(QUEUES); 31 | if (queues == null) { 32 | event.reply(createErrorReply().put(MESSAGE, "No queues to delete provided")); 33 | return; 34 | } 35 | 36 | if (queues.isEmpty()) { 37 | event.reply(createOkReply().put(VALUE, 0)); 38 | return; 39 | } 40 | 41 | if (!jsonArrayContainsStringsOnly(queues)) { 42 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT).put(MESSAGE, "Queues must be string values")); 43 | return; 44 | } 45 | redisService.del(buildQueueKeys(queues)).onComplete(delManyReply -> { 46 | if (delManyReply.succeeded()) { 47 | queueStatisticsCollector.resetQueueStatistics(queues, (Throwable ex, Void v) -> { 48 | if (ex != null) log.warn("TODO_q93258hu38 error handling", ex); 49 | }); 50 | event.reply(createOkReply().put(VALUE, delManyReply.result().toLong())); 51 | } else { 52 | handleFail(event, "Failed to bulkDeleteQueues", delManyReply.cause()); 53 | } 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetQueuesItemsCountAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetQueuesItemsCountHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.*; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.concurrent.Semaphore; 16 | import java.util.regex.Pattern; 17 | 18 | import static org.swisspush.redisques.util.RedisquesAPI.*; 19 | 20 | /** 21 | * Retrieve the size of the queues matching the given filter pattern 22 | */ 23 | public class GetQueuesItemsCountAction extends AbstractQueueAction { 24 | 25 | private final RedisQuesExceptionFactory exceptionFactory; 26 | private final Semaphore redisRequestQuota; 27 | 28 | public GetQueuesItemsCountAction( 29 | Vertx vertx, 30 | RedisService redisService, 31 | KeyspaceHelper keyspaceHelper, 32 | List queueConfigurations, 33 | RedisQuesExceptionFactory exceptionFactory, 34 | Semaphore redisRequestQuota, 35 | QueueStatisticsCollector queueStatisticsCollector, 36 | Logger log 37 | ) { 38 | super(vertx, redisService, keyspaceHelper, queueConfigurations, 39 | exceptionFactory, queueStatisticsCollector, log); 40 | this.exceptionFactory = exceptionFactory; 41 | this.redisRequestQuota = redisRequestQuota; 42 | } 43 | 44 | @Override 45 | public void execute(Message event) { 46 | Result, String> filterPattern = MessageUtil.extractFilterPattern(event); 47 | if (filterPattern.isErr()) { 48 | event.reply(createErrorReply().put(ERROR_TYPE, BAD_INPUT) 49 | .put(MESSAGE, filterPattern.getErr())); 50 | } else { 51 | redisService.zrangebyscore(keyspaceHelper.getQueuesKey(), 52 | String.valueOf(getMaxAgeTimestamp()), "+inf").onComplete(response -> 53 | new GetQueuesItemsCountHandler(vertx, event, filterPattern.getOk(), 54 | keyspaceHelper.getQueuesPrefix(), redisService, exceptionFactory, redisRequestQuota).handle(response)); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/performance/LongRingbuffer.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.performance; 2 | 3 | /** 4 | *

'long' in the class name refers to the type 'long'. It does NOT 5 | * say anything about how 'long' the buffer is!

6 | */ 7 | public class LongRingbuffer { 8 | 9 | private final Object pushpopLock = new Object(); 10 | private final long ring[]; 11 | private int wrCur; 12 | private boolean isFilled; 13 | 14 | LongRingbuffer(int capacity) { 15 | if (capacity < 1) throw new IllegalArgumentException("assert(capacity >= 1)"); 16 | this.ring = new long[capacity]; 17 | this.wrCur = 0; 18 | } 19 | 20 | public void add(long value) { 21 | synchronized (pushpopLock) { 22 | //log.trace("ring[{}] = {}", wrCur, value); 23 | ring[wrCur] = value; 24 | wrCur += 1; 25 | if (wrCur >= ring.length) { 26 | wrCur = 0; 27 | isFilled = true; 28 | } 29 | } 30 | } 31 | 32 | public int read(long dst[], int off, int len) { 33 | synchronized (pushpopLock){ 34 | int rangeOneOff, rangeOneLen, rangeTwoOff, rangeTwoLen; 35 | if (!isFilled) { 36 | rangeOneOff = 0; 37 | rangeOneLen = wrCur; 38 | rangeTwoOff = -999999; 39 | rangeTwoLen = 0; 40 | } else { 41 | rangeOneOff = wrCur; 42 | rangeOneLen = ring.length - rangeOneOff; 43 | rangeTwoOff = 0; 44 | rangeTwoLen = rangeOneOff == 0 ? 0 : wrCur; 45 | } 46 | int numCopied = 0; 47 | if( rangeOneLen > 0 ){ 48 | //log.trace("readOne {}-{}", rangeOneOff, rangeOneOff + rangeOneLen); 49 | int numToCopy = Math.min(len, rangeOneLen); 50 | System.arraycopy(ring, rangeOneOff, dst, off, numToCopy); 51 | numCopied += numToCopy; 52 | len -= numToCopy; 53 | off += numToCopy; 54 | } 55 | if (rangeTwoLen > 0 && len > 0) { 56 | //log.trace("readTwo {}-{}", rangeTwoOff, rangeTwoOff + rangeTwoLen); 57 | int numToCopy = Math.min(len, rangeTwoLen); 58 | System.arraycopy(ring, rangeTwoOff, dst, off, numToCopy); 59 | numCopied += numToCopy; 60 | len -= numToCopy; 61 | off += numToCopy; 62 | } 63 | return numCopied; 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/AddQueueItemActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.eventbus.ReplyException; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mockito; 11 | import org.slf4j.Logger; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.ArrayList; 15 | 16 | import static org.mockito.Mockito.*; 17 | import static org.swisspush.redisques.util.RedisquesAPI.buildAddQueueItemOperation; 18 | 19 | /** 20 | * Tests for {@link AddQueueItemAction} class. 21 | * 22 | * @author Marc-André Weber 23 | */ 24 | @RunWith(VertxUnitRunner.class) 25 | public class AddQueueItemActionTest extends AbstractQueueActionTest { 26 | 27 | @Before 28 | @Override 29 | public void setup() { 30 | super.setup(); 31 | action = new AddQueueItemAction(vertx, redisService, keyspaceHelper, 32 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 33 | } 34 | 35 | @Test 36 | public void testAddQueueItemWhenRedisIsNotReady(TestContext context){ 37 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 38 | when(message.body()).thenReturn(buildAddQueueItemOperation("queue2", "fooBar")); 39 | 40 | action.execute(message); 41 | 42 | verify(message, times(1)).reply(isA(ReplyException.class)); 43 | verifyNoInteractions(redisAPI); 44 | } 45 | 46 | @Test 47 | public void testAddQueueItem(TestContext context){ 48 | when(message.body()).thenReturn(buildAddQueueItemOperation("queue2", "fooBar")); 49 | when(redisAPI.rpush(anyList())).thenReturn(Future.succeededFuture()); 50 | 51 | action.execute(message); 52 | 53 | verify(redisAPI, times(1)).rpush(anyList()); 54 | verify(message, times(1)).reply(eq(STATUS_OK)); 55 | 56 | } 57 | 58 | @Test 59 | public void testAddQueueItemRPUSHFail(TestContext context){ 60 | when(message.body()).thenReturn(buildAddQueueItemOperation("queue2", "fooBar")); 61 | when(redisAPI.rpush(anyList())).thenReturn(Future.failedFuture("booom")); 62 | 63 | action.execute(message); 64 | 65 | verify(redisAPI, times(1)).rpush(anyList()); 66 | verify(message, times(1)).reply(isA(ReplyException.class)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/util/MessageUtilTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.eventbus.Message; 4 | import io.vertx.core.json.JsonObject; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mockito; 11 | 12 | import java.util.Optional; 13 | import java.util.regex.Pattern; 14 | 15 | import static org.mockito.Mockito.when; 16 | 17 | /** 18 | * Tests for {@link MessageUtil} class. 19 | * 20 | * @author Marc-André Weber 21 | */ 22 | @RunWith(VertxUnitRunner.class) 23 | public class MessageUtilTest { 24 | 25 | private Message message; 26 | 27 | @Before 28 | public void setUp() { 29 | message = Mockito.mock(Message.class); 30 | } 31 | 32 | @Test 33 | public void testExtractFilterPattern(TestContext context) { 34 | when(message.body()).thenReturn(RedisquesAPI.buildCheckOperation()); // no payload object 35 | Result, String> result = MessageUtil.extractFilterPattern(message); 36 | context.assertTrue(result.isOk()); 37 | context.assertFalse(result.getOk().isPresent()); 38 | 39 | when(message.body()).thenReturn(RedisquesAPI.buildGetLockOperation("queue_x")); // payload object without filter property 40 | result = MessageUtil.extractFilterPattern(message); 41 | context.assertTrue(result.isOk()); 42 | context.assertFalse(result.getOk().isPresent()); 43 | 44 | when(message.body()).thenReturn(RedisquesAPI.buildGetAllLocksOperation("")); // payload object with empty filter property 45 | result = MessageUtil.extractFilterPattern(message); 46 | context.assertTrue(result.isOk()); 47 | context.assertTrue(result.getOk().isPresent()); 48 | 49 | when(message.body()).thenReturn(RedisquesAPI.buildGetAllLocksOperation("xyz(.*)")); // payload object with valid filter property 50 | result = MessageUtil.extractFilterPattern(message); 51 | context.assertTrue(result.isOk()); 52 | context.assertTrue(result.getOk().isPresent()); 53 | context.assertEquals("xyz(.*)", result.getOk().get().pattern()); 54 | 55 | when(message.body()).thenReturn(RedisquesAPI.buildGetAllLocksOperation("xyz(.*")); // payload object with invalid filter property 56 | result = MessageUtil.extractFilterPattern(message); 57 | context.assertTrue(result.isErr()); 58 | context.assertTrue(result.getErr().contains("Error while compile regex pattern")); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetQueueItemsActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.eventbus.ReplyException; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.impl.types.SimpleStringType; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetQueueItemsOperation; 19 | 20 | /** 21 | * Tests for {@link GetQueueItemsAction} class. 22 | * 23 | * @author Marc-André Weber 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class GetQueueItemsActionTest extends AbstractQueueActionTest { 27 | 28 | @Before 29 | @Override 30 | public void setup() { 31 | super.setup(); 32 | action = new GetQueueItemsAction(vertx, redisService, keyspaceHelper, 33 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 34 | } 35 | 36 | @Test 37 | public void testGetQueueItemsWhenRedisIsNotReady(TestContext context){ 38 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 39 | when(message.body()).thenReturn(buildGetQueueItemsOperation("q1", null)); 40 | 41 | action.execute(message); 42 | 43 | verify(message, times(1)).reply(isA(ReplyException.class)); 44 | verifyNoInteractions(redisAPI); 45 | } 46 | 47 | @Test 48 | public void testLLENFail(TestContext context){ 49 | when(message.body()).thenReturn(buildGetQueueItemsOperation("q1", null)); 50 | when(redisAPI.llen(anyString())).thenReturn(Future.failedFuture("boooom")); 51 | 52 | action.execute(message); 53 | 54 | verify(redisAPI, times(1)).llen(anyString()); 55 | verify(message, times(1)).reply(isA(ReplyException.class)); 56 | } 57 | 58 | @Test 59 | public void testLLENInvalidResponseValue(TestContext context){ 60 | when(message.body()).thenReturn(buildGetQueueItemsOperation("q1", null)); 61 | when(redisAPI.llen(anyString())).thenReturn(Future.succeededFuture(SimpleStringType.create(null))); 62 | 63 | action.execute(message); 64 | 65 | verify(redisAPI, times(1)).llen(anyString()); 66 | verify(message, times(1)).reply(isA(ReplyException.class)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/exception/ThriftyRedisQuesExceptionFactory.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.exception; 2 | 3 | import io.vertx.core.eventbus.ReplyException; 4 | import io.vertx.core.eventbus.ReplyFailure; 5 | 6 | /** 7 | * Trades maintainability for speed. For example prefers lightweight 8 | * exceptions without stacktrace recording. It may even decide to drop 'cause' 9 | * and 'suppressed' exceptions. If an app needs more error details it should use 10 | * {@link WastefulRedisQuesExceptionFactory}. If none of those fits the apps needs, it 11 | * can provide its own implementation. 12 | */ 13 | class ThriftyRedisQuesExceptionFactory implements RedisQuesExceptionFactory { 14 | 15 | ThriftyRedisQuesExceptionFactory() { 16 | } 17 | 18 | public Exception newException(String message, Throwable cause) { 19 | // This impl exists for speed. So why even bother creating new instances 20 | // if we can use already existing ones. If caller really needs another 21 | // instance, he should use another implementation of this factory. 22 | if (cause instanceof Exception) return (Exception) cause; 23 | return new NoStacktraceException(message, cause); 24 | } 25 | 26 | @Override 27 | public RuntimeException newRuntimeException(String message, Throwable cause) { 28 | // This impl exists for speed. So why even bother creating new instances 29 | // if we can use already existing ones. If caller really needs another 30 | // instance, he should use another implementation of this factory. 31 | if (cause instanceof RuntimeException) return (RuntimeException) cause; 32 | return new NoStacktraceException(message, cause); 33 | } 34 | 35 | @Override 36 | public ReplyException newReplyException(int failureCode, String msg, Throwable cause) { 37 | // There was once a fix in vertx for this (https://github.com/eclipse-vertx/vert.x/issues/4840) 38 | // but for whatever reason in our case we still see stack-trace recordings. Passing 39 | // this subclass to {@link io.vertx.core.eventbus.Message#reply(Object)} seems to 40 | // do the trick. 41 | if (msg == null && cause != null) msg = cause.getMessage(); 42 | return new ReplyException(ReplyFailure.RECIPIENT_FAILURE, failureCode, msg) { 43 | @Override public Throwable fillInStackTrace() { return this; } 44 | }; 45 | } 46 | 47 | @Override 48 | public ResourceExhaustionException newResourceExhaustionException(String msg, Throwable cause) { 49 | if (cause instanceof ResourceExhaustionException) return (ResourceExhaustionException) cause; 50 | return new ResourceExhaustionException(msg, cause) { 51 | @Override public Throwable fillInStackTrace() { return this; } 52 | }; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/ReplaceQueueItemActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.eventbus.ReplyException; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.Mockito; 13 | import org.slf4j.Logger; 14 | import org.swisspush.redisques.util.QueueStatisticsCollector; 15 | 16 | import java.util.ArrayList; 17 | 18 | import static org.mockito.Mockito.*; 19 | import static org.swisspush.redisques.util.RedisquesAPI.buildReplaceQueueItemOperation; 20 | 21 | /** 22 | * Tests for {@link ReplaceQueueItemAction} class. 23 | * 24 | * @author Marc-André Weber 25 | */ 26 | @RunWith(VertxUnitRunner.class) 27 | public class ReplaceQueueItemActionTest extends AbstractQueueActionTest { 28 | 29 | @Before 30 | @Override 31 | public void setup() { 32 | super.setup(); 33 | action = new ReplaceQueueItemAction(vertx, redisService, keyspaceHelper, 34 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 35 | } 36 | 37 | @Test 38 | public void testReplaceQueueItemWhenRedisIsNotReady(TestContext context){ 39 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 40 | when(message.body()).thenReturn(buildReplaceQueueItemOperation("q1", 0,"geronimo")); 41 | 42 | action.execute(message); 43 | 44 | verify(message, times(1)).reply(isA(ReplyException.class)); 45 | verifyNoInteractions(redisAPI); 46 | } 47 | 48 | @Test 49 | public void testReplaceQueueItem(TestContext context){ 50 | when(message.body()).thenReturn(buildReplaceQueueItemOperation("q1", 0,"geronimo")); 51 | 52 | when(redisAPI.lset(anyString(), anyString(), anyString())).thenReturn(Future.succeededFuture()); 53 | 54 | action.execute(message); 55 | 56 | verify(redisAPI, times(1)).lset(anyString(), anyString(), anyString()); 57 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"ok\"}")))); 58 | } 59 | 60 | @Test 61 | public void testReplaceQueueItemWithLSETFail(TestContext context){ 62 | when(message.body()).thenReturn(buildReplaceQueueItemOperation("q1", 0,"geronimo")); 63 | 64 | when(redisAPI.lset(anyString(), anyString(), anyString())).thenReturn(Future.failedFuture("booom")); 65 | 66 | action.execute(message); 67 | 68 | verify(redisAPI, times(1)).lset(anyString(), anyString(), anyString()); 69 | verify(message, times(1)).reply(isA(ReplyException.class)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/LockedEnqueueActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.micrometer.core.instrument.Counter; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.buffer.Buffer; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.ext.unit.TestContext; 10 | import io.vertx.ext.unit.junit.VertxUnitRunner; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mockito; 15 | import org.slf4j.Logger; 16 | import org.swisspush.redisques.util.MetricMeter; 17 | import org.swisspush.redisques.util.MetricTags; 18 | import org.swisspush.redisques.util.QueueStatisticsCollector; 19 | 20 | import java.util.ArrayList; 21 | 22 | import static org.mockito.Mockito.*; 23 | import static org.swisspush.redisques.util.RedisquesAPI.buildLockedEnqueueOperation; 24 | 25 | /** 26 | * Tests for {@link LockedEnqueueAction} class. 27 | * 28 | * @author Marc-André Weber 29 | */ 30 | @RunWith(VertxUnitRunner.class) 31 | public class LockedEnqueueActionTest extends AbstractQueueActionTest { 32 | 33 | private Counter enqueueCounterSuccess; 34 | private Counter enqueueCounterFail; 35 | 36 | @Before 37 | @Override 38 | public void setup() { 39 | super.setup(); 40 | MeterRegistry meterRegistry = new SimpleMeterRegistry(); 41 | enqueueCounterSuccess = meterRegistry.counter(MetricMeter.ENQUEUE_SUCCESS.getId(), MetricTags.IDENTIFIER.getId(), "foo"); 42 | enqueueCounterFail = meterRegistry.counter(MetricMeter.ENQUEUE_FAIL.getId(), MetricTags.IDENTIFIER.getId(), "foo"); 43 | action = new LockedEnqueueAction(vertx, redisService, keyspaceHelper, 44 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), 45 | Mockito.mock(Logger.class), memoryUsageProvider, 80, meterRegistry, "foo"); 46 | } 47 | 48 | @Test 49 | public void testLockedEnqueueWhenRedisIsNotReady(TestContext context){ 50 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 51 | when(message.body()).thenReturn(buildLockedEnqueueOperation("queueEnqueue", "helloEnqueue", "someuser")); 52 | 53 | action.execute(message); 54 | 55 | assertEnqueueCounts(context, 0.0, 1.0); 56 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 57 | verifyNoInteractions(redisAPI); 58 | } 59 | 60 | private void assertEnqueueCounts(TestContext context, double successCount, double failCount){ 61 | context.assertEquals(successCount, enqueueCounterSuccess.count(), "Success enqueue count is wrong"); 62 | context.assertEquals(failCount, enqueueCounterFail.count(), "Failed enqueue count is wrong"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/DeleteQueueItemAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.queue.KeyspaceHelper; 9 | import org.swisspush.redisques.queue.RedisService; 10 | import org.swisspush.redisques.util.QueueConfiguration; 11 | import org.swisspush.redisques.util.QueueStatisticsCollector; 12 | 13 | import java.util.List; 14 | 15 | import static org.swisspush.redisques.util.RedisquesAPI.*; 16 | 17 | public class DeleteQueueItemAction extends AbstractQueueAction { 18 | 19 | public DeleteQueueItemAction( 20 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 21 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log 22 | ) { 23 | super(vertx, redisService, keyspaceHelper, 24 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 25 | } 26 | 27 | @Override 28 | public void execute(Message event) { 29 | String keyLset = keyspaceHelper.getQueuesPrefix() + event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 30 | int indexLset = event.body().getJsonObject(PAYLOAD).getInteger(INDEX); 31 | redisService.lset(keyLset, String.valueOf(indexLset), "TO_DELETE").onComplete( 32 | event1 -> { 33 | if (event1.succeeded()) { 34 | String keyLrem = keyspaceHelper.getQueuesPrefix() + event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 35 | redisService.lrem(keyLrem, "0", "TO_DELETE").onComplete(replyLrem -> { 36 | if (replyLrem.failed()) { 37 | handleFail(event, "Failed to 'lrem' while deleteQueueItem", replyLrem.cause()); 38 | } else { 39 | event.reply(createOkReply()); 40 | } 41 | }); 42 | } else { 43 | if (checkRedisErrorCodes(event1.cause().getMessage())) { 44 | log.error("Failed to 'lset' while deleteQueueItem.", exceptionFactory.newException(event1.cause())); 45 | event.reply(createErrorReply()); 46 | } else { 47 | handleFail(event, "Failed to 'lset' while deleteQueueItem", event1.cause()); 48 | } 49 | } 50 | }); 51 | } 52 | 53 | private boolean checkRedisErrorCodes(String message) { 54 | if (message == null) { 55 | return false; 56 | } 57 | return message.contains("no such key") || message.contains("index out of range"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/lock/impl/RedisBasedLock.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.lock.impl; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.Promise; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.lock.Lock; 9 | import org.swisspush.redisques.lock.lua.LockLuaScripts; 10 | import org.swisspush.redisques.lua.LuaScriptState; 11 | import org.swisspush.redisques.lock.lua.ReleaseLockRedisCommand; 12 | import org.swisspush.redisques.queue.RedisService; 13 | 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | /** 18 | * Implementation of the {@link Lock} interface based on a redis database. 19 | * 20 | * @author https://github.com/mcweba [Marc-Andre Weber] 21 | */ 22 | public class RedisBasedLock implements Lock { 23 | 24 | private static final Logger log = LoggerFactory.getLogger(RedisBasedLock.class); 25 | private static final String[] EMPTY_STRING_ARRAY = new String[0]; 26 | 27 | public static final String STORAGE_PREFIX = "gateleen.core-lock:"; 28 | 29 | private final LuaScriptState releaseLockLuaScriptState; 30 | private final RedisService redisService; 31 | private final RedisQuesExceptionFactory exceptionFactory; 32 | 33 | public RedisBasedLock(RedisService redisService, RedisQuesExceptionFactory exceptionFactory) { 34 | this.redisService = redisService; 35 | this.exceptionFactory = exceptionFactory; 36 | this.releaseLockLuaScriptState = new LuaScriptState(LockLuaScripts.LOCK_RELEASE, redisService, exceptionFactory, false); 37 | } 38 | 39 | 40 | @Override 41 | public Future acquireLock(String lock, String token, long lockExpiryMs) { 42 | Promise promise = Promise.promise(); 43 | String lockKey = buildLockKey(lock); 44 | redisService.setNxPx(lockKey, token, true, lockExpiryMs).onComplete(event -> { 45 | if (event.succeeded()) { 46 | promise.complete(event.result()); 47 | } else { 48 | Throwable ex = exceptionFactory.newException( 49 | "redisSetWithOptions(lockKey=\"" + lockKey + "\") failed", event.cause()); 50 | promise.fail(ex); 51 | } 52 | }); 53 | return promise.future(); 54 | } 55 | 56 | @Override 57 | public Future releaseLock(String lock, String token) { 58 | Promise promise = Promise.promise(); 59 | List keys = Collections.singletonList(buildLockKey(lock)); 60 | List arguments = Collections.singletonList(token); 61 | ReleaseLockRedisCommand cmd = new ReleaseLockRedisCommand(releaseLockLuaScriptState, 62 | keys, arguments, redisService, exceptionFactory, log, promise); 63 | cmd.exec(0); 64 | return promise.future(); 65 | } 66 | 67 | private String buildLockKey(String lock) { 68 | return STORAGE_PREFIX + lock; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/AbstractQueueActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Handler; 6 | import io.vertx.core.Vertx; 7 | import io.vertx.core.buffer.Buffer; 8 | import io.vertx.core.eventbus.Message; 9 | import io.vertx.core.json.JsonObject; 10 | import io.vertx.redis.client.RedisAPI; 11 | import io.vertx.redis.client.Response; 12 | import org.junit.Before; 13 | import org.mockito.Mockito; 14 | import org.mockito.invocation.InvocationOnMock; 15 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 16 | import org.swisspush.redisques.queue.KeyspaceHelper; 17 | import org.swisspush.redisques.queue.RedisService; 18 | import org.swisspush.redisques.util.MemoryUsageProvider; 19 | import org.swisspush.redisques.util.RedisProvider; 20 | 21 | import java.util.Optional; 22 | 23 | import static org.mockito.Mockito.*; 24 | import static org.swisspush.redisques.exception.RedisQuesExceptionFactory.newWastefulExceptionFactory; 25 | 26 | public abstract class AbstractQueueActionTest { 27 | 28 | protected RedisAPI redisAPI; 29 | protected RedisProvider redisProvider; 30 | protected RedisService redisService; 31 | protected RedisQuesExceptionFactory exceptionFactory; 32 | protected KeyspaceHelper keyspaceHelper; 33 | 34 | protected Vertx vertx; 35 | 36 | protected MemoryUsageProvider memoryUsageProvider; 37 | 38 | protected Message message; 39 | protected AbstractQueueAction action; 40 | 41 | protected static final JsonObject STATUS_OK = new JsonObject(Buffer.buffer("{\"status\":\"ok\"}")); 42 | protected static final JsonObject STATUS_ERROR = new JsonObject(Buffer.buffer("{\"status\":\"error\"}")); 43 | @Before 44 | public void setup() { 45 | redisAPI = Mockito.mock(RedisAPI.class); 46 | 47 | redisProvider = Mockito.mock(RedisProvider.class); 48 | when(redisProvider.redis()).thenReturn(Future.succeededFuture(redisAPI)); 49 | 50 | keyspaceHelper = Mockito.mock(KeyspaceHelper.class); 51 | when(keyspaceHelper.getAddress()).thenReturn("addr"); 52 | when(keyspaceHelper.getQueuesKey()).thenReturn("q-"); 53 | when(keyspaceHelper.getQueuesPrefix()).thenReturn("prefix-"); 54 | when(keyspaceHelper.getConsumersPrefix()).thenReturn("c-"); 55 | when(keyspaceHelper.getLocksKey()).thenReturn("l-"); 56 | exceptionFactory = newWastefulExceptionFactory(); 57 | redisService = new RedisService(redisProvider); 58 | 59 | memoryUsageProvider = Mockito.mock(MemoryUsageProvider.class); 60 | when(memoryUsageProvider.currentMemoryUsagePercentage()).thenReturn(Optional.empty()); 61 | vertx = Vertx.vertx(); 62 | 63 | message = Mockito.mock(Message.class); 64 | } 65 | 66 | protected Handler> createResponseHandler(InvocationOnMock invocation, int handlerIndex) { 67 | return (Handler>) invocation.getArguments()[handlerIndex]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/Result.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | public class Result { 4 | 5 | private final Type type; 6 | private final TOk okValue; 7 | private final TErr errValue; 8 | 9 | public static Result ok(TOk value) { 10 | return new Result<>(Type.OK, value, null); 11 | } 12 | 13 | public static Result err(TErr value) { 14 | return new Result<>(Type.ERROR, null, value); 15 | } 16 | 17 | private Result(Type type, TOk okValue, TErr errValue) { 18 | if (okValue != null && errValue != null) { 19 | throw new IllegalStateException("A result cannot be ok and error at the same time"); 20 | } 21 | this.type = type; 22 | this.okValue = okValue; 23 | this.errValue = errValue; 24 | } 25 | 26 | /** 27 | * See https://doc.rust-lang.org/stable/std/result/enum.Result.html#method.unwrap 28 | * Returns the ok result or throws if this result is in error state. 29 | */ 30 | public TOk unwrap() throws RuntimeException { 31 | if (isOk()) { 32 | return getOk(); 33 | } else { 34 | throw new RuntimeException("Got an error result. Error value is '" + getErr() + "'"); 35 | } 36 | } 37 | 38 | public boolean isOk() { 39 | return this.type == Type.OK; 40 | } 41 | 42 | public TOk getOk() throws IllegalStateException { 43 | if (!isOk()) { 44 | throw new IllegalStateException("Cannot call this method for results in error state"); 45 | } 46 | return okValue; 47 | } 48 | 49 | public boolean isErr() { 50 | return !isOk(); 51 | } 52 | 53 | public TErr getErr() throws IllegalStateException { 54 | if (isOk()) { 55 | throw new IllegalStateException("Cannot call this method for results in ok state"); 56 | } 57 | return errValue; 58 | } 59 | 60 | @Override 61 | public boolean equals(Object o) { 62 | if (this == o) return true; 63 | if (o == null || getClass() != o.getClass()) return false; 64 | 65 | Result result = (Result) o; 66 | 67 | if (type != result.type) return false; 68 | if (okValue != null ? !okValue.equals(result.okValue) : result.okValue != null) return false; 69 | return errValue != null ? errValue.equals(result.errValue) : result.errValue == null; 70 | } 71 | 72 | @Override 73 | public int hashCode() { 74 | int result = type.hashCode(); 75 | result = 31 * result + (okValue != null ? okValue.hashCode() : 0); 76 | result = 31 * result + (errValue != null ? errValue.hashCode() : 0); 77 | return result; 78 | } 79 | 80 | @Override 81 | public String toString() { 82 | if (isOk()) { 83 | return "ResultOk{" + okValue + '}'; 84 | } else { 85 | return "ResultErr{" + errValue + '}'; 86 | } 87 | } 88 | 89 | private enum Type { 90 | OK, 91 | ERROR 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/DeleteAllQueueItemsAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.redis.client.Response; 8 | import org.slf4j.Logger; 9 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 10 | import org.swisspush.redisques.queue.KeyspaceHelper; 11 | import org.swisspush.redisques.queue.RedisService; 12 | import org.swisspush.redisques.util.QueueConfiguration; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | import static org.swisspush.redisques.util.RedisquesAPI.*; 19 | 20 | public class DeleteAllQueueItemsAction extends AbstractQueueAction { 21 | 22 | public DeleteAllQueueItemsAction( 23 | Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 24 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log 25 | ) { 26 | super(vertx, redisService, keyspaceHelper, 27 | queueConfigurations, exceptionFactory, queueStatisticsCollector, log); 28 | } 29 | 30 | @Override 31 | public void execute(Message event) { 32 | JsonObject payload = event.body().getJsonObject(PAYLOAD); 33 | boolean unlock = payload.getBoolean(UNLOCK, false); 34 | String queue = payload.getString(QUEUENAME); 35 | 36 | redisService.del(Collections.singletonList(buildQueueKey(queue))).onComplete(deleteReply -> { 37 | if (deleteReply.failed()) { 38 | handleFail(event, "Operation DeleteAllQueueItems failed", deleteReply.cause()); 39 | return; 40 | } 41 | queueStatisticsCollector.resetQueueFailureStatistics(queue, (Throwable ex, Void v) -> { 42 | if (ex != null) log.warn("TODO_2958iouhj error handling", ex); 43 | }); 44 | if (unlock) { 45 | redisService.hdel(keyspaceHelper.getLocksKey(), queue).onComplete(unlockReply -> { 46 | if (unlockReply.failed()) { 47 | handleFail(event, "Failed to unlock queue " + queue, unlockReply.cause()); 48 | } else { 49 | handleDeleteQueueReply(event, deleteReply); 50 | } 51 | }); 52 | } else { 53 | handleDeleteQueueReply(event, deleteReply); 54 | } 55 | }); 56 | } 57 | 58 | private void handleDeleteQueueReply(Message event, AsyncResult reply) { 59 | if (reply.succeeded()) { 60 | event.reply(createOkReply().put(VALUE, reply.result().toLong())); 61 | } else { 62 | log.error("Failed to replyResultGreaterThanZero", exceptionFactory.newException(reply.cause())); 63 | event.reply(createErrorReply()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/GetQueueItemsAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.eventbus.Message; 5 | import io.vertx.core.json.JsonObject; 6 | import org.slf4j.Logger; 7 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 8 | import org.swisspush.redisques.handler.GetQueueItemsHandler; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.QueueConfiguration; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.List; 15 | 16 | import static org.swisspush.redisques.util.RedisquesAPI.*; 17 | 18 | public class GetQueueItemsAction extends AbstractQueueAction { 19 | 20 | private static final int DEFAULT_MAX_QUEUEITEM_COUNT = 49; 21 | 22 | public GetQueueItemsAction(Vertx vertx, RedisService redisService, KeyspaceHelper keyspaceHelper, List queueConfigurations, 23 | RedisQuesExceptionFactory exceptionFactory, QueueStatisticsCollector queueStatisticsCollector, Logger log) { 24 | super(vertx, redisService, keyspaceHelper, queueConfigurations, 25 | exceptionFactory, queueStatisticsCollector, log); 26 | } 27 | 28 | @Override 29 | public void execute(Message event) { 30 | String queueName = event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 31 | String keyListRange = keyspaceHelper.getQueuesPrefix() + queueName; 32 | int maxQueueItemCountIndex = getMaxQueueItemCountIndex(event.body().getJsonObject(PAYLOAD).getString(LIMIT)); 33 | 34 | redisService.llen(keyListRange).onSuccess(countReply -> { 35 | Long queueItemCount = countReply.toLong(); 36 | if(queueItemCount != null) { 37 | redisService.lrange(keyListRange, "0", String.valueOf(maxQueueItemCountIndex)).onComplete(response -> 38 | new GetQueueItemsHandler(event, queueItemCount).handle(response)); 39 | } else { 40 | event.reply(exceptionFactory.newReplyException( 41 | "Operation getQueueItems failed to extract queueItemCount", null)); 42 | } 43 | }).onFailure(throwable -> handleFail(event, "Operation getQueueItems failed", throwable)); 44 | } 45 | 46 | private int getMaxQueueItemCountIndex(String limit) { 47 | int defaultMaxIndex = DEFAULT_MAX_QUEUEITEM_COUNT; 48 | if (limit != null) { 49 | try { 50 | int maxIndex = Integer.parseInt(limit) - 1; 51 | if (maxIndex >= 0) { 52 | defaultMaxIndex = maxIndex; 53 | } 54 | log.info("use limit parameter {}", maxIndex); 55 | } catch (NumberFormatException ex) { 56 | log.warn("Invalid limit parameter '{}' configured for max queue item count. Using default {}", 57 | limit, DEFAULT_MAX_QUEUEITEM_COUNT); 58 | } 59 | } 60 | return defaultMaxIndex; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/MonitorAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.eventbus.Message; 4 | import io.vertx.core.http.HttpClient; 5 | import io.vertx.core.http.HttpMethod; 6 | import io.vertx.core.http.RequestOptions; 7 | import io.vertx.core.json.JsonObject; 8 | import org.slf4j.Logger; 9 | import org.swisspush.redisques.util.RedisquesConfiguration; 10 | 11 | import java.util.Base64; 12 | 13 | import static org.swisspush.redisques.util.RedisquesAPI.*; 14 | import static org.swisspush.redisques.util.RedisquesAPI.LIMIT; 15 | 16 | public class MonitorAction implements QueueAction { 17 | 18 | private final HttpClient client; 19 | private final RedisquesConfiguration modConfig; 20 | private final Logger log; 21 | 22 | public MonitorAction(RedisquesConfiguration modConfig, HttpClient client, Logger log) { 23 | this.modConfig = modConfig; 24 | this.client = client; 25 | this.log = log; 26 | } 27 | 28 | @Override 29 | public void execute(Message event) { 30 | if (!modConfig.getHttpRequestHandlerEnabled()) { 31 | event.reply(createErrorReply().put(MESSAGE, "HttpRequestHandler is disabled")); 32 | return; 33 | } 34 | 35 | String limit = event.body().getJsonObject(PAYLOAD).getString(LIMIT); 36 | boolean emptyQueues = event.body().getJsonObject(PAYLOAD).getBoolean(EMPTY_QUEUES, false); 37 | 38 | String requestParams = "?limit=" + limit + "&emptyQueues=" + emptyQueues; 39 | 40 | RequestOptions requestOptions = new RequestOptions() 41 | .setMethod(HttpMethod.GET) 42 | .setPort(modConfig.getHttpRequestHandlerPort()) 43 | .setHost("localhost") 44 | .setURI(modConfig.getHttpRequestHandlerPrefix() + "/monitor" + requestParams); 45 | 46 | if(modConfig.getHttpRequestHandlerAuthenticationEnabled()) { 47 | String credentials = modConfig.getHttpRequestHandlerUsername() + ":" + modConfig.getHttpRequestHandlerPassword(); 48 | String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); 49 | requestOptions.putHeader("Authorization", "Basic " + encodedCredentials); 50 | } 51 | 52 | client.request(requestOptions) 53 | .compose(req -> req.send().compose(response -> { 54 | if (response.statusCode() == 200) { 55 | return response.body(); 56 | } else { 57 | throw new RuntimeException("Failed to fetch monitor data: " + response.statusMessage()); 58 | } 59 | })) 60 | .onComplete(ar -> { 61 | if (ar.succeeded()) { 62 | event.reply(createOkReply().put(VALUE, ar.result().toJsonObject())); 63 | } else { 64 | event.reply(createErrorReply().put(MESSAGE, ar.cause().getMessage())); 65 | log.warn("Failed to fetch monitor data", ar.cause()); 66 | } 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/lock/lua/ReleaseLockRedisCommand.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.lock.lua; 2 | 3 | import io.vertx.core.Promise; 4 | import org.slf4j.Logger; 5 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 6 | import org.swisspush.redisques.lua.LuaScriptState; 7 | import org.swisspush.redisques.lua.RedisCommand; 8 | import org.swisspush.redisques.queue.RedisService; 9 | 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * @author https://github.com/mcweba [Marc-Andre Weber] 16 | */ 17 | public class ReleaseLockRedisCommand implements RedisCommand { 18 | 19 | private LuaScriptState luaScriptState; 20 | private List keys; 21 | private List arguments; 22 | private Promise promise; 23 | private RedisService redisService; 24 | private final RedisQuesExceptionFactory exceptionFactory; 25 | private Logger log; 26 | 27 | public ReleaseLockRedisCommand( 28 | LuaScriptState luaScriptState, 29 | List keys, 30 | List arguments, 31 | RedisService redisService, 32 | RedisQuesExceptionFactory exceptionFactory, 33 | Logger log, 34 | final Promise promise 35 | ) { 36 | this.luaScriptState = luaScriptState; 37 | this.keys = keys; 38 | this.arguments = arguments; 39 | this.redisService = redisService; 40 | this.exceptionFactory = exceptionFactory; 41 | this.log = log; 42 | this.promise = promise; 43 | } 44 | 45 | @Override 46 | public void exec(int executionCounter) { 47 | List args = new ArrayList<>(); 48 | args.add(luaScriptState.getSha()); 49 | args.add(String.valueOf(keys.size())); 50 | args.addAll(keys); 51 | args.addAll(arguments); 52 | redisService.evalsha(args).onComplete(event -> { 53 | if (event.succeeded()) { 54 | Long unlocked = event.result().toLong(); 55 | promise.complete(unlocked > 0); 56 | } else { 57 | Throwable ex = event.cause(); 58 | String message = ex.getMessage(); 59 | if (message != null && message.startsWith("NOSCRIPT")) { 60 | log.warn("ReleaseLockRedisCommand script couldn't be found, reload it", 61 | exceptionFactory.newException(ex)); 62 | log.warn("amount the script got loaded: {}", executionCounter); 63 | if (executionCounter > 10) { 64 | promise.fail(exceptionFactory.newException("amount the script got loaded is higher than 10, we abort")); 65 | } else { 66 | luaScriptState.loadLuaScript(new ReleaseLockRedisCommand(luaScriptState, keys, 67 | arguments, redisService, exceptionFactory, log, promise), executionCounter); 68 | } 69 | } else { 70 | promise.fail(exceptionFactory.newException("ReleaseLockRedisCommand request failed", ex)); 71 | } 72 | } 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/util/RedisQuesTimerTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.ext.unit.Async; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | /** 11 | * Tests for {@link RedisQuesTimer} class. 12 | * 13 | * @author Marc-André Weber 14 | */ 15 | @RunWith(VertxUnitRunner.class) 16 | public class RedisQuesTimerTest { 17 | 18 | private static final double TEST_BUFFER_MS = 2.5; 19 | 20 | @Test 21 | public void testExecuteDelayedLong(TestContext context){ 22 | Async async = context.async(); 23 | RedisQuesTimer timer = new RedisQuesTimer(Vertx.vertx()); 24 | final int delayMs = 1500; 25 | final long start = System.currentTimeMillis(); 26 | 27 | timer.executeDelayedMax(delayMs).onComplete(delayed -> { 28 | context.assertTrue(delayed.succeeded()); 29 | 30 | long end = System.currentTimeMillis(); 31 | long duration = end - start; 32 | assertMaxDuration(context, duration, delayMs); 33 | async.complete(); 34 | }); 35 | } 36 | 37 | @Test 38 | public void testExecuteDelayedShort(TestContext context){ 39 | Async async = context.async(); 40 | RedisQuesTimer timer = new RedisQuesTimer(Vertx.vertx()); 41 | final int delayMs = 50; 42 | final long start = System.currentTimeMillis(); 43 | 44 | timer.executeDelayedMax(delayMs).onComplete(delayed -> { 45 | context.assertTrue(delayed.succeeded()); 46 | 47 | long end = System.currentTimeMillis(); 48 | long duration = end - start; 49 | assertMaxDuration(context, duration, delayMs); 50 | async.complete(); 51 | }); 52 | } 53 | 54 | @Test 55 | public void testExecuteDelayedZero(TestContext context){ 56 | Async async = context.async(); 57 | RedisQuesTimer timer = new RedisQuesTimer(Vertx.vertx()); 58 | final int delayMs = 0; 59 | final long start = System.currentTimeMillis(); 60 | 61 | timer.executeDelayedMax(delayMs).onComplete(delayed -> { 62 | context.assertTrue(delayed.succeeded()); 63 | 64 | long end = System.currentTimeMillis(); 65 | long duration = end - start; 66 | assertMaxDuration(context, duration, delayMs); 67 | async.complete(); 68 | }); 69 | } 70 | 71 | private void assertMaxDuration(TestContext context, long duration, int delayMs){ 72 | if(delayMs <= 0){ 73 | delayMs = 1; 74 | } 75 | double delayPlus50Percent = delayMs * 1.5; 76 | if(delayPlus50Percent <= TEST_BUFFER_MS) { 77 | delayPlus50Percent = TEST_BUFFER_MS; // to increase test stability for very low delays we add some ms 78 | } 79 | context.assertTrue(duration <= delayPlus50Percent, "Future completed after " + duration + "ms. " + 80 | "However it should not have taken more than the delay + 50% which would be " + delayPlus50Percent + "ms"); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/queue/KeyspaceHelper.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.queue; 2 | 3 | import org.swisspush.redisques.util.RedisquesConfiguration; 4 | 5 | public class KeyspaceHelper { 6 | private final RedisquesConfiguration configuration; 7 | private final String queuesKey; 8 | private final String queuesPrefix; 9 | private final String consumersPrefix; 10 | private final String locksKey; 11 | private final String queueCheckLastexecKey; 12 | 13 | private final String verticleUid; 14 | private final String verticleRefreshRegistrationKey; 15 | private final String verticleNotifyConsumerKey; 16 | private final String trimRequestKey; 17 | private final String consumersAddress; 18 | private final String consumersAliveAddress; 19 | private final String metricsCollectorAddress; 20 | public static final String QUEUE_STATE_COUNT_KEY = "queueStateCount"; 21 | 22 | public KeyspaceHelper(RedisquesConfiguration configuration, String verticleUid) { 23 | this.configuration = configuration; 24 | this.verticleUid = verticleUid; 25 | queuesKey = configuration.getRedisPrefix() + "queues"; 26 | queuesPrefix = configuration.getRedisPrefix() + "queues:"; 27 | consumersPrefix = configuration.getRedisPrefix() + "consumers:"; 28 | locksKey = configuration.getRedisPrefix() + "locks"; 29 | queueCheckLastexecKey = configuration.getRedisPrefix() + "check:lastexec"; 30 | verticleRefreshRegistrationKey = "refreshRegistration:" + verticleUid; 31 | verticleNotifyConsumerKey = "notifyConsumer:" + verticleUid; 32 | trimRequestKey = "trim_request:" + verticleUid; 33 | consumersAddress = configuration.getAddress() + "-consumers"; 34 | consumersAliveAddress = configuration.getAddress() + "-consumer-alive"; 35 | metricsCollectorAddress = configuration.getAddress() + "-" + verticleUid + "-" + QUEUE_STATE_COUNT_KEY; 36 | } 37 | 38 | public String getAddress() { 39 | return configuration.getAddress(); 40 | } 41 | 42 | public String getVerticleUid() { 43 | return verticleUid; 44 | } 45 | 46 | public String getQueueCheckLastExecKey() { 47 | return queueCheckLastexecKey; 48 | } 49 | 50 | public String getLocksKey() { 51 | return locksKey; 52 | } 53 | 54 | public String getQueuesKey() { 55 | return queuesKey; 56 | } 57 | 58 | public String getQueuesPrefix() { 59 | return queuesPrefix; 60 | } 61 | 62 | public String getConsumersPrefix() { 63 | return consumersPrefix; 64 | } 65 | 66 | public String getVerticleRefreshRegistrationKey() { 67 | return verticleRefreshRegistrationKey; 68 | } 69 | 70 | public String getVerticleNotifyConsumerKey() { 71 | return verticleNotifyConsumerKey; 72 | } 73 | 74 | public String getTrimRequestKey() { 75 | return trimRequestKey; 76 | } 77 | 78 | public String getConsumersAddress() { 79 | return consumersAddress; 80 | } 81 | 82 | public String getConsumersAliveAddress() { 83 | return consumersAliveAddress; 84 | } 85 | 86 | public String getMetricsCollectorAddress() { 87 | return metricsCollectorAddress; 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/handler/RedisquesConfigurationAuthentication.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.handler; 2 | 3 | import io.netty.util.internal.StringUtil; 4 | import io.vertx.core.AsyncResult; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.ext.auth.authentication.AuthenticationProvider; 9 | import io.vertx.ext.auth.authentication.Credentials; 10 | import io.vertx.ext.auth.authentication.UsernamePasswordCredentials; 11 | import org.swisspush.redisques.util.RedisquesConfiguration; 12 | 13 | import java.util.Objects; 14 | import java.util.logging.Logger; 15 | 16 | /** 17 | * Custom implementation of a {@link AuthenticationProvider} using credentials from {@link RedisquesConfiguration} 18 | * 19 | * @author Marc-André Weber 20 | */ 21 | public class RedisquesConfigurationAuthentication implements AuthenticationProvider { 22 | 23 | private final static Logger logger = Logger.getLogger(RedisquesConfigurationAuthentication.class.getName()); 24 | 25 | private static final String INVALID_CREDENTIALS = "invalid username/password"; 26 | 27 | private static class User { 28 | final String name; 29 | final String password; 30 | 31 | private User(String name, String password) { 32 | this.name = Objects.requireNonNull(name); 33 | this.password = Objects.requireNonNull(password); 34 | } 35 | } 36 | 37 | private final User user; 38 | 39 | public RedisquesConfigurationAuthentication(RedisquesConfiguration configuration) { 40 | Objects.requireNonNull(configuration); 41 | 42 | String username = configuration.getHttpRequestHandlerUsername(); 43 | String password = configuration.getHttpRequestHandlerPassword(); 44 | 45 | if (StringUtil.isNullOrEmpty(username) || StringUtil.isNullOrEmpty(password)) { 46 | logger.warning("Username and/or password is missing/empty"); 47 | this.user = null; 48 | } else { 49 | this.user = new User(username, password); 50 | } 51 | } 52 | 53 | @Override 54 | public void authenticate(JsonObject authInfo, Handler> resultHandler) { 55 | authenticate(new UsernamePasswordCredentials(authInfo), resultHandler); 56 | } 57 | 58 | @Override 59 | public void authenticate(Credentials credentials, Handler> resultHandler) { 60 | try { 61 | UsernamePasswordCredentials authInfo = (UsernamePasswordCredentials) credentials; 62 | authInfo.checkValid(null); 63 | 64 | if(user == null) { 65 | resultHandler.handle(Future.failedFuture(INVALID_CREDENTIALS)); 66 | } else { 67 | if (Objects.equals(user.name, authInfo.getUsername()) 68 | && Objects.equals(user.password, authInfo.getPassword())) { 69 | resultHandler.handle(Future.succeededFuture(io.vertx.ext.auth.User.fromName(user.name))); 70 | } else { 71 | resultHandler.handle(Future.failedFuture(INVALID_CREDENTIALS)); 72 | } 73 | } 74 | } catch (RuntimeException e) { 75 | resultHandler.handle(Future.failedFuture(e)); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetQueueItemActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.eventbus.ReplyException; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import io.vertx.redis.client.impl.types.SimpleStringType; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mockito; 14 | import org.slf4j.Logger; 15 | import org.swisspush.redisques.util.QueueStatisticsCollector; 16 | 17 | import java.util.ArrayList; 18 | 19 | import static org.mockito.Mockito.*; 20 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetQueueItemOperation; 21 | 22 | /** 23 | * Tests for {@link GetQueueItemAction} class. 24 | * 25 | * @author Marc-André Weber 26 | */ 27 | @RunWith(VertxUnitRunner.class) 28 | public class GetQueueItemActionTest extends AbstractQueueActionTest { 29 | 30 | @Before 31 | @Override 32 | public void setup() { 33 | super.setup(); 34 | action = new GetQueueItemAction(vertx, redisService, keyspaceHelper, 35 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 36 | } 37 | 38 | @Test 39 | public void testGetQueueItemWhenRedisIsNotReady(TestContext context){ 40 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 41 | when(message.body()).thenReturn(buildGetQueueItemOperation("q1", 0)); 42 | 43 | action.execute(message); 44 | 45 | verify(message, times(1)).reply(isA(ReplyException.class)); 46 | verifyNoInteractions(redisAPI); 47 | } 48 | 49 | @Test 50 | public void testGetQueueItem(TestContext context){ 51 | when(message.body()).thenReturn(buildGetQueueItemOperation("q1", 0)); 52 | 53 | when(redisAPI.lindex(anyString(), anyString())).thenReturn(Future.succeededFuture(SimpleStringType.create(new JsonObject().put("foo", "bar").encode()))); 54 | 55 | action.execute(message); 56 | 57 | verify(redisAPI, times(1)).lindex(anyString(), anyString()); 58 | verify(message, times(1)).reply(eq(new JsonObject( 59 | Buffer.buffer("{\"status\":\"ok\",\"value\":\"{\\\"foo\\\":\\\"bar\\\"}\"}")))); 60 | } 61 | 62 | @Test 63 | public void testGetQueueItemNotExistingIndex(TestContext context){ 64 | when(message.body()).thenReturn(buildGetQueueItemOperation("q1", 0)); 65 | 66 | when(redisAPI.lindex(anyString(), anyString())).thenReturn(Future.succeededFuture()); 67 | action.execute(message); 68 | 69 | verify(redisAPI, times(1)).lindex(anyString(), anyString()); 70 | verify(message, times(1)).reply(eq(STATUS_ERROR)); 71 | } 72 | 73 | @Test 74 | public void testGetQueueItemLINDEXFail(TestContext context){ 75 | when(message.body()).thenReturn(buildGetQueueItemOperation("q1", 0)); 76 | 77 | when(redisAPI.lindex(anyString(), anyString())).thenReturn(Future.failedFuture("booom")); 78 | action.execute(message); 79 | 80 | verify(redisAPI, times(1)).lindex(anyString(), anyString()); 81 | verify(message, times(1)).reply(isA(ReplyException.class)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/action/LockedEnqueueAction.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.micrometer.core.instrument.MeterRegistry; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.eventbus.Message; 6 | import io.vertx.core.json.JsonObject; 7 | import org.slf4j.Logger; 8 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 9 | import org.swisspush.redisques.queue.KeyspaceHelper; 10 | import org.swisspush.redisques.queue.RedisService; 11 | import org.swisspush.redisques.util.MemoryUsageProvider; 12 | import org.swisspush.redisques.util.QueueConfiguration; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.Arrays; 16 | import java.util.List; 17 | 18 | import static org.swisspush.redisques.util.RedisquesAPI.*; 19 | 20 | public class LockedEnqueueAction extends EnqueueAction { 21 | 22 | public LockedEnqueueAction(Vertx vertx, RedisService redisService, 23 | KeyspaceHelper keyspaceHelper, List queueConfigurations, 24 | RedisQuesExceptionFactory exceptionFactory, 25 | QueueStatisticsCollector queueStatisticsCollector, Logger log, 26 | MemoryUsageProvider memoryUsageProvider, int memoryUsageLimitPercent, MeterRegistry meterRegistry, 27 | String metricsIdentifier) { 28 | super(vertx, redisService, keyspaceHelper, queueConfigurations, exceptionFactory, queueStatisticsCollector, log, memoryUsageProvider, 29 | memoryUsageLimitPercent, meterRegistry, metricsIdentifier); 30 | } 31 | 32 | @Override 33 | public void execute(Message event) { 34 | log.debug("RedisQues about to lockedEnqueue"); 35 | String queueName = event.body().getJsonObject(PAYLOAD).getString(QUEUENAME); 36 | if (isMemoryUsageLimitReached()) { 37 | log.warn("Failed to lockedEnqueue into queue {} because the memory usage limit is reached", queueName); 38 | incrEnqueueFailCount(); 39 | event.reply(createErrorReply().put(MESSAGE, MEMORY_FULL)); 40 | return; 41 | } 42 | JsonObject lockInfo = extractLockInfo(event.body().getJsonObject(PAYLOAD).getString(REQUESTED_BY)); 43 | if (lockInfo != null) { 44 | redisService.hmset(Arrays.asList(keyspaceHelper.getLocksKey(), queueName, lockInfo.encode())).onComplete(putLockResult -> { 45 | if (putLockResult.succeeded()) { 46 | log.debug("RedisQues lockedEnqueue locking successful, now going to enqueue"); 47 | enqueueActionExecute(event); 48 | } else { 49 | log.warn("RedisQues lockedEnqueue locking failed. Skip enqueue", 50 | new Exception(putLockResult.cause())); 51 | incrEnqueueFailCount(); 52 | event.reply(createErrorReply()); 53 | } 54 | }); 55 | } else { 56 | log.warn("RedisQues lockedEnqueue failed because property '{}' was missing", REQUESTED_BY); 57 | incrEnqueueFailCount(); 58 | event.reply(createErrorReply().put(MESSAGE, "Property '" + REQUESTED_BY + "' missing")); 59 | } 60 | } 61 | 62 | private void enqueueActionExecute(Message event) { 63 | super.execute(event); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetLockActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.eventbus.ReplyException; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import io.vertx.redis.client.impl.types.SimpleStringType; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mockito; 14 | import org.slf4j.Logger; 15 | import org.swisspush.redisques.util.QueueStatisticsCollector; 16 | 17 | import java.util.ArrayList; 18 | 19 | import static org.mockito.Mockito.*; 20 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetLockOperation; 21 | 22 | /** 23 | * Tests for {@link GetLockAction} class. 24 | * 25 | * @author Marc-André Weber 26 | */ 27 | @RunWith(VertxUnitRunner.class) 28 | public class GetLockActionTest extends AbstractQueueActionTest { 29 | 30 | @Before 31 | @Override 32 | public void setup() { 33 | super.setup(); 34 | action = new GetLockAction(vertx, redisService, keyspaceHelper, 35 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 36 | } 37 | 38 | @Test 39 | public void testGetLockWhenRedisIsNotReady(TestContext context){ 40 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 41 | when(message.body()).thenReturn(buildGetLockOperation("q1")); 42 | 43 | action.execute(message); 44 | 45 | verify(message, times(1)).reply(isA(ReplyException.class)); 46 | verifyNoInteractions(redisAPI); 47 | } 48 | 49 | @Test 50 | public void testGetLock(TestContext context){ 51 | when(message.body()).thenReturn(buildGetLockOperation("q1")); 52 | 53 | when(redisAPI.hget(anyString(), anyString())).thenReturn(Future.succeededFuture(SimpleStringType.create(new JsonObject().put("requestedBy", "UNKNOWN").put("timestamp", 1719931433522L).encode()))); 54 | 55 | action.execute(message); 56 | verify(redisAPI, times(1)).hget(anyString(), anyString()); 57 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"ok\"," + 58 | "\"value\":\"{\\\"requestedBy\\\":\\\"UNKNOWN\\\",\\\"timestamp\\\":1719931433522}\"}")))); 59 | } 60 | 61 | @Test 62 | public void testGetLockNoSuchLock(TestContext context){ 63 | when(message.body()).thenReturn(buildGetLockOperation("q1")); 64 | 65 | when(redisAPI.hget(anyString(), anyString())).thenReturn(Future.succeededFuture()); 66 | 67 | action.execute(message); 68 | 69 | verify(redisAPI, times(1)).hget(anyString(), anyString()); 70 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"No such lock\"}")))); 71 | } 72 | 73 | @Test 74 | public void testGetLockHGETFail(TestContext context){ 75 | when(message.body()).thenReturn(buildGetLockOperation("q1")); 76 | 77 | when(redisAPI.hget(anyString(), anyString())).thenReturn(Future.failedFuture("booom")); 78 | 79 | action.execute(message); 80 | 81 | verify(redisAPI, times(1)).hget(anyString(), anyString()); 82 | //verify(message, times(1)).fail(eq(0), eq("booom")); 83 | verify(message, times(1)).reply(isA(ReplyException.class)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/DeleteLockActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.eventbus.ReplyException; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.impl.types.SimpleStringType; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.slf4j.Logger; 13 | import org.swisspush.redisques.util.QueueStatisticsCollector; 14 | 15 | import java.util.ArrayList; 16 | 17 | import static org.mockito.Mockito.*; 18 | import static org.swisspush.redisques.util.RedisquesAPI.buildDeleteLockOperation; 19 | 20 | /** 21 | * Tests for {@link DeleteLockAction} class. 22 | * 23 | * @author Marc-André Weber 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class DeleteLockActionTest extends AbstractQueueActionTest { 27 | 28 | @Before 29 | @Override 30 | public void setup() { 31 | super.setup(); 32 | action = new DeleteLockAction(vertx, redisService, keyspaceHelper, 33 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 34 | } 35 | 36 | @Test 37 | public void testDeleteLockWhenRedisIsNotReady(TestContext context) { 38 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 39 | when(message.body()).thenReturn(buildDeleteLockOperation("testLock1")); 40 | 41 | action.execute(message); 42 | 43 | verify(message, times(1)).reply(isA(ReplyException.class)); 44 | verifyNoInteractions(redisAPI); 45 | } 46 | 47 | @Test 48 | public void testDeleteLock(TestContext context) { 49 | when(message.body()).thenReturn(buildDeleteLockOperation("testLock1")); 50 | 51 | when(redisAPI.exists(anyList())).thenReturn(Future.succeededFuture(SimpleStringType.create("0"))); 52 | when(redisAPI.hdel(anyList())).thenReturn(Future.succeededFuture()); 53 | 54 | action.execute(message); 55 | 56 | verify(redisAPI, times(1)).exists(anyList()); 57 | verify(redisAPI, times(1)).hdel(anyList()); 58 | verify(message, times(1)).reply(eq(STATUS_OK)); 59 | } 60 | 61 | @Test 62 | public void testDeleteLockExistsFail(TestContext context) { 63 | when(message.body()).thenReturn(buildDeleteLockOperation("testLock1")); 64 | 65 | when(redisAPI.exists(anyList())).thenReturn(Future.failedFuture("booom")); 66 | when(redisAPI.hdel(anyList())).thenReturn(Future.succeededFuture()); 67 | 68 | action.execute(message); 69 | 70 | verify(redisAPI, times(1)).exists(anyList()); 71 | verify(redisAPI, times(1)).hdel(anyList()); 72 | verify(message, times(1)).reply(eq(STATUS_OK)); 73 | } 74 | 75 | @Test 76 | public void testDeleteLockHDELFail(TestContext context) { 77 | when(message.body()).thenReturn(buildDeleteLockOperation("testLock1")); 78 | 79 | when(redisAPI.exists(anyList())).thenReturn(Future.succeededFuture(SimpleStringType.create("0"))); 80 | when(redisAPI.hdel(anyList())).thenReturn(Future.failedFuture("booom")); 81 | 82 | action.execute(message); 83 | 84 | verify(redisAPI, times(1)).exists(anyList()); 85 | verify(redisAPI, times(1)).hdel(anyList()); 86 | verify(message, times(1)).reply(isA(ReplyException.class)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/PutLockActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.eventbus.ReplyException; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.Mockito; 13 | import org.slf4j.Logger; 14 | import org.swisspush.redisques.util.QueueStatisticsCollector; 15 | import org.swisspush.redisques.util.RedisquesAPI; 16 | 17 | import java.util.ArrayList; 18 | 19 | import static org.mockito.Mockito.*; 20 | import static org.swisspush.redisques.util.RedisquesAPI.*; 21 | 22 | /** 23 | * Tests for {@link PutLockAction} class. 24 | * 25 | * @author Marc-André Weber 26 | */ 27 | @RunWith(VertxUnitRunner.class) 28 | public class PutLockActionTest extends AbstractQueueActionTest { 29 | 30 | @Before 31 | @Override 32 | public void setup() { 33 | super.setup(); 34 | action = new PutLockAction(vertx, redisService, keyspaceHelper, 35 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 36 | } 37 | 38 | @Test 39 | public void testPutLockWhenRedisIsNotReady(TestContext context){ 40 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 41 | when(message.body()).thenReturn(buildPutLockOperation("q1", "geronimo")); 42 | 43 | action.execute(message); 44 | 45 | verify(message, times(1)).reply(isA(ReplyException.class)); 46 | verifyNoInteractions(redisAPI); 47 | } 48 | 49 | @Test 50 | public void testPutLockWithoutRequestedByProperty(TestContext context){ 51 | when(message.body()).thenReturn(buildOperation(QueueOperation.putLock, new JsonObject().put(QUEUENAME, "q1"))); 52 | 53 | action.execute(message); 54 | 55 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\",\"message\":\"Property 'requestedBy' missing\"}")))); 56 | verifyNoInteractions(redisAPI); 57 | } 58 | 59 | @Test 60 | public void testPutLockWithInvalidQueuenameProperty(TestContext context){ 61 | when(message.body()).thenReturn(buildOperation(QueueOperation.putLock, new JsonObject().put(REQUESTED_BY, "geronimo"))); 62 | 63 | action.execute(message); 64 | 65 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\",\"errorType\":\"bad input\",\"message\":\"Lock must be a string value\"}")))); 66 | verifyNoInteractions(redisAPI); 67 | } 68 | 69 | @Test 70 | public void testPutLock(TestContext context){ 71 | when(message.body()).thenReturn(buildPutLockOperation("q1", "geronimo")); 72 | 73 | when(redisAPI.hmset(anyList())).thenReturn(Future.succeededFuture()); 74 | 75 | action.execute(message); 76 | 77 | verify(redisAPI, times(1)).hmset(anyList()); 78 | verify(message, times(1)).reply(eq(STATUS_OK)); 79 | } 80 | 81 | @Test 82 | public void testPutLockHMSETFail(TestContext context){ 83 | when(message.body()).thenReturn(buildPutLockOperation("q1", "geronimo")); 84 | 85 | when(redisAPI.hmset(anyList())).thenReturn(Future.failedFuture("booom")); 86 | action.execute(message); 87 | 88 | verify(redisAPI, times(1)).hmset(anyList()); 89 | verify(message, times(1)).reply(isA(ReplyException.class)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/LockUtil.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.Promise; 5 | import org.slf4j.Logger; 6 | import org.swisspush.redisques.exception.RedisQuesExceptionFactory; 7 | import org.swisspush.redisques.lock.Lock; 8 | 9 | /** 10 | * Class LockUtil 11 | * 12 | * @author https://github.com/mcweba [Marc-Andre Weber] 13 | */ 14 | public class LockUtil { 15 | 16 | private final RedisQuesExceptionFactory exceptionFactory; 17 | 18 | public LockUtil(RedisQuesExceptionFactory exceptionFactory) { 19 | this.exceptionFactory = exceptionFactory; 20 | } 21 | 22 | /** 23 | * Acquire a lock. Resolves always to Boolean.TRUE when no lock implementation is provided 24 | * 25 | * @param lockImpl the lock implementation 26 | * @param lock the lock 27 | * @param token the unique token 28 | * @param lockExpiryMs the expiry of the lock 29 | * @param log the logger 30 | * @return A boolean {@link Future} whether the lock could have been acquired or not 31 | */ 32 | public static Future acquireLock(Lock lockImpl, String lock, String token, long lockExpiryMs, Logger log){ 33 | Promise promise = Promise.promise(); 34 | 35 | if(lockImpl == null){ 36 | log.info("No lock implementation defined, going to pretend like we got the lock"); 37 | promise.complete(Boolean.TRUE); 38 | return promise.future(); 39 | } 40 | 41 | log.debug("Trying to acquire lock '{}' with token '{}' and expiry {}ms", lock, token, lockExpiryMs); 42 | lockImpl.acquireLock(lock, token, lockExpiryMs).onComplete(lockEvent -> { 43 | if(lockEvent.succeeded()){ 44 | if(lockEvent.result()){ 45 | log.debug("Acquired lock '{}' with token '{}'", lock, token); 46 | promise.complete(Boolean.TRUE); 47 | } else { 48 | promise.complete(Boolean.FALSE); 49 | } 50 | } else { 51 | promise.fail(lockEvent.cause()); 52 | } 53 | }); 54 | 55 | return promise.future(); 56 | } 57 | 58 | /** 59 | * Release a lock. 60 | * 61 | * @param lockImpl the lock implementation 62 | * @param lock the lock 63 | * @param token the unique token 64 | * @param log the Logger 65 | */ 66 | public void releaseLock(Lock lockImpl, String lock, String token, Logger log){ 67 | if(lockImpl == null){ 68 | log.info("No lock implementation defined, going to pretend like we released the lock"); 69 | return; 70 | } 71 | log.debug("Trying to release lock '{}' with token '{}'", lock, token); 72 | lockImpl.releaseLock(lock, token).onComplete(releaseEvent -> { 73 | if(releaseEvent.succeeded()){ 74 | if(releaseEvent.result()){ 75 | log.debug("Released lock '{}' with token '{}'", lock, token); 76 | } 77 | } else { 78 | log.error("Could not release lock '{}'.", lock, 79 | exceptionFactory.newException(releaseEvent.cause())); 80 | } 81 | }); 82 | } 83 | 84 | /** 85 | * Calculate the lock expiry time. This is a simple helper to work with the lock expiry time. 86 | * 87 | * @param taskInterval the interval of the task 88 | * @return the calculated lock expiry time 89 | */ 90 | public static long calcLockExpiry(long taskInterval) { 91 | return taskInterval <= 1 ? 1 : taskInterval / 2; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/util/StatuscodeTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.ext.unit.TestContext; 4 | import io.vertx.ext.unit.junit.VertxUnitRunner; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | /** 9 | * Tests for {@link StatusCode} class. 10 | * 11 | * @author Marc-André Weber 12 | */ 13 | @RunWith(VertxUnitRunner.class) 14 | public class StatuscodeTest { 15 | 16 | @Test 17 | public void testGetStatusCodeAndStatusMessage(TestContext context){ 18 | context.assertEquals(200, StatusCode.OK.getStatusCode()); 19 | context.assertEquals("OK", StatusCode.OK.getStatusMessage()); 20 | 21 | context.assertEquals(202, StatusCode.ACCEPTED.getStatusCode()); 22 | context.assertEquals("Accepted", StatusCode.ACCEPTED.getStatusMessage()); 23 | 24 | context.assertEquals(304, StatusCode.NOT_MODIFIED.getStatusCode()); 25 | context.assertEquals("Not Modified", StatusCode.NOT_MODIFIED.getStatusMessage()); 26 | 27 | context.assertEquals(400, StatusCode.BAD_REQUEST.getStatusCode()); 28 | context.assertEquals("Bad Request", StatusCode.BAD_REQUEST.getStatusMessage()); 29 | 30 | context.assertEquals(403, StatusCode.FORBIDDEN.getStatusCode()); 31 | context.assertEquals("Forbidden", StatusCode.FORBIDDEN.getStatusMessage()); 32 | 33 | context.assertEquals(404, StatusCode.NOT_FOUND.getStatusCode()); 34 | context.assertEquals("Not Found", StatusCode.NOT_FOUND.getStatusMessage()); 35 | 36 | context.assertEquals(405, StatusCode.METHOD_NOT_ALLOWED.getStatusCode()); 37 | context.assertEquals("Method Not Allowed", StatusCode.METHOD_NOT_ALLOWED.getStatusMessage()); 38 | 39 | context.assertEquals(409, StatusCode.CONFLICT.getStatusCode()); 40 | context.assertEquals("Conflict", StatusCode.CONFLICT.getStatusMessage()); 41 | 42 | context.assertEquals(500, StatusCode.INTERNAL_SERVER_ERROR.getStatusCode()); 43 | context.assertEquals("Internal Server Error", StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage()); 44 | 45 | context.assertEquals(503, StatusCode.SERVICE_UNAVAILABLE.getStatusCode()); 46 | context.assertEquals("Service Unavailable", StatusCode.SERVICE_UNAVAILABLE.getStatusMessage()); 47 | 48 | context.assertEquals(504, StatusCode.TIMEOUT.getStatusCode()); 49 | context.assertEquals("Gateway Timeout", StatusCode.TIMEOUT.getStatusMessage()); 50 | 51 | context.assertEquals(507, StatusCode.INSUFFICIENT_STORAGE.getStatusCode()); 52 | context.assertEquals("Insufficient Storage", StatusCode.INSUFFICIENT_STORAGE.getStatusMessage()); 53 | } 54 | 55 | @Test 56 | public void testStatusCodeFromCode(TestContext context){ 57 | context.assertNull(StatusCode.fromCode(0)); 58 | context.assertNull(StatusCode.fromCode(999)); 59 | context.assertEquals(StatusCode.OK, StatusCode.fromCode(200)); 60 | context.assertEquals(StatusCode.BAD_REQUEST, StatusCode.fromCode(400)); 61 | context.assertEquals(StatusCode.METHOD_NOT_ALLOWED, StatusCode.fromCode(405)); 62 | context.assertEquals(StatusCode.SERVICE_UNAVAILABLE, StatusCode.fromCode(503)); 63 | } 64 | 65 | @Test 66 | public void testStatusCodeToString(TestContext context){ 67 | context.assertEquals("200 OK", StatusCode.OK.toString()); 68 | context.assertEquals("304 Not Modified", StatusCode.NOT_MODIFIED.toString()); 69 | context.assertEquals("500 Internal Server Error", StatusCode.INTERNAL_SERVER_ERROR.toString()); 70 | context.assertEquals("504 Gateway Timeout", StatusCode.TIMEOUT.toString()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/swisspush/redisques/util/DefaultRedisReadyProvider.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.util; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.redis.client.RedisAPI; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | /** 14 | * Default implementation of the {@link RedisReadyProvider} based on the INFO command in Redis 15 | * 16 | * @author Marc-André Weber 17 | */ 18 | public class DefaultRedisReadyProvider implements RedisReadyProvider { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(DefaultRedisReadyProvider.class); 21 | private static final String DELIMITER = ":"; 22 | private static final String LOADING = "loading"; 23 | final AtomicBoolean redisReady = new AtomicBoolean(true); 24 | final AtomicBoolean updateRedisReady = new AtomicBoolean(true); 25 | 26 | /** 27 | * Constructor defining the "ready-state" update interval 28 | * @param vertx 29 | * @param updateIntervalMs interval in ms how often to update the "ready-state" 30 | */ 31 | public DefaultRedisReadyProvider(Vertx vertx, int updateIntervalMs) { 32 | vertx.setPeriodic(updateIntervalMs, l -> updateRedisReady.set(true)); 33 | } 34 | 35 | @Override 36 | public Future ready(RedisAPI redisAPI) { 37 | if(updateRedisReady.compareAndSet(true, false)){ 38 | return updateRedisReadyState(redisAPI); 39 | } 40 | return Future.succeededFuture(redisReady.get()); 41 | } 42 | 43 | /** 44 | * Call the INFO command in Redis with a constraint to persistence related information 45 | * 46 | * @param redisAPI 47 | * @return async boolean true when Redis is ready, otherwise false 48 | */ 49 | public Future updateRedisReadyState(RedisAPI redisAPI) { 50 | return redisAPI.info(List.of("Persistence")).compose(response -> { 51 | boolean ready = getReadyStateFromResponse(response.toString()); 52 | redisReady.set(ready); 53 | return Future.succeededFuture(ready); 54 | }, throwable -> { 55 | log.error("Error reading redis info", throwable); 56 | redisReady.set(false); 57 | return Future.succeededFuture(false); 58 | }); 59 | } 60 | 61 | /** 62 | * Check the response having a loading:0 entry. If so, Redis is ready. When the response contains a 63 | * loading:1 entry or not related entry at all, we consider Redis to be not ready 64 | * 65 | * @param persistenceInfo the response from Redis _INFO_ command 66 | * @return boolean true when Redis is ready, otherwise false 67 | */ 68 | private boolean getReadyStateFromResponse(String persistenceInfo) { 69 | byte loadingValue; 70 | try { 71 | Optional loadingOpt = persistenceInfo 72 | .lines() 73 | .filter(source -> source.startsWith(LOADING + DELIMITER)) 74 | .findAny(); 75 | if (loadingOpt.isEmpty()) { 76 | log.warn("No 'loading' section received from redis. Unable to calculate ready state"); 77 | return false; 78 | } 79 | loadingValue = Byte.parseByte(loadingOpt.get().split(DELIMITER)[1]); 80 | if (loadingValue == 0) { 81 | return true; 82 | } 83 | 84 | } catch (NumberFormatException ex) { 85 | log.warn("Invalid 'loading' section received from redis. Unable to calculate ready state"); 86 | return false; 87 | } 88 | 89 | return false; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/GetAllLocksActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.eventbus.ReplyException; 6 | import io.vertx.core.impl.future.FailedFuture; 7 | import io.vertx.core.impl.future.SucceededFuture; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.ext.unit.TestContext; 10 | import io.vertx.ext.unit.junit.VertxUnitRunner; 11 | import io.vertx.redis.client.impl.types.MultiType; 12 | import io.vertx.redis.client.impl.types.SimpleStringType; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mockito; 17 | import org.slf4j.Logger; 18 | import org.swisspush.redisques.util.QueueStatisticsCollector; 19 | 20 | import java.util.ArrayList; 21 | 22 | import static org.mockito.Mockito.*; 23 | import static org.swisspush.redisques.util.RedisquesAPI.buildGetAllLocksOperation; 24 | 25 | /** 26 | * Tests for {@link GetAllLocksAction} class. 27 | * 28 | * @author Marc-André Weber 29 | */ 30 | @RunWith(VertxUnitRunner.class) 31 | public class GetAllLocksActionTest extends AbstractQueueActionTest { 32 | 33 | @Before 34 | @Override 35 | public void setup() { 36 | super.setup(); 37 | action = new GetAllLocksAction(vertx, redisService, keyspaceHelper, 38 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 39 | } 40 | 41 | @Test 42 | public void testGetAllLocksWhenRedisIsNotReady(TestContext context){ 43 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 44 | when(message.body()).thenReturn(buildGetAllLocksOperation()); 45 | 46 | action.execute(message); 47 | 48 | verify(message, times(1)).reply(isA(ReplyException.class)); 49 | verifyNoInteractions(redisAPI); 50 | } 51 | 52 | @Test 53 | public void testGetAllLocksInvalidFilter(TestContext context){ 54 | when(message.body()).thenReturn(buildGetAllLocksOperation("xyz(.*")); 55 | 56 | action.execute(message); 57 | 58 | String newlineEscaped = System.lineSeparator() 59 | .replace("\n", "\\n").replace("\r", "\\r"); 60 | var expectedResult = new JsonObject(Buffer.buffer("" 61 | + "{\"status\":\"error\",\"errorType\":\"bad input\",\"message\":" 62 | + "\"Error while compile regex pattern. Cause: Unclosed group near index 6" 63 | + newlineEscaped 64 | + "xyz(.*\"}")); 65 | verify(message, times(1)).reply(eq(expectedResult)); 66 | 67 | verifyNoInteractions(redisAPI); 68 | } 69 | 70 | @Test 71 | public void testGetAllLocksHKEYSFail(TestContext context){ 72 | when(message.body()).thenReturn(buildGetAllLocksOperation()); 73 | 74 | when(redisAPI.hkeys(anyString())).thenReturn(new FailedFuture("booom")); 75 | 76 | action.execute(message); 77 | 78 | verify(redisAPI, times(1)).hkeys(anyString()); 79 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"error\"}")))); 80 | } 81 | 82 | @Test 83 | public void testGetAllLocks(TestContext context){ 84 | when(message.body()).thenReturn(buildGetAllLocksOperation()); 85 | 86 | MultiType response = MultiType.create(2, false); 87 | response.add(SimpleStringType.create("foo")); 88 | response.add(SimpleStringType.create("bar")); 89 | when(redisAPI.hkeys(anyString())).thenReturn(new SucceededFuture<>(response)); 90 | 91 | action.execute(message); 92 | 93 | verify(redisAPI, times(1)).hkeys(anyString()); 94 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"ok\",\"value\":{\"locks\":[\"foo\",\"bar\"]}}")))); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/RedisQuesProcessorDelayedExecutionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques; 2 | 3 | import org.awaitility.Awaitility; 4 | import io.vertx.core.DeploymentOptions; 5 | import io.vertx.core.Vertx; 6 | import io.vertx.core.eventbus.MessageConsumer; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.ext.unit.Async; 9 | import io.vertx.ext.unit.TestContext; 10 | import io.vertx.ext.unit.junit.Timeout; 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.swisspush.redisques.util.RedisquesConfiguration; 16 | import redis.clients.jedis.Jedis; 17 | 18 | import java.time.Duration; 19 | import java.util.concurrent.atomic.AtomicBoolean; 20 | 21 | import static org.hamcrest.CoreMatchers.equalTo; 22 | import static org.swisspush.redisques.util.RedisquesAPI.*; 23 | 24 | /** 25 | * @author Marc-André Weber 26 | */ 27 | public class RedisQuesProcessorDelayedExecutionTest extends AbstractTestCase { 28 | 29 | private static MessageConsumer queueProcessor = null; 30 | 31 | private static final String CUSTOM_REDIS_KEY_PREFIX = "mycustomredisprefix:"; 32 | private static final String CUSTOM_REDISQUES_ADDRESS = "customredisques"; 33 | 34 | @Override 35 | protected String getRedisPrefix() { 36 | return CUSTOM_REDIS_KEY_PREFIX; 37 | } 38 | 39 | @Override 40 | protected String getRedisquesAddress() { 41 | return CUSTOM_REDISQUES_ADDRESS; 42 | } 43 | 44 | @Rule 45 | public Timeout rule = Timeout.seconds(300); 46 | 47 | @Before 48 | public void createQueueProcessor(TestContext context) { 49 | deployRedisques(context); 50 | queueProcessor = vertx.eventBus().consumer("processor-address"); 51 | } 52 | 53 | @After 54 | public void tearDown(TestContext context) { 55 | vertx.close(context.asyncAssertSuccess()); 56 | } 57 | 58 | protected void deployRedisques(TestContext context) { 59 | vertx = Vertx.vertx(); 60 | JsonObject config = RedisquesConfiguration.with() 61 | .address(getRedisquesAddress()) 62 | .redisPrefix(CUSTOM_REDIS_KEY_PREFIX) 63 | .processorAddress("processor-address") 64 | .refreshPeriod(2) 65 | .processorDelayMax(1500) 66 | .build() 67 | .asJsonObject(); 68 | 69 | RedisQues redisQues = new RedisQues(); 70 | vertx.deployVerticle(redisQues, new DeploymentOptions().setConfig(config), context.asyncAssertSuccess(event -> { 71 | deploymentId = event; 72 | log.info("vert.x Deploy - {} was successful.", redisQues.getClass().getSimpleName()); 73 | jedis = new Jedis("localhost", 6379, 5000); 74 | })); 75 | } 76 | 77 | @Test 78 | public void queueProcessorShouldBeNotifiedWithNonLockedQueue(TestContext context) { 79 | Async async = context.async(); 80 | flushAll(); 81 | 82 | String queue = "queue1"; 83 | final AtomicBoolean processorCalled = new AtomicBoolean(false); 84 | 85 | long start = System.currentTimeMillis(); 86 | 87 | queueProcessor.handler(event -> { 88 | long duration = System.currentTimeMillis() - start; 89 | context.assertTrue(duration < 1600, "QueueProcessor should have been called at the latest after 1600ms"); 90 | processorCalled.set(true); 91 | }); 92 | 93 | eventBusSend(buildEnqueueOperation(queue, "hello"), reply -> 94 | context.assertEquals(OK, reply.result().body().getString(STATUS))); 95 | 96 | // after at most 5 seconds, the processor-address consumer should have been called 97 | Awaitility.await().atMost(Duration.ofSeconds(5)).until(processorCalled::get, equalTo(true)); 98 | 99 | async.complete(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/DeleteQueueItemActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.eventbus.ReplyException; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mockito; 11 | import org.slf4j.Logger; 12 | import org.swisspush.redisques.util.QueueStatisticsCollector; 13 | 14 | import java.util.ArrayList; 15 | 16 | import static org.mockito.Mockito.*; 17 | import static org.swisspush.redisques.util.RedisquesAPI.buildDeleteQueueItemOperation; 18 | 19 | /** 20 | * Tests for {@link DeleteQueueItemAction} class. 21 | * 22 | * @author Marc-André Weber 23 | */ 24 | @RunWith(VertxUnitRunner.class) 25 | public class DeleteQueueItemActionTest extends AbstractQueueActionTest { 26 | 27 | @Before 28 | @Override 29 | public void setup() { 30 | super.setup(); 31 | action = new DeleteQueueItemAction(vertx, redisService, keyspaceHelper, 32 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 33 | } 34 | 35 | @Test 36 | public void testDeleteQueueItemWhenRedisIsNotReady(TestContext context){ 37 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 38 | when(message.body()).thenReturn(buildDeleteQueueItemOperation("queue1", 0)); 39 | 40 | action.execute(message); 41 | 42 | verify(message, times(1)).reply(isA(ReplyException.class)); 43 | verifyNoInteractions(redisAPI); 44 | } 45 | 46 | @Test 47 | public void testFailedLSET(TestContext context){ 48 | when(redisAPI.lset(anyString(), anyString(), anyString())).thenReturn(Future.failedFuture("boooom")); 49 | 50 | when(message.body()).thenReturn(buildDeleteQueueItemOperation("queue1", 0)); 51 | 52 | action.execute(message); 53 | 54 | verify(redisAPI, times(1)).lset(anyString(), eq("0"), eq("TO_DELETE")); 55 | verify(redisAPI, never()).lrem(anyString(), anyString(), anyString()); 56 | verify(message, times(1)).reply(isA(ReplyException.class)); 57 | } 58 | 59 | @Test 60 | public void testFailedLSETNoSuchKey(TestContext context){ 61 | when(redisAPI.lset(anyString(), anyString(), anyString())).thenReturn(Future.failedFuture("ERR no such key")); 62 | 63 | when(message.body()).thenReturn(buildDeleteQueueItemOperation("queue1", 0)); 64 | 65 | action.execute(message); 66 | 67 | verify(redisAPI, times(1)).lset(anyString(), eq("0"), eq("TO_DELETE")); 68 | verify(redisAPI, never()).lrem(anyString(), anyString(), anyString()); 69 | verify(message, times(1)).reply(eq(STATUS_ERROR)); 70 | } 71 | 72 | @Test 73 | public void testFailedLREM(TestContext context){ 74 | when(redisAPI.lset(anyString(), anyString(), anyString())).thenReturn(Future.succeededFuture()); 75 | when(redisAPI.lrem(anyString(), anyString(), anyString())).thenReturn(Future.failedFuture("boooom")); 76 | 77 | when(message.body()).thenReturn(buildDeleteQueueItemOperation("queue1", 0)); 78 | 79 | action.execute(message); 80 | 81 | verify(redisAPI, times(1)).lset(anyString(), eq("0"), eq("TO_DELETE")); 82 | verify(redisAPI, times(1)).lrem(anyString(), eq("0"), eq("TO_DELETE")); 83 | verify(message, times(1)).reply(isA(ReplyException.class)); 84 | } 85 | 86 | @Test 87 | public void testDeleteQueueItem(TestContext context){ 88 | when(redisAPI.lset(anyString(), anyString(), anyString())).thenReturn(Future.succeededFuture()); 89 | when(redisAPI.lrem(anyString(), anyString(), anyString())).thenReturn(Future.succeededFuture()); 90 | 91 | when(message.body()).thenReturn(buildDeleteQueueItemOperation("queue1", 0)); 92 | 93 | action.execute(message); 94 | 95 | verify(redisAPI, times(1)).lset(anyString(), eq("0"), eq("TO_DELETE")); 96 | verify(redisAPI, times(1)).lrem(anyString(), eq("0"), eq("TO_DELETE")); 97 | verify(message, times(1)).reply(eq(STATUS_OK)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/org/swisspush/redisques/action/DeleteAllQueueItemsActionTest.java: -------------------------------------------------------------------------------- 1 | package org.swisspush.redisques.action; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.eventbus.ReplyException; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import io.vertx.redis.client.impl.types.SimpleStringType; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mockito; 14 | import org.slf4j.Logger; 15 | import org.swisspush.redisques.util.QueueStatisticsCollector; 16 | 17 | import java.util.ArrayList; 18 | 19 | import static org.mockito.Mockito.*; 20 | import static org.swisspush.redisques.util.RedisquesAPI.buildDeleteAllQueueItemsOperation; 21 | 22 | /** 23 | * Tests for {@link DeleteAllQueueItemsAction} class. 24 | * 25 | * @author Marc-André Weber 26 | */ 27 | @RunWith(VertxUnitRunner.class) 28 | public class DeleteAllQueueItemsActionTest extends AbstractQueueActionTest { 29 | 30 | @Before 31 | @Override 32 | public void setup() { 33 | super.setup(); 34 | action = new DeleteAllQueueItemsAction(vertx, redisService, keyspaceHelper, 35 | new ArrayList<>(), exceptionFactory, Mockito.mock(QueueStatisticsCollector.class), Mockito.mock(Logger.class)); 36 | } 37 | 38 | @Test 39 | public void testDeleteAllQueueItemsNoUnlock(TestContext context){ 40 | when(message.body()).thenReturn(buildDeleteAllQueueItemsOperation("q1")); 41 | 42 | when(redisAPI.del(anyList())).thenReturn(Future.succeededFuture(SimpleStringType.create("1"))); 43 | 44 | action.execute(message); 45 | 46 | verify(redisAPI, times(1)).del(anyList()); 47 | verify(redisAPI, never()).hdel(anyList()); 48 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"ok\",\"value\":1}")))); 49 | } 50 | 51 | @Test 52 | public void testDeleteAllQueueItemsWithUnlock(TestContext context){ 53 | when(message.body()).thenReturn(buildDeleteAllQueueItemsOperation("q1", true)); 54 | 55 | when(redisAPI.del(anyList())).thenReturn(Future.succeededFuture(SimpleStringType.create("1"))); 56 | when(redisAPI.hdel(anyList())).thenReturn(Future.succeededFuture(SimpleStringType.create("1"))); 57 | 58 | action.execute(message); 59 | 60 | verify(redisAPI, times(1)).del(anyList()); 61 | verify(redisAPI, times(1)).hdel(anyList()); 62 | verify(message, times(1)).reply(eq(new JsonObject(Buffer.buffer("{\"status\":\"ok\",\"value\":1}")))); 63 | } 64 | 65 | @Test 66 | public void testDeleteAllQueueItemsWhenRedisIsNotReady(TestContext context){ 67 | when(redisProvider.redis()).thenReturn(Future.failedFuture("not ready")); 68 | when(message.body()).thenReturn(buildDeleteAllQueueItemsOperation("q1")); 69 | 70 | action.execute(message); 71 | 72 | verify(message, times(1)).reply(isA(ReplyException.class)); 73 | verifyNoInteractions(redisAPI); 74 | } 75 | 76 | @Test 77 | public void testRedisApiDELFail(TestContext context){ 78 | when(message.body()).thenReturn(buildDeleteAllQueueItemsOperation("q1")); 79 | 80 | when(redisAPI.del(anyList())).thenReturn(Future.failedFuture("boooom")); 81 | 82 | action.execute(message); 83 | 84 | verify(redisAPI, times(1)).del(anyList()); 85 | verify(message, times(1)).reply(isA(ReplyException.class)); 86 | } 87 | 88 | @Test 89 | public void testRedisApiUnlockFail(TestContext context){ 90 | when(message.body()).thenReturn(buildDeleteAllQueueItemsOperation("q1", true)); 91 | 92 | when(redisAPI.del(anyList())).thenReturn(Future.succeededFuture(SimpleStringType.create("1"))); 93 | when(redisAPI.hdel(anyList())).thenReturn(Future.failedFuture("boooom")); 94 | 95 | action.execute(message); 96 | 97 | verify(redisAPI, times(1)).del(anyList()); 98 | verify(redisAPI, times(1)).hdel(anyList()); 99 | verify(message, times(1)).reply(isA(ReplyException.class)); 100 | //verify(message, times(1)).fail(eq(0), eq("boooom")); 101 | } 102 | } 103 | --------------------------------------------------------------------------------