From 3c378e4cc5f46bcae7aa18a2c8eb16c3f30e2419 Mon Sep 17 00:00:00 2001 From: Sonu Kumar Date: Tue, 24 Mar 2026 01:45:53 +0530 Subject: [PATCH 1/5] add worker heart beat --- .../rqueue/common/RqueueRedisTemplate.java | 21 + .../sonus21/rqueue/config/RqueueConfig.java | 48 +- .../config/RqueueListenerBaseConfig.java | 16 + .../rqueue/config/RqueueWebConfig.java | 6 + .../rqueue/core/RqueueBeanProvider.java | 4 + .../rqueue/core/RqueueMessageIdGenerator.java | 23 + .../rqueue/core/RqueueMessageTemplate.java | 2 + .../rqueue/core/impl/BaseMessageSender.java | 55 +- .../rqueue/core/impl/MessageSweeper.java | 2 - .../ReactiveRqueueMessageEnqueuerImpl.java | 34 +- .../core/impl/RqueueEndpointManagerImpl.java | 15 +- .../core/impl/RqueueMessageEnqueuerImpl.java | 15 +- .../core/impl/RqueueMessageManagerImpl.java | 15 +- .../core/impl/RqueueMessageTemplateImpl.java | 6 + .../impl/UuidV4RqueueMessageIdGenerator.java | 28 + .../core/support/RqueueMessageUtils.java | 93 +- .../rqueue/listener/DefaultRqueuePoller.java | 1 + .../rqueue/listener/RqueueMessagePoller.java | 13 + .../sonus21/rqueue/models/db/QueueConfig.java | 5 + .../rqueue/models/enums/ActionType.java | 1 + .../sonus21/rqueue/models/enums/NavTab.java | 1 + .../models/registry/RqueueWorkerInfo.java | 42 + .../registry/RqueueWorkerPollerMetadata.java | 44 + .../registry/RqueueWorkerPollerView.java | 51 + .../models/registry/RqueueWorkerView.java | 52 + .../models/request/MessageEnqueueRequest.java | 45 + .../sonus21/rqueue/utils/DateTimeUtils.java | 74 +- .../sonus21/rqueue/utils/QueueThreadPool.java | 1 - .../MissingRqueueMessageIdGenerator.java | 36 + .../rqueue/utils/pebble/DurationFunction.java | 42 + .../pebble/ReadableDateTimeFunction.java | 42 + .../utils/pebble/RqueuePebbleExtension.java | 2 + .../ReactiveRqueueRestController.java | 12 + .../ReactiveRqueueViewController.java | 23 +- .../web/controller/RqueueRestController.java | 12 + .../web/controller/RqueueViewController.java | 23 +- .../web/service/RqueueQDetailService.java | 3 + .../web/service/RqueueUtilityService.java | 4 + .../service/RqueueViewControllerService.java | 4 +- .../impl/RqueueQDetailServiceImpl.java | 23 +- .../impl/RqueueUtilityServiceImpl.java | 46 + .../impl/RqueueViewControllerServiceImpl.java | 122 +- .../rqueue/worker/RqueueWorkerRegistry.java | 31 + .../worker/RqueueWorkerRegistryImpl.java | 339 ++++ .../resources/public/rqueue/css/rqueue.css | 1419 ++++++++++++++++- .../main/resources/public/rqueue/js/rqueue.js | 146 +- .../main/resources/templates/rqueue/base.html | 10 +- .../templates/rqueue/data_explorer_modal.html | 65 +- .../templates/rqueue/queue_detail.html | 105 +- .../resources/templates/rqueue/queues.html | 274 +++- .../resources/templates/rqueue/workers.html | 161 ++ .../rqueue/config/RqueueConfigTest.java | 37 + .../core/RqueueMessageTemplateTest.java | 24 +- .../impl/RqueueMessageEnqueuerImplTest.java | 31 +- .../core/support/RqueueMessageUtilsTest.java | 79 +- .../sonus21/rqueue/listener/JobImplTest.java | 4 +- .../rqueue/listener/RqueueExecutorTest.java | 19 +- .../rqueue/listener/RqueueMiddlewareTest.java | 55 +- .../rqueue/utils/DateTimeUtilsTest.java | 17 + .../utils/MessageMetadataTestUtils.java | 5 +- .../rqueue/utils/RqueueMessageTestUtils.java | 9 +- .../pebble/RqueuePebbleExtensionTest.java | 65 + .../web/service/RqueueQDetailServiceTest.java | 66 +- .../web/service/RqueueUtilityServiceTest.java | 61 +- .../worker/RqueueWorkerRegistryImplTest.java | 216 +++ .../spring/boot/RqueueListenerAutoConfig.java | 29 +- .../RqueueMessageTemplateTest.java | 19 +- .../unit/RqueueListenerAutoConfigTest.java | 12 +- .../rqueue/test/common/SpringTestBase.java | 84 +- .../rqueue/spring/RqueueListenerConfig.java | 29 +- .../tests/unit/RqueueMessageConfigTest.java | 7 +- 71 files changed, 4137 insertions(+), 388 deletions(-) create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueMessageIdGenerator.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/UuidV4RqueueMessageIdGenerator.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerInfo.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerPollerMetadata.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerPollerView.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerView.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/models/request/MessageEnqueueRequest.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/condition/MissingRqueueMessageIdGenerator.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/DurationFunction.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/ReadableDateTimeFunction.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistry.java create mode 100644 rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImpl.java create mode 100644 rqueue-core/src/main/resources/templates/rqueue/workers.html create mode 100644 rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/pebble/RqueuePebbleExtensionTest.java create mode 100644 rqueue-core/src/test/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImplTest.java diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/common/RqueueRedisTemplate.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/common/RqueueRedisTemplate.java index a51ea14e..91329fab 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/common/RqueueRedisTemplate.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/common/RqueueRedisTemplate.java @@ -66,6 +66,10 @@ public Long rpush(String listName, V val) { return redisTemplate.opsForList().rightPush(listName, val); } + public Long lpush(String listName, V val) { + return redisTemplate.opsForList().leftPush(listName, val); + } + public Long addToSet(String setName, V... values) { return redisTemplate.opsForSet().add(setName, values); } @@ -98,6 +102,23 @@ public void set(String key, V val, Duration duration) { redisTemplate.opsForValue().set(key, val, duration.toMillis(), TimeUnit.MILLISECONDS); } + public void putHashValue(String key, String hashKey, V val) { + redisTemplate.opsForHash().put(key, hashKey, val); + } + + @SuppressWarnings("unchecked") + public Map getHashEntries(String key) { + return (Map) (Map) redisTemplate.opsForHash().entries(key); + } + + public Long deleteHashValues(String key, String... hashKeys) { + return redisTemplate.opsForHash().delete(key, (Object[]) hashKeys); + } + + public Boolean expire(String key, Duration duration) { + return redisTemplate.expire(key, duration.toMillis(), TimeUnit.MILLISECONDS); + } + public Boolean setIfAbsent(String lockKey, V val, Duration duration) { boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, val); if (result) { diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java index f487d08f..6266b523 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java @@ -43,6 +43,7 @@ @Configuration public class RqueueConfig { + @Getter private static final String brokerId = UUID.randomUUID().toString(); private static final AtomicLong counter = new AtomicLong(1); private final RedisConnectionFactory connectionFactory; @@ -139,9 +140,26 @@ public class RqueueConfig { @Value("${rqueue.completed.job.cleanup.interval:30000}") private long completedJobCleanupIntervalInMs; - public static String getBrokerId() { - return brokerId; - } + @Value("${rqueue.worker.registry.enabled:true}") + private boolean workerRegistryEnabled; + + @Value("${rqueue.worker.registry.worker.ttl:300}") + private long workerRegistryWorkerTtlInSeconds; + + @Value("${rqueue.worker.registry.worker.heartbeat.interval:60}") + private long workerRegistryWorkerHeartbeatIntervalInSeconds; + + @Value("${rqueue.worker.registry.queue.ttl:3600}") + private long workerRegistryQueueTtlInSeconds; + + @Value("${rqueue.worker.registry.queue.heartbeat.interval:15}") + private long workerRegistryQueueHeartbeatIntervalInSeconds; + + @Value("${rqueue.worker.registry.key.prefix:worker::}") + private String workerRegistryKeyPrefix; + + @Value("${rqueue.worker.registry.queue.key.prefix:q-pollers::}") + private String workerRegistryQueueKeyPrefix; public boolean messageInTerminalStateShouldBeStored() { return getMessageDurabilityInTerminalStateInSecond() > 0; @@ -294,6 +312,14 @@ public String getJobsKey(String messageId) { return prefix + jobsCollectionNamePrefix + messageId; } + public String getWorkerRegistryKey(String workerId) { + return prefix + workerRegistryKeyPrefix + workerId; + } + + public String getWorkerRegistryQueueKey(String queueName) { + return prefix + workerRegistryQueueKeyPrefix + getTaggedName(queueName); + } + public String getDelDataName(String queueName) { return prefix + delPrefix @@ -307,6 +333,22 @@ public Duration getJobDurabilityInTerminalState() { return Duration.ofSeconds(jobDurabilityInTerminalStateInSecond); } + public Duration getWorkerRegistryWorkerTtl() { + return Duration.ofSeconds(workerRegistryWorkerTtlInSeconds); + } + + public Duration getWorkerRegistryWorkerHeartbeatInterval() { + return Duration.ofSeconds(workerRegistryWorkerHeartbeatIntervalInSeconds); + } + + public Duration getWorkerRegistryQueueTtl() { + return Duration.ofSeconds(workerRegistryQueueTtlInSeconds); + } + + public Duration getWorkerRegistryQueueHeartbeatInterval() { + return Duration.ofSeconds(workerRegistryQueueHeartbeatIntervalInSeconds); + } + public String getLibVersion() { if (StringUtils.isEmpty(version)) { ClassPathResource resource = diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java index 7bda3979..8fc13dd2 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java @@ -25,15 +25,20 @@ import com.github.sonus21.rqueue.core.ProcessingQueueMessageScheduler; import com.github.sonus21.rqueue.core.RqueueBeanProvider; import com.github.sonus21.rqueue.core.RqueueInternalPubSubChannel; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.core.RqueueRedisListenerContainerFactory; import com.github.sonus21.rqueue.core.ScheduledQueueMessageScheduler; import com.github.sonus21.rqueue.core.impl.RqueueMessageTemplateImpl; +import com.github.sonus21.rqueue.core.impl.UuidV4RqueueMessageIdGenerator; import com.github.sonus21.rqueue.dao.RqueueStringDao; import com.github.sonus21.rqueue.dao.impl.RqueueStringDaoImpl; import com.github.sonus21.rqueue.listener.RqueueMessageListenerContainer; import com.github.sonus21.rqueue.metrics.RqueueQueueMetrics; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistryImpl; import com.github.sonus21.rqueue.utils.RedisUtils; +import com.github.sonus21.rqueue.utils.condition.MissingRqueueMessageIdGenerator; import com.github.sonus21.rqueue.utils.condition.ReactiveEnabled; import com.github.sonus21.rqueue.utils.pebble.ResourceLoader; import com.github.sonus21.rqueue.utils.pebble.RqueuePebbleExtension; @@ -151,6 +156,12 @@ public RqueueWebConfig rqueueWebConfig() { return new RqueueWebConfig(); } + @Bean + @Conditional(MissingRqueueMessageIdGenerator.class) + public RqueueMessageIdGenerator rqueueMessageIdGenerator() { + return new UuidV4RqueueMessageIdGenerator(); + } + @Bean public RqueueSchedulerConfig rqueueSchedulerConfig() { return new RqueueSchedulerConfig(); @@ -215,6 +226,11 @@ public RqueueStringDao rqueueStringDao(RqueueConfig rqueueConfig) { return new RqueueStringDaoImpl(rqueueConfig); } + @Bean + public RqueueWorkerRegistry rqueueWorkerRegistry(RqueueConfig rqueueConfig) { + return new RqueueWorkerRegistryImpl(rqueueConfig); + } + @Bean public RqueueLockManager rqueueLockManager(RqueueStringDao rqueueStringDao) { return new RqueueLockManagerImpl(rqueueStringDao); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueWebConfig.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueWebConfig.java index 934f35a4..34cbd6de 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueWebConfig.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueWebConfig.java @@ -48,6 +48,12 @@ public class RqueueWebConfig { @Value("${rqueue.web.max.message.move.count:1000}") private int maxMessageMoveCount; + @Value("${rqueue.web.queue.page.size:12}") + private int queuePageSize; + + @Value("${rqueue.web.worker.page.size:10}") + private int workerPageSize; + /** * Whether queue stats should be collected or not. When this flag is disabled, metric data won't * be available in the dashboard. diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueBeanProvider.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueBeanProvider.java index f0512f57..f0248a3f 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueBeanProvider.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueBeanProvider.java @@ -24,6 +24,7 @@ import com.github.sonus21.rqueue.dao.RqueueSystemConfigDao; import com.github.sonus21.rqueue.listener.RqueueMessageHandler; import com.github.sonus21.rqueue.metrics.RqueueMetricsCounter; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; import com.github.sonus21.rqueue.web.service.RqueueMessageMetadataService; import lombok.Getter; import lombok.Setter; @@ -55,6 +56,9 @@ public class RqueueBeanProvider { @Autowired(required = false) private RqueueMetricsCounter rqueueMetricsCounter; + @Autowired(required = false) + private RqueueWorkerRegistry rqueueWorkerRegistry; + @Autowired private RqueueMessageHandler rqueueMessageHandler; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueMessageIdGenerator.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueMessageIdGenerator.java new file mode 100644 index 00000000..75a44cfe --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueMessageIdGenerator.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.core; + +@FunctionalInterface +public interface RqueueMessageIdGenerator { + + String generate(); +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueMessageTemplate.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueMessageTemplate.java index ff3fc097..be5a1550 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueMessageTemplate.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueMessageTemplate.java @@ -49,6 +49,8 @@ void moveMessageWithDelay( Long addMessage(String queueName, RqueueMessage rqueueMessage); + Long addMessageAtFront(String queueName, RqueueMessage rqueueMessage); + Boolean addToZset(String zsetName, RqueueMessage rqueueMessage, long score); List getAllMessages( diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/BaseMessageSender.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/BaseMessageSender.java index 55ff06c8..d7f499cc 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/BaseMessageSender.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/BaseMessageSender.java @@ -26,6 +26,7 @@ import com.github.sonus21.rqueue.config.RqueueConfig; import com.github.sonus21.rqueue.core.EndpointRegistry; import com.github.sonus21.rqueue.core.RqueueMessage; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.core.impl.MessageSweeper.MessageDeleteRequest; import com.github.sonus21.rqueue.dao.RqueueStringDao; @@ -51,25 +52,23 @@ abstract class BaseMessageSender { protected final MessageHeaders messageHeaders; protected final MessageConverter messageConverter; protected final RqueueMessageTemplate messageTemplate; - - @Autowired - protected RqueueStringDao rqueueStringDao; - - @Autowired - protected RqueueConfig rqueueConfig; - - @Autowired - protected RqueueMessageMetadataService rqueueMessageMetadataService; + protected final RqueueMessageIdGenerator messageIdGenerator; + @Autowired protected RqueueStringDao rqueueStringDao; + @Autowired protected RqueueConfig rqueueConfig; + @Autowired protected RqueueMessageMetadataService rqueueMessageMetadataService; BaseMessageSender( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, - MessageHeaders messageHeaders) { + MessageHeaders messageHeaders, + RqueueMessageIdGenerator messageIdGenerator) { notNull(messageTemplate, "messageTemplate cannot be null"); notNull(messageConverter, "messageConverter cannot be null"); + notNull(messageIdGenerator, "messageIdGenerator cannot be null"); this.messageTemplate = messageTemplate; this.messageConverter = messageConverter; this.messageHeaders = messageHeaders; + this.messageIdGenerator = messageIdGenerator; } protected Object storeMessageMetadata( @@ -119,14 +118,16 @@ protected String pushMessage( Long delayInMilliSecs, boolean isUnique) { QueueDetail queueDetail = EndpointRegistry.get(queueName); - RqueueMessage rqueueMessage = buildMessage( - messageConverter, - queueName, - messageId, - message, - retryCount, - delayInMilliSecs, - messageHeaders); + RqueueMessage rqueueMessage = + buildMessage( + messageIdGenerator, + messageConverter, + queueName, + messageId, + message, + retryCount, + delayInMilliSecs, + messageHeaders); try { storeMessageMetadata(rqueueMessage, delayInMilliSecs, false, isUnique); enqueue(queueDetail, rqueueMessage, delayInMilliSecs, false); @@ -146,14 +147,16 @@ protected String pushMessage( protected String pushPeriodicMessage( String queueName, String messageId, Object message, long periodInMilliSeconds) { QueueDetail queueDetail = EndpointRegistry.get(queueName); - RqueueMessage rqueueMessage = buildPeriodicMessage( - messageConverter, - queueName, - messageId, - message, - null, - periodInMilliSeconds, - messageHeaders); + RqueueMessage rqueueMessage = + buildPeriodicMessage( + messageIdGenerator, + messageConverter, + queueName, + messageId, + message, + null, + periodInMilliSeconds, + messageHeaders); try { storeMessageMetadata(rqueueMessage, periodInMilliSeconds, false, false); enqueue(queueDetail, rqueueMessage, periodInMilliSeconds, false); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/MessageSweeper.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/MessageSweeper.java index b8279c84..16e56459 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/MessageSweeper.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/MessageSweeper.java @@ -42,9 +42,7 @@ @Slf4j public class MessageSweeper { - private static MessageSweeper messageSweeper; - private final ExecutorService executorService; private final RqueueMessageTemplate messageTemplate; private final RqueueMessageMetadataService rqueueMessageMetadataService; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/ReactiveRqueueMessageEnqueuerImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/ReactiveRqueueMessageEnqueuerImpl.java index 78769118..26be7dbd 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/ReactiveRqueueMessageEnqueuerImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/ReactiveRqueueMessageEnqueuerImpl.java @@ -21,6 +21,7 @@ import com.github.sonus21.rqueue.core.EndpointRegistry; import com.github.sonus21.rqueue.core.ReactiveRqueueMessageEnqueuer; import com.github.sonus21.rqueue.core.RqueueMessage; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.core.support.RqueueMessageUtils; import com.github.sonus21.rqueue.exception.DuplicateMessageException; @@ -41,7 +42,19 @@ public ReactiveRqueueMessageEnqueuerImpl( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, MessageHeaders messageHeaders) { - super(messageTemplate, messageConverter, messageHeaders); + this( + messageTemplate, + messageConverter, + messageHeaders, + new UuidV4RqueueMessageIdGenerator()); + } + + public ReactiveRqueueMessageEnqueuerImpl( + RqueueMessageTemplate messageTemplate, + MessageConverter messageConverter, + MessageHeaders messageHeaders, + RqueueMessageIdGenerator messageIdGenerator) { + super(messageTemplate, messageConverter, messageHeaders, messageIdGenerator); } @SuppressWarnings("unchecked") @@ -55,14 +68,16 @@ private Mono pushReactiveMessage( boolean isUnique, Function> monoConverter) { QueueDetail queueDetail = EndpointRegistry.get(queueName); - RqueueMessage rqueueMessage = builder.build( - messageConverter, - queueName, - messageId, - message, - retryCount, - delayInMilliSecs, - messageHeaders); + RqueueMessage rqueueMessage = + builder.build( + messageIdGenerator, + messageConverter, + queueName, + messageId, + message, + retryCount, + delayInMilliSecs, + messageHeaders); try { Mono storeResult = (Mono) storeMessageMetadata(rqueueMessage, delayInMilliSecs, true, isUnique); @@ -280,6 +295,7 @@ private interface MonoConverter { @FunctionalInterface private interface MessageBuilder { RqueueMessage build( + RqueueMessageIdGenerator messageIdGenerator, MessageConverter converter, String queueName, String messageId, diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueEndpointManagerImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueEndpointManagerImpl.java index 44197ad3..e2402447 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueEndpointManagerImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueEndpointManagerImpl.java @@ -18,6 +18,7 @@ import com.github.sonus21.rqueue.core.EndpointRegistry; import com.github.sonus21.rqueue.core.RqueueEndpointManager; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.dao.RqueueSystemConfigDao; import com.github.sonus21.rqueue.exception.QueueDoesNotExist; @@ -51,7 +52,19 @@ public RqueueEndpointManagerImpl( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, MessageHeaders messageHeaders) { - super(messageTemplate, messageConverter, messageHeaders); + this( + messageTemplate, + messageConverter, + messageHeaders, + new UuidV4RqueueMessageIdGenerator()); + } + + public RqueueEndpointManagerImpl( + RqueueMessageTemplate messageTemplate, + MessageConverter messageConverter, + MessageHeaders messageHeaders, + RqueueMessageIdGenerator messageIdGenerator) { + super(messageTemplate, messageConverter, messageHeaders, messageIdGenerator); } @Override diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImpl.java index c1fabab5..af0bf04a 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImpl.java @@ -25,6 +25,7 @@ import static com.github.sonus21.rqueue.utils.Validator.validateRetryCount; import com.github.sonus21.rqueue.core.RqueueMessageEnqueuer; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.utils.PriorityUtils; import java.util.Objects; @@ -39,7 +40,19 @@ public RqueueMessageEnqueuerImpl( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, MessageHeaders messageHeaders) { - super(messageTemplate, messageConverter, messageHeaders); + this( + messageTemplate, + messageConverter, + messageHeaders, + new UuidV4RqueueMessageIdGenerator()); + } + + public RqueueMessageEnqueuerImpl( + RqueueMessageTemplate messageTemplate, + MessageConverter messageConverter, + MessageHeaders messageHeaders, + RqueueMessageIdGenerator messageIdGenerator) { + super(messageTemplate, messageConverter, messageHeaders, messageIdGenerator); } private void validateBasic(String queue, Object message) { diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageManagerImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageManagerImpl.java index 9de4e7c6..914a3983 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageManagerImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageManagerImpl.java @@ -22,6 +22,7 @@ import com.github.sonus21.rqueue.common.RqueueLockManager; import com.github.sonus21.rqueue.core.EndpointRegistry; import com.github.sonus21.rqueue.core.RqueueMessage; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageManager; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.core.support.RqueueMessageUtils; @@ -50,7 +51,19 @@ public RqueueMessageManagerImpl( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, MessageHeaders messageHeaders) { - super(messageTemplate, messageConverter, messageHeaders); + this( + messageTemplate, + messageConverter, + messageHeaders, + new UuidV4RqueueMessageIdGenerator()); + } + + public RqueueMessageManagerImpl( + RqueueMessageTemplate messageTemplate, + MessageConverter messageConverter, + MessageHeaders messageHeaders, + RqueueMessageIdGenerator messageIdGenerator) { + super(messageTemplate, messageConverter, messageHeaders, messageIdGenerator); } @Override diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageTemplateImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageTemplateImpl.java index 2a2d5b03..33971cd6 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageTemplateImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageTemplateImpl.java @@ -129,6 +129,12 @@ public Long addMessage(String listName, RqueueMessage rqueueMessage) { return rpush(listName, rqueueMessage); } + @Override + public Long addMessageAtFront(String listName, RqueueMessage rqueueMessage) { + log.debug("AddMessageAtFront Queue: {}, Message: {}", listName, rqueueMessage); + return lpush(listName, rqueueMessage); + } + @Override public Mono addReactiveMessage(String listName, RqueueMessage rqueueMessage) { log.debug("AddReactiveMessage Queue: {}, Message: {}", listName, rqueueMessage); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/UuidV4RqueueMessageIdGenerator.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/UuidV4RqueueMessageIdGenerator.java new file mode 100644 index 00000000..9a164659 --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/UuidV4RqueueMessageIdGenerator.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.core.impl; + +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; +import java.util.UUID; + +public class UuidV4RqueueMessageIdGenerator implements RqueueMessageIdGenerator { + + @Override + public String generate() { + return UUID.randomUUID().toString(); + } +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtils.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtils.java index 967ad14f..2ea3a7f9 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtils.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtils.java @@ -19,10 +19,10 @@ import static com.github.sonus21.rqueue.utils.Constants.REDIS_KEY_SEPARATOR; import com.github.sonus21.rqueue.core.RqueueMessage; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.UUID; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.MessageConversionException; @@ -50,6 +50,7 @@ public static Object convertMessageToObject( } public static RqueueMessage buildPeriodicMessage( + RqueueMessageIdGenerator messageIdGenerator, MessageConverter converter, String queueName, String messageId, @@ -71,14 +72,15 @@ public static RqueueMessage buildPeriodicMessage( } else { throw new MessageConversionException("Message payload is neither String nor byte[]"); } - RqueueMessage rqueueMessage = RqueueMessage.builder() - .id(UUID.randomUUID().toString()) - .queueName(queueName) - .message(strMessage) - .processAt(processAt) - .retryCount(retryCount) - .period(period) - .build(); + RqueueMessage rqueueMessage = + RqueueMessage.builder() + .id(messageIdGenerator.generate()) + .queueName(queueName) + .message(strMessage) + .processAt(processAt) + .retryCount(retryCount) + .period(period) + .build(); if (messageId != null) { rqueueMessage.setId(messageId); } @@ -86,6 +88,7 @@ public static RqueueMessage buildPeriodicMessage( } public static RqueueMessage buildMessage( + RqueueMessageIdGenerator messageIdGenerator, MessageConverter converter, String queueName, String messageId, @@ -111,14 +114,15 @@ public static RqueueMessage buildMessage( } else { throw new MessageConversionException("Message payload is neither String nor byte[]"); } - RqueueMessage rqueueMessage = RqueueMessage.builder() - .retryCount(retryCount) - .queuedTime(queuedTime) - .id(UUID.randomUUID().toString()) - .queueName(queueName) - .message(strMessage) - .processAt(processAt) - .build(); + RqueueMessage rqueueMessage = + RqueueMessage.builder() + .retryCount(retryCount) + .queuedTime(queuedTime) + .id(messageIdGenerator.generate()) + .queueName(queueName) + .message(strMessage) + .processAt(processAt) + .build(); if (messageId != null) { rqueueMessage.setId(messageId); } @@ -126,21 +130,53 @@ public static RqueueMessage buildMessage( } public static List generateMessages( - MessageConverter converter, String queueName, int count) { - return generateMessages(converter, UUID.randomUUID().toString(), queueName, null, null, count); + RqueueMessageIdGenerator messageIdGenerator, + MessageConverter converter, + String queueName, + int count) { + return generateMessages( + messageIdGenerator, + converter, + messageIdGenerator.generate(), + queueName, + null, + null, + count); } - public static RqueueMessage generateMessage(MessageConverter converter, String queueName) { - return generateMessages(converter, UUID.randomUUID().toString(), queueName, null, null, 1) + public static RqueueMessage generateMessage( + RqueueMessageIdGenerator messageIdGenerator, + MessageConverter converter, + String queueName) { + return generateMessages( + messageIdGenerator, + converter, + messageIdGenerator.generate(), + queueName, + null, + null, + 1) .get(0); } public static List generateMessages( - MessageConverter converter, String queueName, long delay, int count) { - return generateMessages(converter, UUID.randomUUID().toString(), queueName, null, delay, count); + RqueueMessageIdGenerator messageIdGenerator, + MessageConverter converter, + String queueName, + long delay, + int count) { + return generateMessages( + messageIdGenerator, + converter, + messageIdGenerator.generate(), + queueName, + null, + delay, + count); } public static List generateMessages( + RqueueMessageIdGenerator messageIdGenerator, MessageConverter converter, Object object, String queueName, @@ -149,7 +185,16 @@ public static List generateMessages( int count) { List messages = new ArrayList<>(); for (int i = 0; i < count; i++) { - messages.add(buildMessage(converter, queueName, null, object, retryCount, delay, null)); + messages.add( + buildMessage( + messageIdGenerator, + converter, + queueName, + null, + object, + retryCount, + delay, + null)); } return messages; } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/DefaultRqueuePoller.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/DefaultRqueuePoller.java index b9c3806d..1c054d4e 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/DefaultRqueuePoller.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/DefaultRqueuePoller.java @@ -92,6 +92,7 @@ private void logNotAvailable() { void poll() { if (!hasAvailableThreads(queueDetail, queueThreadPool)) { + recordCapacityExhausted(queueDetail, queueThreadPool); logNotAvailable(); TimeoutUtils.sleepLog(pollingInterval, false); } else { diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/RqueueMessagePoller.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/RqueueMessagePoller.java index a64bd101..f2ead0f2 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/RqueueMessagePoller.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/RqueueMessagePoller.java @@ -103,6 +103,13 @@ protected boolean hasAvailableThreads(QueueDetail queueDetail, QueueThreadPool q return queueThreadPool.availableThreads() > 0; } + protected void recordCapacityExhausted(QueueDetail queueDetail, QueueThreadPool queueThreadPool) { + if (rqueueBeanProvider.getRqueueWorkerRegistry() != null) { + rqueueBeanProvider.getRqueueWorkerRegistry().recordQueueCapacityExhausted( + queueDetail, queueThreadPool); + } + } + protected int getBatchSize(QueueDetail queueDetail, QueueThreadPool queueThreadPool) { int batchSize = Math.min(queueDetail.getBatchSize(), queueThreadPool.availableThreads()); batchSize = Math.max(batchSize, Constants.MIN_BATCH_SIZE); @@ -132,6 +139,11 @@ private void pollAndExecute( List messages = getMessages(queueDetail, batchSize); log(Level.TRACE, "Queue: {} Fetched Msgs {}", null, queue, messages); int messageCount = CollectionUtils.isEmpty(messages) ? 0 : messages.size(); + if (rqueueBeanProvider.getRqueueWorkerRegistry() != null) { + rqueueBeanProvider + .getRqueueWorkerRegistry() + .recordQueuePoll(queueDetail, queueThreadPool, messageCount > 0); + } // free additional requested threads e.g 10 requested but only 5 messages are there queueThreadPool.release(batchSize - messageCount); if (messageCount > 0) { @@ -162,6 +174,7 @@ void poll(int index, String queue, QueueDetail queueDetail, QueueThreadPool queu return; } if (!acquired) { + recordCapacityExhausted(queueDetail, queueThreadPool); deactivate(index, queue, DeactivateType.SEMAPHORE_UNAVAILABLE); return; } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/db/QueueConfig.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/db/QueueConfig.java index d0f6377a..60b497cd 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/db/QueueConfig.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/db/QueueConfig.java @@ -173,4 +173,9 @@ public boolean isDeadLetterQueue(String name) { public boolean hasDeadLetterQueue() { return !CollectionUtils.isEmpty(deadLetterQueues); } + + @JsonIgnore + public boolean isUnlimitedRetry() { + return numRetry == Integer.MAX_VALUE; + } } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/enums/ActionType.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/enums/ActionType.java index 265a3ca1..649559bc 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/enums/ActionType.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/enums/ActionType.java @@ -17,6 +17,7 @@ package com.github.sonus21.rqueue.models.enums; public enum ActionType { + ENQUEUE, DELETE, NONE } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/enums/NavTab.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/enums/NavTab.java index d8ba4d43..e362235f 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/enums/NavTab.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/enums/NavTab.java @@ -24,6 +24,7 @@ @Getter public enum NavTab { QUEUES("Queues"), + WORKERS("Workers"), DEAD("Dead"), RUNNING("Running"), PENDING("Pending"), diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerInfo.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerInfo.java new file mode 100644 index 00000000..4340b7a3 --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerInfo.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.models.registry; + +import com.github.sonus21.rqueue.models.SerializableBase; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RqueueWorkerInfo extends SerializableBase { + private String workerId; + private String host; + private String pid; + private String version; + private long startedAt; + private long lastSeenAt; +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerPollerMetadata.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerPollerMetadata.java new file mode 100644 index 00000000..4304ffa1 --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerPollerMetadata.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.models.registry; + +import com.github.sonus21.rqueue.models.SerializableBase; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RqueueWorkerPollerMetadata extends SerializableBase { + + private static final long serialVersionUID = -8612115593908071754L; + + private String workerId; + private long lastPollAt; + private Long lastMessageAt; + private Long lastCapacityExhaustedAt; + private long capacityExhaustedCount; +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerPollerView.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerPollerView.java new file mode 100644 index 00000000..d38605f1 --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerPollerView.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.models.registry; + +import com.github.sonus21.rqueue.models.SerializableBase; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RqueueWorkerPollerView extends SerializableBase { + + private static final long serialVersionUID = 7432122245407288494L; + + private String queue; + private String workerId; + private String host; + private String pid; + private String status; + private long lastPollAt; + private String lastPollAge; + private Long lastMessageAt; + private String lastMessageAge; + private Long lastCapacityExhaustedAt; + private String lastCapacityExhaustedAge; + private long capacityExhaustedCount; +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerView.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerView.java new file mode 100644 index 00000000..f78aa036 --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/registry/RqueueWorkerView.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.models.registry; + +import com.github.sonus21.rqueue.models.SerializableBase; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RqueueWorkerView extends SerializableBase { + + private static final long serialVersionUID = 4777550203661409019L; + + private String workerId; + private String host; + private String pid; + private long lastPollAt; + private String lastPollAge; + private int activeQueues; + private int staleQueues; + private int recentCapacityExhaustedQueues; + + @Builder.Default + private List pollers = new ArrayList<>(); +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/request/MessageEnqueueRequest.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/request/MessageEnqueueRequest.java new file mode 100644 index 00000000..1b64780c --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/models/request/MessageEnqueueRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.models.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.sonus21.rqueue.models.SerializableBase; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = true) +public class MessageEnqueueRequest extends SerializableBase { + + @JsonProperty("queue") + @NotEmpty + private String queueName; + + @JsonProperty("message_id") + @NotEmpty + private String messageId; + + @JsonProperty("position") + private String position = "REAR"; +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/DateTimeUtils.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/DateTimeUtils.java index ad886809..4a23f17c 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/DateTimeUtils.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/DateTimeUtils.java @@ -22,16 +22,19 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Locale; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class DateTimeUtils { - private static final DateTimeFormatter dateTimeFormatter = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - private static final DateTimeFormatter dateTimeFormatterWithSecond = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm"); + private static final DateTimeFormatter dateTimeFormatterWithSecond = DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter readableDateTimeFormatter = + DateTimeFormatter.ofPattern("dd MMM, yyyy 'at' hh:mm a", Locale.ENGLISH); private static String hourString(long hour) { if (hour > 1) { @@ -125,6 +128,69 @@ public static String formatMilliToString(Long milli) { return zonedDateTime.format(dateTimeFormatter); } + public static String formatMilliToReadableString(Long milli) { + if (milli == null) { + return ""; + } + Instant instant = Instant.ofEpochMilli(milli); + ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()); + return zonedDateTime.format(readableDateTimeFormatter); + } + + public static String formatMilliToCompactDuration(Long milli) { + if (milli == null) { + return ""; + } + long millis = milli; + long absMillis = Math.abs(millis); + String prefix = millis < 0 ? "- " : ""; + if (absMillis < Constants.ONE_MILLI) { + return prefix + absMillis + " ms"; + } + if (absMillis < 2L * Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI) { + long seconds = absMillis / Constants.ONE_MILLI; + return prefix + seconds + " sec"; + } + if (absMillis < Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI + && absMillis % (Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI) == 0) { + long minutes = absMillis / (Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI); + return prefix + minutes + " min"; + } + if (absMillis < Constants.HOURS_IN_A_DAY + * Constants.MINUTES_IN_AN_HOUR + * Constants.SECONDS_IN_A_MINUTE + * Constants.ONE_MILLI + && absMillis + % (Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI) + == 0) { + long hours = + absMillis + / (Constants.MINUTES_IN_AN_HOUR + * Constants.SECONDS_IN_A_MINUTE + * Constants.ONE_MILLI); + return prefix + hours + " hr"; + } + long totalSeconds = absMillis / Constants.ONE_MILLI; + long hours = totalSeconds / (Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE); + long minutes = + (totalSeconds % (Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE)) + / Constants.SECONDS_IN_A_MINUTE; + long seconds = totalSeconds % Constants.SECONDS_IN_A_MINUTE; + if (hours > 0) { + if (minutes > 0) { + return prefix + hours + " hr " + minutes + " min"; + } + return prefix + hours + " hr"; + } + if (minutes > 0) { + if (seconds > 0) { + return prefix + minutes + " min " + seconds + " sec"; + } + return prefix + minutes + " min"; + } + return prefix + totalSeconds + " sec"; + } + public static LocalDate localDateFromMilli(long millis) { return Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate(); } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/QueueThreadPool.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/QueueThreadPool.java index f62f1468..6a99fce3 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/QueueThreadPool.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/QueueThreadPool.java @@ -26,7 +26,6 @@ @Slf4j public final class QueueThreadPool { - private final AsyncTaskExecutor taskExecutor; private final boolean defaultExecutor; private final Semaphore semaphore; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/condition/MissingRqueueMessageIdGenerator.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/condition/MissingRqueueMessageIdGenerator.java new file mode 100644 index 00000000..3700b125 --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/condition/MissingRqueueMessageIdGenerator.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.utils.condition; + +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class MissingRqueueMessageIdGenerator implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + if (!(context.getBeanFactory() instanceof ListableBeanFactory)) { + return true; + } + ListableBeanFactory beanFactory = (ListableBeanFactory) context.getBeanFactory(); + String[] beanNames = beanFactory.getBeanNamesForType(RqueueMessageIdGenerator.class, false, false); + return beanNames.length == 0; + } +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/DurationFunction.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/DurationFunction.java new file mode 100644 index 00000000..41b90bca --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/DurationFunction.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.utils.pebble; + +import com.github.sonus21.rqueue.utils.DateTimeUtils; +import io.pebbletemplates.pebble.extension.Function; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.pebbletemplates.pebble.template.PebbleTemplate; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class DurationFunction implements Function { + + public static final String FUNCTION_NAME = "duration"; + + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + Long milli = (Long) args.get("milli"); + return DateTimeUtils.formatMilliToCompactDuration(milli); + } + + @Override + public List getArgumentNames() { + return Collections.singletonList("milli"); + } +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/ReadableDateTimeFunction.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/ReadableDateTimeFunction.java new file mode 100644 index 00000000..0988de8f --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/ReadableDateTimeFunction.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.utils.pebble; + +import com.github.sonus21.rqueue.utils.DateTimeUtils; +import io.pebbletemplates.pebble.extension.Function; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.pebbletemplates.pebble.template.PebbleTemplate; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ReadableDateTimeFunction implements Function { + + public static final String FUNCTION_NAME = "readableTime"; + + @Override + public Object execute( + Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + Long milli = (Long) args.get("milli"); + return DateTimeUtils.formatMilliToReadableString(milli); + } + + @Override + public List getArgumentNames() { + return Collections.singletonList("milli"); + } +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/RqueuePebbleExtension.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/RqueuePebbleExtension.java index 2401aa7d..8fd7ad64 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/RqueuePebbleExtension.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/pebble/RqueuePebbleExtension.java @@ -28,6 +28,8 @@ public Map getFunctions() { Map map = new HashMap<>(); map.put(DeadLetterQueuesFunction.FUNCTION_NAME, new DeadLetterQueuesFunction()); map.put(DateTimeFunction.FUNCTION_NAME, new DateTimeFunction()); + map.put(DurationFunction.FUNCTION_NAME, new DurationFunction()); + map.put(ReadableDateTimeFunction.FUNCTION_NAME, new ReadableDateTimeFunction()); map.put(DefaultFunction.FUNCTION_NAME, new DefaultFunction()); return map; } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueRestController.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueRestController.java index 68f69110..23744b74 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueRestController.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueRestController.java @@ -24,6 +24,7 @@ import com.github.sonus21.rqueue.models.request.DataTypeRequest; import com.github.sonus21.rqueue.models.request.DateViewRequest; import com.github.sonus21.rqueue.models.request.MessageDeleteRequest; +import com.github.sonus21.rqueue.models.request.MessageEnqueueRequest; import com.github.sonus21.rqueue.models.request.MessageMoveRequest; import com.github.sonus21.rqueue.models.request.PauseUnpauseQueueRequest; import com.github.sonus21.rqueue.models.request.QueueExploreRequest; @@ -143,6 +144,17 @@ public Mono deleteMessage( return null; } + @PostMapping("enqueue-message") + @ResponseBody + public Mono enqueueMessage( + @RequestBody @Valid MessageEnqueueRequest request, ServerHttpResponse response) { + if (isEnabled(response)) { + return rqueueUtilityService.enqueueReactiveMessage( + request.getQueueName(), request.getMessageId(), request.getPosition()); + } + return null; + } + @PostMapping("delete-queue") @ResponseBody public Mono deleteQueue( diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueViewController.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueViewController.java index 70c5d99a..88370a91 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueViewController.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueViewController.java @@ -29,6 +29,7 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.reactive.result.view.View; import org.springframework.web.reactive.result.view.ViewResolver; @@ -67,10 +68,14 @@ public Mono index(Model model, ServerHttpRequest request, ServerHttpRespon } @GetMapping("queues") - public Mono queues(Model model, ServerHttpRequest request, ServerHttpResponse response) + public Mono queues( + Model model, + @RequestParam(name = "page", defaultValue = "1") int pageNumber, + ServerHttpRequest request, + ServerHttpResponse response) throws Exception { if (isEnabled(response)) { - rqueueViewControllerService.queues(model, xForwardedPrefix(request)); + rqueueViewControllerService.queues(model, xForwardedPrefix(request), pageNumber); return rqueueViewResolver.resolveViewName("queues", Locale.ENGLISH); } return null; @@ -90,6 +95,20 @@ public Mono queueDetail( return null; } + @GetMapping("workers") + public Mono workers( + Model model, + @RequestParam(name = "page", defaultValue = "1") int pageNumber, + ServerHttpRequest request, + ServerHttpResponse response) + throws Exception { + if (isEnabled(response)) { + rqueueViewControllerService.workers(model, xForwardedPrefix(request), pageNumber); + return rqueueViewResolver.resolveViewName("workers", Locale.ENGLISH); + } + return null; + } + @GetMapping("running") public Mono running(Model model, ServerHttpRequest request, ServerHttpResponse response) throws Exception { diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueRestController.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueRestController.java index dc084251..b9f8a4e0 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueRestController.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueRestController.java @@ -24,6 +24,7 @@ import com.github.sonus21.rqueue.models.request.DataTypeRequest; import com.github.sonus21.rqueue.models.request.DateViewRequest; import com.github.sonus21.rqueue.models.request.MessageDeleteRequest; +import com.github.sonus21.rqueue.models.request.MessageEnqueueRequest; import com.github.sonus21.rqueue.models.request.MessageMoveRequest; import com.github.sonus21.rqueue.models.request.PauseUnpauseQueueRequest; import com.github.sonus21.rqueue.models.request.QueueExploreRequest; @@ -141,6 +142,17 @@ public BooleanResponse deleteMessage( return null; } + @PostMapping("enqueue-message") + @ResponseBody + public BooleanResponse enqueueMessage( + @Valid @RequestBody MessageEnqueueRequest request, HttpServletResponse response) { + if (isEnable(response)) { + return rqueueUtilityService.enqueueMessage( + request.getQueueName(), request.getMessageId(), request.getPosition()); + } + return null; + } + @PostMapping("delete-queue") @ResponseBody public BaseResponse deleteQueue( diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueViewController.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueViewController.java index de4e43a3..4f495c8e 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueViewController.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueViewController.java @@ -29,6 +29,7 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; @@ -66,15 +67,33 @@ public View index(Model model, HttpServletRequest request, HttpServletResponse r } @GetMapping("queues") - public View queues(Model model, HttpServletRequest request, HttpServletResponse response) + public View queues( + Model model, + @RequestParam(name = "page", defaultValue = "1") int pageNumber, + HttpServletRequest request, + HttpServletResponse response) throws Exception { if (isEnable(response)) { - rqueueViewControllerService.queues(model, xForwardedPrefix(request)); + rqueueViewControllerService.queues(model, xForwardedPrefix(request), pageNumber); return rqueueViewResolver.resolveViewName("queues", Locale.ENGLISH); } return null; } + @GetMapping("workers") + public View workers( + Model model, + @RequestParam(name = "page", defaultValue = "1") int pageNumber, + HttpServletRequest request, + HttpServletResponse response) + throws Exception { + if (isEnable(response)) { + rqueueViewControllerService.workers(model, xForwardedPrefix(request), pageNumber); + return rqueueViewResolver.resolveViewName("workers", Locale.ENGLISH); + } + return null; + } + @GetMapping("queues/{queueName}") public View queueDetail( @PathVariable("queueName") String queueName, diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueQDetailService.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueQDetailService.java index 39e11540..580ff40d 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueQDetailService.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueQDetailService.java @@ -21,6 +21,7 @@ import com.github.sonus21.rqueue.models.enums.NavTab; import com.github.sonus21.rqueue.models.response.DataViewResponse; import com.github.sonus21.rqueue.models.response.RedisDataDetail; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -49,6 +50,8 @@ DataViewResponse viewData( List> getDeadLetterTasks(); + List getQueueWorkers(String queueName); + Mono getReactiveExplorePageData( String src, String name, DataType type, int pageNumber, int itemPerPage); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueUtilityService.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueUtilityService.java index f5bc62e3..0dcba99f 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueUtilityService.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueUtilityService.java @@ -31,6 +31,8 @@ public interface RqueueUtilityService { BooleanResponse deleteMessage(String queueName, String id); + BooleanResponse enqueueMessage(String queueName, String id, String position); + MessageMoveResponse moveMessage(MessageMoveRequest messageMoveRequest); BooleanResponse makeEmpty(String queueName, String dataName); @@ -43,6 +45,8 @@ public interface RqueueUtilityService { Mono deleteReactiveMessage(String queueName, String messageId); + Mono enqueueReactiveMessage(String queueName, String messageId, String position); + Mono getReactiveDataType(String name); Mono moveReactiveMessage(MessageMoveRequest request); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueViewControllerService.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueViewControllerService.java index 3cae1594..7fe2dcff 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueViewControllerService.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueViewControllerService.java @@ -22,7 +22,9 @@ public interface RqueueViewControllerService { void index(Model model, String forwardedFor); - void queues(Model model, String xForwardedPrefix); + void queues(Model model, String xForwardedPrefix, int pageNumber); + + void workers(Model model, String xForwardedPrefix, int pageNumber); void queueDetail(Model model, String xForwardedPrefix, String queueName); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueQDetailServiceImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueQDetailServiceImpl.java index 2f925a01..5249bdad 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueQDetailServiceImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueQDetailServiceImpl.java @@ -32,6 +32,7 @@ import com.github.sonus21.rqueue.models.enums.DataType; import com.github.sonus21.rqueue.models.enums.NavTab; import com.github.sonus21.rqueue.models.enums.TableColumnType; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView; import com.github.sonus21.rqueue.models.response.Action; import com.github.sonus21.rqueue.models.response.DataViewResponse; import com.github.sonus21.rqueue.models.response.RedisDataDetail; @@ -46,6 +47,7 @@ import com.github.sonus21.rqueue.web.service.RqueueMessageMetadataService; import com.github.sonus21.rqueue.web.service.RqueueQDetailService; import com.github.sonus21.rqueue.web.service.RqueueSystemManagerService; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -71,6 +73,7 @@ public class RqueueQDetailServiceImpl implements RqueueQDetailService { private final RqueueSystemManagerService rqueueSystemManagerService; private final RqueueMessageMetadataService rqueueMessageMetadataService; private final RqueueConfig rqueueConfig; + private final RqueueWorkerRegistry rqueueWorkerRegistry; @Autowired public RqueueQDetailServiceImpl( @@ -78,12 +81,14 @@ public RqueueQDetailServiceImpl( RqueueMessageTemplate rqueueMessageTemplate, RqueueSystemManagerService rqueueSystemManagerService, RqueueMessageMetadataService rqueueMessageMetadataService, - RqueueConfig rqueueConfig) { + RqueueConfig rqueueConfig, + RqueueWorkerRegistry rqueueWorkerRegistry) { this.stringRqueueRedisTemplate = stringRqueueRedisTemplate; this.rqueueMessageTemplate = rqueueMessageTemplate; this.rqueueSystemManagerService = rqueueSystemManagerService; this.rqueueMessageMetadataService = rqueueMessageMetadataService; this.rqueueConfig = rqueueConfig; + this.rqueueWorkerRegistry = rqueueWorkerRegistry; } @Override @@ -374,9 +379,9 @@ private void setHeadersIfRequired( headers.add("Time Left"); } if (deadLetterQueue) { - headers.add("AddedOn"); + headers.add("Added On"); } else if (completedQueue) { - headers.add("CompletedOn"); + headers.add("Completed On"); } else { headers.add("Action"); } @@ -504,6 +509,11 @@ public List> getDeadLetterTasks() { return rows; } + @Override + public List getQueueWorkers(String queueName) { + return rqueueWorkerRegistry.getQueueWorkers(queueName); + } + @Override public Mono getReactiveExplorePageData( String src, String name, DataType type, int pageNumber, int itemPerPage) { @@ -587,7 +597,12 @@ public TableRow row(RqueueMessage rqueueMessage, boolean deleted, Double score) } if (!completionQueue) { if (!deleted) { - row.addColumn(new TableColumn(TableColumnType.ACTION, ActionType.DELETE)); + row.addColumn( + new TableColumn( + TableColumnType.ACTION, + scheduledQueue && !rqueueMessage.isPeriodic() + ? ActionType.ENQUEUE + : ActionType.DELETE)); } else { row.addColumn(new TableColumn(Constants.BLANK)); } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueUtilityServiceImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueUtilityServiceImpl.java index 6ddf69a4..e1cd7641 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueUtilityServiceImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueUtilityServiceImpl.java @@ -21,6 +21,7 @@ import com.github.sonus21.rqueue.config.RqueueConfig; import com.github.sonus21.rqueue.config.RqueueWebConfig; import com.github.sonus21.rqueue.core.RqueueInternalPubSubChannel; +import com.github.sonus21.rqueue.core.RqueueMessage; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.core.impl.MessageSweeper; import com.github.sonus21.rqueue.core.impl.MessageSweeper.MessageDeleteRequest; @@ -29,6 +30,7 @@ import com.github.sonus21.rqueue.exception.UnknownSwitchCase; import com.github.sonus21.rqueue.models.MessageMoveResult; import com.github.sonus21.rqueue.models.Pair; +import com.github.sonus21.rqueue.models.db.MessageMetadata; import com.github.sonus21.rqueue.models.db.QueueConfig; import com.github.sonus21.rqueue.models.enums.AggregationType; import com.github.sonus21.rqueue.models.enums.DataType; @@ -100,6 +102,44 @@ public BooleanResponse deleteMessage(String queueName, String id) { return booleanResponse; } + @Override + public BooleanResponse enqueueMessage(String queueName, String id, String position) { + BooleanResponse booleanResponse = new BooleanResponse(); + QueueConfig queueConfig = rqueueSystemConfigDao.getConfigByName(queueName, true); + if (queueConfig == null) { + booleanResponse.setCode(1); + booleanResponse.setMessage("Queue config not found!"); + return booleanResponse; + } + if (StringUtils.isEmpty(queueConfig.getScheduledQueueName())) { + booleanResponse.setCode(1); + booleanResponse.setMessage("Scheduled queue not found!"); + return booleanResponse; + } + MessageMetadata messageMetadata = messageMetadataService.getByMessageId(queueName, id); + if (messageMetadata == null || messageMetadata.getRqueueMessage() == null) { + booleanResponse.setCode(1); + booleanResponse.setMessage("Message not found!"); + return booleanResponse; + } + RqueueMessage rqueueMessage = messageMetadata.getRqueueMessage(); + Long removed = + rqueueMessageTemplate.removeElementFromZset( + queueConfig.getScheduledQueueName(), rqueueMessage); + if (removed == null || removed == 0) { + booleanResponse.setCode(1); + booleanResponse.setMessage("Message is not available in the scheduled queue."); + return booleanResponse; + } + if ("FRONT".equalsIgnoreCase(position)) { + rqueueMessageTemplate.addMessageAtFront(queueConfig.getQueueName(), rqueueMessage); + } else { + rqueueMessageTemplate.addMessage(queueConfig.getQueueName(), rqueueMessage); + } + booleanResponse.setValue(true); + return booleanResponse; + } + private MessageMoveResponse moveMessageToZset(MessageMoveRequest messageMoveRequest) { String src = messageMoveRequest.getSrc(); String dst = messageMoveRequest.getDst(); @@ -221,6 +261,12 @@ public Mono deleteReactiveMessage(String queueName, String mess return Mono.just(deleteMessage(queueName, messageId)); } + @Override + public Mono enqueueReactiveMessage( + String queueName, String messageId, String position) { + return Mono.just(enqueueMessage(queueName, messageId, position)); + } + @Override public Mono getReactiveDataType(String name) { return Mono.just(getDataType(name)); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueViewControllerServiceImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueViewControllerServiceImpl.java index 50bf146a..b72e1a4b 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueViewControllerServiceImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueViewControllerServiceImpl.java @@ -24,6 +24,8 @@ import com.github.sonus21.rqueue.models.enums.ChartDataType; import com.github.sonus21.rqueue.models.enums.DataType; import com.github.sonus21.rqueue.models.enums.NavTab; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerView; import com.github.sonus21.rqueue.models.response.RedisDataDetail; import com.github.sonus21.rqueue.utils.DateTimeUtils; import com.github.sonus21.rqueue.web.service.RqueueQDetailService; @@ -32,8 +34,12 @@ import com.github.sonus21.rqueue.web.service.RqueueViewControllerService; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.ui.Model; @@ -89,17 +95,110 @@ public void index(Model model, String xForwardedPrefix) { model.addAttribute("typeSelectors", ChartDataType.getActiveCharts()); } + private List getPage(List items, int pageNumber, int pageSize) { + if (items.isEmpty()) { + return items; + } + int page = Math.max(1, pageNumber); + int start = (page - 1) * pageSize; + if (start >= items.size()) { + start = ((items.size() - 1) / pageSize) * pageSize; + } + int end = Math.min(start + pageSize, items.size()); + return items.subList(start, end); + } + @Override - public void queues(Model model, String xForwardedPrefix) { + public void queues(Model model, String xForwardedPrefix, int pageNumber) { addBasicDetails(model, xForwardedPrefix); addNavData(model, NavTab.QUEUES); model.addAttribute("title", "Queues"); - List queueConfigs = rqueueSystemManagerService.getSortedQueueConfigs(); + List allQueueConfigs = rqueueSystemManagerService.getSortedQueueConfigs(); + int pageSize = Math.max(1, rqueueWebConfig.getQueuePageSize()); + int totalPages = Math.max(1, (allQueueConfigs.size() + pageSize - 1) / pageSize); + int currentPage = Math.max(1, Math.min(pageNumber, totalPages)); + List queueConfigs = getPage(allQueueConfigs, currentPage, pageSize); List>>> queueNameConfigs = new ArrayList<>( rqueueQDetailService.getQueueDataStructureDetails(queueConfigs).entrySet()); queueNameConfigs.sort(Entry.comparingByKey()); model.addAttribute("queues", queueConfigs); model.addAttribute("queueConfigs", queueNameConfigs); + model.addAttribute("currentPage", currentPage); + model.addAttribute("totalPages", totalPages); + model.addAttribute("hasPreviousPage", currentPage > 1); + model.addAttribute("hasNextPage", currentPage < totalPages); + model.addAttribute("previousPage", Math.max(1, currentPage - 1)); + model.addAttribute("nextPage", Math.min(totalPages, currentPage + 1)); + model.addAttribute("queuePageSize", pageSize); + model.addAttribute("totalQueueCount", allQueueConfigs.size()); + } + + @Override + public void workers(Model model, String xForwardedPrefix, int pageNumber) { + addBasicDetails(model, xForwardedPrefix); + addNavData(model, NavTab.WORKERS); + model.addAttribute("title", "Workers"); + List queueConfigs = rqueueSystemManagerService.getSortedQueueConfigs(); + Map workerIdToView = new LinkedHashMap<>(); + for (QueueConfig queueConfig : queueConfigs) { + List queueWorkers = + rqueueQDetailService.getQueueWorkers(queueConfig.getName()); + for (RqueueWorkerPollerView queueWorker : queueWorkers) { + RqueueWorkerView workerView = + workerIdToView.getOrDefault( + queueWorker.getWorkerId(), + RqueueWorkerView.builder() + .workerId(queueWorker.getWorkerId()) + .host(queueWorker.getHost()) + .pid(queueWorker.getPid()) + .lastPollAt(queueWorker.getLastPollAt()) + .lastPollAge(queueWorker.getLastPollAge()) + .build()); + workerView.setHost(queueWorker.getHost()); + workerView.setPid(queueWorker.getPid()); + if (queueWorker.getLastPollAt() > workerView.getLastPollAt()) { + workerView.setLastPollAt(queueWorker.getLastPollAt()); + workerView.setLastPollAge(queueWorker.getLastPollAge()); + } + if ("ACTIVE".equals(queueWorker.getStatus())) { + workerView.setActiveQueues(workerView.getActiveQueues() + 1); + } else { + workerView.setStaleQueues(workerView.getStaleQueues() + 1); + } + workerView.getPollers().add(queueWorker); + workerIdToView.put(queueWorker.getWorkerId(), workerView); + } + } + List allWorkers = new ArrayList<>(workerIdToView.values()); + allWorkers.forEach( + e -> { + e.getPollers().sort(Comparator.comparing(RqueueWorkerPollerView::getQueue)); + int recentCapacityExhaustedQueues = 0; + long recentThreshold = + 2L * rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); + for (RqueueWorkerPollerView poller : e.getPollers()) { + Long lastCapacityExhaustedAt = poller.getLastCapacityExhaustedAt(); + if (lastCapacityExhaustedAt != null + && System.currentTimeMillis() - lastCapacityExhaustedAt <= recentThreshold) { + recentCapacityExhaustedQueues += 1; + } + } + e.setRecentCapacityExhaustedQueues(recentCapacityExhaustedQueues); + }); + allWorkers.sort(Comparator.comparingLong(RqueueWorkerView::getLastPollAt).reversed()); + int pageSize = Math.max(1, rqueueWebConfig.getWorkerPageSize()); + int totalPages = Math.max(1, (allWorkers.size() + pageSize - 1) / pageSize); + int currentPage = Math.max(1, Math.min(pageNumber, totalPages)); + List workers = getPage(allWorkers, currentPage, pageSize); + model.addAttribute("workers", workers); + model.addAttribute("currentPage", currentPage); + model.addAttribute("totalPages", totalPages); + model.addAttribute("hasPreviousPage", currentPage > 1); + model.addAttribute("hasNextPage", currentPage < totalPages); + model.addAttribute("previousPage", Math.max(1, currentPage - 1)); + model.addAttribute("nextPage", Math.min(totalPages, currentPage + 1)); + model.addAttribute("workerPageSize", pageSize); + model.addAttribute("totalWorkerCount", allWorkers.size()); } @Override @@ -108,6 +207,7 @@ public void queueDetail(Model model, String xForwardedPrefix, String queueName) List queueActions = rqueueQDetailService.getNavTabs(queueConfig); List> queueRedisDataDetail = rqueueQDetailService.getQueueDataStructureDetail(queueConfig); + List queueWorkers = rqueueQDetailService.getQueueWorkers(queueName); addBasicDetails(model, xForwardedPrefix); addNavData(model, NavTab.QUEUES); model.addAttribute("title", "Queue: " + queueName); @@ -119,6 +219,24 @@ public void queueDetail(Model model, String xForwardedPrefix, String queueName) model.addAttribute("queueActions", queueActions); model.addAttribute("queueRedisDataDetails", queueRedisDataDetail); model.addAttribute("config", queueConfig); + model.addAttribute("workerRegistryEnabled", rqueueConfig.isWorkerRegistryEnabled()); + model.addAttribute("queueWorkers", queueWorkers); + model.addAttribute( + "activeQueueWorkers", + queueWorkers.stream().filter(e -> "ACTIVE".equals(e.getStatus())).count()); + model.addAttribute( + "staleQueueWorkers", + queueWorkers.stream().filter(e -> "STALE".equals(e.getStatus())).count()); + long recentThreshold = 2L * rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); + model.addAttribute( + "queueWorkerRecentCapacityExhausted", + queueWorkers.stream() + .filter( + e -> + e.getLastCapacityExhaustedAt() != null + && System.currentTimeMillis() - e.getLastCapacityExhaustedAt() + <= recentThreshold) + .count()); } @Override diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistry.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistry.java new file mode 100644 index 00000000..e59dab26 --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistry.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.worker; + +import com.github.sonus21.rqueue.listener.QueueDetail; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView; +import com.github.sonus21.rqueue.utils.QueueThreadPool; +import java.util.List; + +public interface RqueueWorkerRegistry { + + void recordQueuePoll(QueueDetail queueDetail, QueueThreadPool queueThreadPool, boolean messageReceived); + + void recordQueueCapacityExhausted(QueueDetail queueDetail, QueueThreadPool queueThreadPool); + + List getQueueWorkers(String queueName); +} diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImpl.java new file mode 100644 index 00000000..0fc9b4db --- /dev/null +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImpl.java @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.worker; + +import com.github.sonus21.rqueue.common.RqueueRedisTemplate; +import com.github.sonus21.rqueue.config.RqueueConfig; +import com.github.sonus21.rqueue.core.EndpointRegistry; +import com.github.sonus21.rqueue.listener.QueueDetail; +import com.github.sonus21.rqueue.models.event.RqueueBootstrapEvent; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerInfo; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerMetadata; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView; +import com.github.sonus21.rqueue.utils.DateTimeUtils; +import com.github.sonus21.rqueue.utils.QueueThreadPool; +import com.github.sonus21.rqueue.utils.SerializationUtils; +import com.github.sonus21.rqueue.utils.StringUtils; +import java.lang.management.ManagementFactory; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationListener; +import org.springframework.util.CollectionUtils; +import tools.jackson.databind.ObjectMapper; + +@Slf4j +public class RqueueWorkerRegistryImpl + implements RqueueWorkerRegistry, ApplicationListener { + private final ObjectMapper objectMapper = SerializationUtils.createObjectMapper(); + private final RqueueConfig rqueueConfig; + private final RqueueRedisTemplate workerTemplate; + private final RqueueRedisTemplate stringTemplate; + private final String workerId; + private final String host; + private final String pid; + private final long startedAt; + private final Map lastMessageAtByQueue = new ConcurrentHashMap<>(); + private final Map lastPollAtByQueue = new ConcurrentHashMap<>(); + private final Map lastQueueHeartbeatAt = new ConcurrentHashMap<>(); + private final Map lastQueueTtlRefreshAt = new ConcurrentHashMap<>(); + private final Map lastCapacityExhaustedAtByQueue = new ConcurrentHashMap<>(); + private final Map capacityExhaustedCountByQueue = new ConcurrentHashMap<>(); + private volatile long lastWorkerHeartbeatAt; + + public RqueueWorkerRegistryImpl(RqueueConfig rqueueConfig) { + this.rqueueConfig = rqueueConfig; + this.workerTemplate = new RqueueRedisTemplate<>(rqueueConfig.getConnectionFactory()); + this.stringTemplate = new RqueueRedisTemplate<>(rqueueConfig.getConnectionFactory()); + this.workerId = RqueueConfig.getBrokerId(); + this.host = getHostName(); + this.pid = getPid(); + this.startedAt = System.currentTimeMillis(); + } + + @Override + public void recordQueuePoll( + QueueDetail queueDetail, QueueThreadPool queueThreadPool, boolean messageReceived) { + if (!rqueueConfig.isWorkerRegistryEnabled()) { + return; + } + String registryQueueName = registryQueueName(queueDetail); + long now = System.currentTimeMillis(); + if (messageReceived) { + lastMessageAtByQueue.put(registryQueueName, now); + } + lastPollAtByQueue.put(registryQueueName, now); + refreshWorkerInfoIfRequired(now); + if (!queueHeartbeatRequired(registryQueueName, now)) { + return; + } + RqueueWorkerPollerMetadata metadata = buildMetadata(registryQueueName, queueThreadPool); + try { + stringTemplate.putHashValue( + rqueueConfig.getWorkerRegistryQueueKey(registryQueueName), + workerId, + objectMapper.writeValueAsString(metadata)); + refreshQueueTtlIfRequired(registryQueueName, now); + lastQueueHeartbeatAt.put(registryQueueName, now); + } catch (Exception e) { + log.warn("Worker registry serialization failed for queue {}", registryQueueName, e); + } + } + + @Override + public void recordQueueCapacityExhausted(QueueDetail queueDetail, QueueThreadPool queueThreadPool) { + if (!rqueueConfig.isWorkerRegistryEnabled()) { + return; + } + String registryQueueName = registryQueueName(queueDetail); + long now = System.currentTimeMillis(); + refreshWorkerInfoIfRequired(now); + lastCapacityExhaustedAtByQueue.put(registryQueueName, now); + capacityExhaustedCountByQueue.compute( + registryQueueName, + (key, count) -> { + if (count == null) { + return 1L; + } + if (count == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + return count + 1L; + }); + if (!queueHeartbeatRequired(registryQueueName, now)) { + return; + } + RqueueWorkerPollerMetadata metadata = buildMetadata(registryQueueName, queueThreadPool); + try { + stringTemplate.putHashValue( + rqueueConfig.getWorkerRegistryQueueKey(registryQueueName), + workerId, + objectMapper.writeValueAsString(metadata)); + refreshQueueTtlIfRequired(registryQueueName, now); + lastQueueHeartbeatAt.put(registryQueueName, now); + } catch (Exception e) { + log.warn("Worker registry serialization failed for queue {}", registryQueueName, e); + } + } + + @Override + public List getQueueWorkers(String queueName) { + if (!rqueueConfig.isWorkerRegistryEnabled()) { + return Collections.emptyList(); + } + String queueKey = rqueueConfig.getWorkerRegistryQueueKey(queueName); + Map rawEntries = stringTemplate.getHashEntries(queueKey); + if (CollectionUtils.isEmpty(rawEntries)) { + return Collections.emptyList(); + } + long now = System.currentTimeMillis(); + long staleAfter = 2 * rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); + Map metadataByWorkerId = new LinkedHashMap<>(); + List toDelete = new ArrayList<>(); + for (Map.Entry entry : rawEntries.entrySet()) { + try { + RqueueWorkerPollerMetadata metadata = + objectMapper.readValue(entry.getValue(), RqueueWorkerPollerMetadata.class); + if (metadata == null || metadata.getWorkerId() == null) { + toDelete.add(entry.getKey()); + continue; + } + // Lazy cleanup for entries that are far older than the queue hash retention window. + if (now - metadata.getLastPollAt() > rqueueConfig.getWorkerRegistryQueueTtl().toMillis()) { + toDelete.add(entry.getKey()); + continue; + } + metadataByWorkerId.put(entry.getKey(), metadata); + } catch (Exception e) { + log.warn("Worker registry deserialization failed for queue {}", queueName, e); + toDelete.add(entry.getKey()); + } + } + if (!toDelete.isEmpty()) { + stringTemplate.deleteHashValues(queueKey, toDelete.toArray(new String[0])); + } + if (metadataByWorkerId.isEmpty()) { + return Collections.emptyList(); + } + Map workerInfoById = getWorkerInfo(metadataByWorkerId.keySet()); + List rows = new ArrayList<>(); + for (Map.Entry entry : metadataByWorkerId.entrySet()) { + String workerId = entry.getKey(); + RqueueWorkerPollerMetadata metadata = entry.getValue(); + RqueueWorkerInfo workerInfo = workerInfoById.get(workerId); + long lastActivityAt = Math.max(metadata.getLastPollAt(), + metadata.getLastCapacityExhaustedAt() == null ? 0 : metadata.getLastCapacityExhaustedAt()); + boolean stale = now - lastActivityAt > staleAfter || workerInfo == null; + rows.add( + RqueueWorkerPollerView.builder() + .queue(queueName) + .workerId(workerId) + .host(workerInfo == null ? "unknown" : workerInfo.getHost()) + .pid(workerInfo == null ? "" : workerInfo.getPid()) + .status(stale ? "STALE" : "ACTIVE") + .lastPollAt(metadata.getLastPollAt()) + .lastPollAge(formatAge(now, metadata.getLastPollAt() == 0 ? null : metadata.getLastPollAt())) + .lastMessageAt(metadata.getLastMessageAt()) + .lastMessageAge(formatAge(now, metadata.getLastMessageAt())) + .lastCapacityExhaustedAt(metadata.getLastCapacityExhaustedAt()) + .lastCapacityExhaustedAge(formatAge(now, metadata.getLastCapacityExhaustedAt())) + .capacityExhaustedCount(metadata.getCapacityExhaustedCount()) + .build()); + } + rows.sort(Comparator.comparingLong(RqueueWorkerPollerView::getLastPollAt).reversed()); + return rows; + } + + @Override + public void onApplicationEvent(RqueueBootstrapEvent event) { + if (!rqueueConfig.isWorkerRegistryEnabled()) { + return; + } + if (event.isStartup()) { + refreshWorkerInfo(System.currentTimeMillis()); + } else if (event.isShutdown()) { + cleanup(); + } + } + + private void refreshWorkerInfoIfRequired(long now) { + if (now - lastWorkerHeartbeatAt < rqueueConfig.getWorkerRegistryWorkerHeartbeatInterval().toMillis()) { + return; + } + refreshWorkerInfo(now); + } + + private void refreshWorkerInfo(long now) { + RqueueWorkerInfo workerInfo = + RqueueWorkerInfo.builder() + .workerId(workerId) + .host(host) + .pid(pid) + .version(rqueueConfig.getLibVersion()) + .startedAt(startedAt) + .lastSeenAt(now) + .build(); + workerTemplate.set( + rqueueConfig.getWorkerRegistryKey(workerId), + workerInfo, + rqueueConfig.getWorkerRegistryWorkerTtl()); + lastWorkerHeartbeatAt = now; + } + + private boolean queueHeartbeatRequired(String queueName, long now) { + Long lastHeartbeat = lastQueueHeartbeatAt.get(queueName); + if (lastHeartbeat == null) { + return true; + } + return now - lastHeartbeat >= rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); + } + + private void refreshQueueTtlIfRequired(String queueName, long now) { + Duration ttl = rqueueConfig.getWorkerRegistryQueueTtl(); + long refreshIntervalInMillis = Math.max(1000L, ttl.toMillis() / 2); + Long lastRefreshAt = lastQueueTtlRefreshAt.get(queueName); + if (lastRefreshAt != null && now - lastRefreshAt < refreshIntervalInMillis) { + return; + } + stringTemplate.expire(rqueueConfig.getWorkerRegistryQueueKey(queueName), ttl); + lastQueueTtlRefreshAt.put(queueName, now); + } + + private void cleanup() { + workerTemplate.delete(rqueueConfig.getWorkerRegistryKey(workerId)); + for (QueueDetail queueDetail : EndpointRegistry.getActiveQueueDetails()) { + stringTemplate.deleteHashValues( + rqueueConfig.getWorkerRegistryQueueKey(registryQueueName(queueDetail)), workerId); + } + lastMessageAtByQueue.clear(); + lastPollAtByQueue.clear(); + lastQueueHeartbeatAt.clear(); + lastQueueTtlRefreshAt.clear(); + lastCapacityExhaustedAtByQueue.clear(); + capacityExhaustedCountByQueue.clear(); + lastWorkerHeartbeatAt = 0L; + } + + private RqueueWorkerPollerMetadata buildMetadata( + String registryQueueName, QueueThreadPool queueThreadPool) { + return RqueueWorkerPollerMetadata.builder() + .workerId(workerId) + .lastPollAt(lastPollAtByQueue.getOrDefault(registryQueueName, 0L)) + .lastMessageAt(lastMessageAtByQueue.get(registryQueueName)) + .lastCapacityExhaustedAt(lastCapacityExhaustedAtByQueue.get(registryQueueName)) + .capacityExhaustedCount(capacityExhaustedCountByQueue.getOrDefault(registryQueueName, 0L)) + .build(); + } + + private Map getWorkerInfo(Collection workerIds) { + List keys = new ArrayList<>(workerIds.size()); + for (String workerId : workerIds) { + keys.add(rqueueConfig.getWorkerRegistryKey(workerId)); + } + List workerInfos = workerTemplate.mget(keys); + if (CollectionUtils.isEmpty(workerInfos)) { + return Collections.emptyMap(); + } + Map workerInfoById = new LinkedHashMap<>(); + for (RqueueWorkerInfo workerInfo : workerInfos) { + if (workerInfo != null && workerInfo.getWorkerId() != null) { + workerInfoById.put(workerInfo.getWorkerId(), workerInfo); + } + } + return workerInfoById; + } + + private static String formatAge(long now, Long time) { + if (time == null || time == 0) { + return ""; + } + return DateTimeUtils.milliToHumanRepresentation(now - time); + } + + private static String registryQueueName(QueueDetail queueDetail) { + if (queueDetail.isSystemGenerated() && !StringUtils.isEmpty(queueDetail.getPriorityGroup())) { + return queueDetail.getPriorityGroup(); + } + return queueDetail.getName(); + } + + private static String getPid() { + String runtimeName = ManagementFactory.getRuntimeMXBean().getName(); + int index = runtimeName.indexOf('@'); + if (index == -1) { + return runtimeName; + } + return runtimeName.substring(0, index); + } + + private static String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + return "unknown"; + } + } +} diff --git a/rqueue-core/src/main/resources/public/rqueue/css/rqueue.css b/rqueue-core/src/main/resources/public/rqueue/css/rqueue.css index bed3ef69..60af65a5 100644 --- a/rqueue-core/src/main/resources/public/rqueue/css/rqueue.css +++ b/rqueue-core/src/main/resources/public/rqueue/css/rqueue.css @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2025 Sonu Kumar + * Copyright (c) 2020-2026 Sonu Kumar * * Licensed under the Apache License, Version 2.0 (the "License"); * You may not use this file except in compliance with the License. @@ -18,6 +18,872 @@ body { color: #444444; } +:root { + --rq-accent: #2a9bb1; + --rq-accent-soft: #dff3f7; + --rq-surface: #ffffff; + --rq-surface-muted: #f5f8f1; + --rq-border: #d8e2d0; + --rq-text-strong: #25301d; + --rq-text-soft: #5f6955; + --rq-shadow: 0 22px 60px rgba(43, 58, 29, 0.16); +} + +.worker-dashboard { + padding-bottom: 40px; +} + +.worker-hero { + display: grid; + grid-template-columns: minmax(0, 1.8fr) minmax(260px, 0.9fr); + gap: 28px; + align-items: end; + margin-bottom: 28px; + padding: 32px 34px; + border: 1px solid rgba(148, 192, 69, 0.18); + border-radius: 28px; + background: + radial-gradient(circle at top left, rgba(148, 192, 69, 0.17), transparent 34%), + radial-gradient(circle at bottom right, rgba(42, 155, 177, 0.18), transparent 30%), + linear-gradient(145deg, #fbfdf7, #f2f7f7 58%, #ffffff); + box-shadow: 0 28px 60px rgba(43, 58, 29, 0.12); +} + +.worker-hero-kicker { + display: inline-block; + margin-bottom: 10px; + color: var(--rq-accent); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.22em; + text-transform: uppercase; +} + +.worker-hero-title { + margin: 0; + color: var(--rq-text-strong); + font-size: 40px; + line-height: 1.05; + font-weight: 700; +} + +.worker-hero-subtitle { + max-width: 700px; + margin: 14px 0 0; + color: var(--rq-text-soft); + font-size: 16px; + line-height: 1.7; +} + +.worker-hero-meta { + display: grid; + gap: 16px; +} + +.worker-hero-stat { + padding: 18px 20px; + border: 1px solid rgba(148, 192, 69, 0.18); + border-radius: 20px; + background: rgba(255, 255, 255, 0.88); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7); +} + +.worker-hero-stat-label { + display: block; + color: #6e7a62; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.worker-hero-stat-value { + display: block; + margin-top: 8px; + color: var(--rq-text-strong); + font-size: 30px; + line-height: 1; +} + +.worker-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + margin-bottom: 20px; +} + +.worker-toolbar-bottom { + margin-top: 24px; +} + +.worker-toolbar-note { + color: var(--rq-text-soft); + font-size: 14px; +} + +.worker-pagination { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.worker-page-btn, +.worker-page-btn:hover { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 104px; + padding: 10px 18px; + border: 1px solid rgba(42, 155, 177, 0.24); + border-radius: 999px; + background: #ffffff; + color: var(--rq-text-strong); + font-size: 13px; + font-weight: 600; + text-decoration: none; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.worker-page-btn:hover { + transform: translateY(-1px); + box-shadow: 0 10px 24px rgba(42, 155, 177, 0.12); +} + +.worker-page-btn-primary, +.worker-page-btn-primary:hover { + background: linear-gradient(135deg, #2a9bb1, #4db9ca); + border-color: transparent; + color: #ffffff; +} + +.worker-page-pill { + display: inline-flex; + align-items: center; + min-height: 42px; + padding: 0 18px; + border-radius: 999px; + background: rgba(148, 192, 69, 0.12); + color: var(--rq-text-strong); + font-size: 13px; + font-weight: 700; +} + +.worker-empty-state { + padding: 44px 32px; + border: 1px dashed rgba(148, 192, 69, 0.4); + border-radius: 24px; + background: linear-gradient(145deg, #fafcf6, #f4f8ee); + text-align: center; +} + +.worker-empty-state h2 { + margin: 0 0 10px; + color: var(--rq-text-strong); + font-size: 28px; +} + +.worker-empty-state p { + margin: 0; + color: var(--rq-text-soft); +} + +.worker-list { + display: grid; + gap: 18px; +} + +.worker-panel { + border: 1px solid rgba(148, 192, 69, 0.16); + border-radius: 26px; + background: + radial-gradient(circle at top right, rgba(42, 155, 177, 0.12), transparent 28%), + linear-gradient(180deg, rgba(248, 251, 242, 0.92), #ffffff 30%); + box-shadow: 0 18px 36px rgba(43, 58, 29, 0.09); + overflow: hidden; +} + +.worker-panel[open] { + box-shadow: 0 24px 46px rgba(43, 58, 29, 0.14); +} + +.worker-panel-summary { + position: relative; + display: grid; + grid-template-columns: minmax(0, 1.4fr) auto 20px; + gap: 20px; + align-items: center; + padding: 24px 26px; + cursor: pointer; + list-style: none; +} + +.worker-panel-summary::-webkit-details-marker { + display: none; +} + +.worker-panel-title-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.worker-id-pill { + display: inline-flex; + align-items: center; + height: 30px; + padding: 0 12px; + border-radius: 999px; + background: rgba(148, 192, 69, 0.16); + color: #5f6d44; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.worker-panel-title { + margin: 0; + color: var(--rq-text-strong); + font-size: 22px; + line-height: 1.25; + word-break: break-word; +} + +.worker-panel-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 14px; +} + +.worker-meta-chip { + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 0 14px; + border: 1px solid rgba(216, 226, 208, 0.9); + border-radius: 999px; + background: rgba(255, 255, 255, 0.82); + color: #53604b; + font-size: 13px; +} + +.worker-panel-stats { + display: flex; + justify-content: flex-end; + gap: 10px; + flex-wrap: wrap; +} + +.worker-stat-chip { + min-width: 104px; + padding: 12px 14px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(216, 226, 208, 0.92); + text-align: center; +} + +.worker-stat-chip-label { + display: block; + color: #6d7561; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.worker-stat-chip strong { + display: block; + margin-top: 8px; + color: var(--rq-text-strong); + font-size: 22px; + line-height: 1; +} + +.worker-stat-chip-warning { + background: rgba(255, 244, 220, 0.82); +} + +.worker-stat-chip-danger { + background: rgba(255, 233, 233, 0.88); +} + +.worker-stat-chip-neutral { + background: rgba(223, 243, 247, 0.62); +} + +.worker-panel-caret { + position: relative; + width: 16px; + height: 16px; +} + +.worker-panel-caret::before, +.worker-panel-caret::after { + content: ""; + position: absolute; + top: 7px; + width: 11px; + height: 2px; + border-radius: 999px; + background: #5f6955; + transition: transform 0.2s ease; +} + +.worker-panel-caret::before { + left: 0; + transform: rotate(45deg); +} + +.worker-panel-caret::after { + right: 0; + transform: rotate(-45deg); +} + +.worker-panel[open] .worker-panel-caret::before { + transform: rotate(-45deg); +} + +.worker-panel[open] .worker-panel-caret::after { + transform: rotate(45deg); +} + +.worker-panel-body { + padding: 0 26px 26px; +} + +.worker-queue-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); + gap: 16px; +} + +.worker-queue-card { + display: flex; + flex-direction: column; + gap: 18px; + min-height: 100%; + padding: 20px; + border: 1px solid rgba(216, 226, 208, 0.9); + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 243, 0.92)); +} + +.worker-queue-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.worker-queue-label { + display: block; + margin-bottom: 8px; + color: #7a846e; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.worker-queue-title { + margin: 0; + font-size: 19px; + line-height: 1.25; + word-break: break-word; +} + +.worker-status-badge { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 0 12px; + border-radius: 999px; + background: rgba(223, 243, 247, 0.72); + color: #1f6472; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.worker-status-active { + background: rgba(223, 243, 247, 0.72); + color: #1f6472; +} + +.worker-status-stale { + background: rgba(255, 244, 220, 0.88); + color: #946423; +} + +.worker-status-paused { + background: rgba(236, 236, 236, 0.9); + color: #505050; +} + +.worker-queue-timeline { + display: grid; + gap: 12px; +} + +.worker-queue-event { + padding: 14px 16px; + border-radius: 16px; + background: rgba(245, 248, 241, 0.8); +} + +.worker-queue-event-label { + display: block; + color: #6d7561; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.worker-queue-event-time { + display: block; + margin-top: 8px; + color: var(--rq-text-strong); + font-size: 15px; + line-height: 1.45; + word-break: break-word; +} + +.worker-queue-event-age { + display: block; + margin-top: 6px; + color: var(--rq-text-soft); + font-size: 13px; +} + +.worker-queue-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-top: 2px; +} + +.worker-queue-footer-label { + color: #6d7561; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.worker-queue-footer-value { + color: var(--rq-text-strong); + font-size: 24px; + line-height: 1; +} + +.queue-dashboard { + padding-bottom: 40px; +} + +.queue-hero { + display: grid; + grid-template-columns: minmax(0, 1.8fr) minmax(260px, 0.9fr); + gap: 28px; + align-items: end; + margin-bottom: 28px; + padding: 32px 34px; + border: 1px solid rgba(42, 155, 177, 0.16); + border-radius: 28px; + background: + radial-gradient(circle at top right, rgba(42, 155, 177, 0.18), transparent 34%), + radial-gradient(circle at bottom left, rgba(148, 192, 69, 0.16), transparent 30%), + linear-gradient(145deg, #fcfefb, #f3f8f5 58%, #ffffff); + box-shadow: 0 28px 60px rgba(43, 58, 29, 0.12); +} + +.queue-hero-kicker { + display: inline-block; + margin-bottom: 10px; + color: var(--rq-accent); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.22em; + text-transform: uppercase; +} + +.queue-hero-title { + margin: 0; + color: var(--rq-text-strong); + font-size: 40px; + line-height: 1.05; + font-weight: 700; +} + +.queue-hero-subtitle { + max-width: 700px; + margin: 14px 0 0; + color: var(--rq-text-soft); + font-size: 16px; + line-height: 1.7; +} + +.queue-hero-meta { + display: grid; + gap: 16px; +} + +.queue-hero-stat { + padding: 18px 20px; + border: 1px solid rgba(42, 155, 177, 0.16); + border-radius: 20px; + background: rgba(255, 255, 255, 0.88); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7); +} + +.queue-hero-stat-label { + display: block; + color: #6e7a62; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.queue-hero-stat-value { + display: block; + margin-top: 8px; + color: var(--rq-text-strong); + font-size: 30px; + line-height: 1; +} + +.queue-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + margin-bottom: 20px; +} + +.queue-toolbar-bottom { + margin-top: 24px; +} + +.queue-toolbar-note { + color: var(--rq-text-soft); + font-size: 14px; +} + +.queue-empty-state { + padding: 44px 32px; + border: 1px dashed rgba(42, 155, 177, 0.34); + border-radius: 24px; + background: linear-gradient(145deg, #fafdfc, #f1f7f8); + text-align: center; +} + +.queue-empty-state h2 { + margin: 0 0 10px; + color: var(--rq-text-strong); + font-size: 28px; +} + +.queue-empty-state p { + margin: 0; + color: var(--rq-text-soft); +} + +.queue-card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 18px; +} + +.queue-card { + display: flex; + flex-direction: column; + gap: 18px; + min-height: 100%; + padding: 22px; + border: 1px solid rgba(148, 192, 69, 0.16); + border-radius: 24px; + background: + radial-gradient(circle at top left, rgba(148, 192, 69, 0.12), transparent 26%), + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 243, 0.95)); + box-shadow: 0 18px 40px rgba(43, 58, 29, 0.08); +} + +.queue-card-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; +} + +.queue-card-label { + display: block; + margin-bottom: 8px; + color: #7a846e; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.queue-card-title { + margin: 0; + font-size: 22px; + line-height: 1.2; + word-break: break-word; +} + +.queue-card-actions { + display: flex; + align-items: center; + gap: 8px; + justify-content: flex-end; + flex-shrink: 0; +} + +.queue-state-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 46px; + height: 46px; + border-radius: 16px; + border: 1px solid rgba(216, 226, 208, 0.9); + font-size: 26px; +} + +.queue-state-live { + background: rgba(223, 243, 247, 0.72); + color: #1f6472; +} + +.queue-state-paused { + background: rgba(255, 233, 233, 0.88); + color: #975353; +} + +.queue-pause-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 46px; + height: 46px; + padding: 0; + border: 1px solid rgba(216, 226, 208, 0.9); + border-radius: 14px; + background: rgba(255, 255, 255, 0.95); +} + +.queue-state-badge .bx, +.queue-card .pause-queue-btn { + font-size: 28px; + line-height: 1; + color: var(--rq-accent); +} + +.queue-state-paused .bx { + color: #975353; +} + +.queue-card-metrics { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.queue-metric { + padding: 14px 16px; + border-radius: 18px; + background: rgba(245, 248, 241, 0.9); +} + +.queue-metric-label { + display: block; + color: #6d7561; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.queue-metric-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(216, 226, 208, 0.9); + color: var(--rq-accent); + font-size: 22px; + letter-spacing: 0; +} + +.queue-metric-value { + display: block; + margin-top: 8px; + color: var(--rq-text-strong); + font-size: 16px; + line-height: 1.45; + word-break: break-word; + white-space: nowrap; +} + +.queue-metric-infinity, +.queue-detail-infinity { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--rq-accent); + font-size: 28px; + line-height: 1; + vertical-align: middle; +} + +.queue-card-meta { + display: grid; + gap: 14px; +} + +.queue-meta-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 12px; +} + +.queue-meta-block { + padding: 16px 18px; + border: 1px solid rgba(216, 226, 208, 0.92); + border-radius: 18px; + background: rgba(255, 255, 255, 0.84); +} + +.queue-meta-label { + display: block; + color: #6d7561; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.queue-meta-value { + margin-top: 8px; + color: var(--rq-text-strong); + font-size: 14px; + line-height: 1.6; + word-break: break-word; +} + +.queue-meta-value-inline { + white-space: nowrap; + font-size: 13px; +} + +.queue-dlq-list a { + margin-right: 8px; +} + +.queue-storage-section { + margin-top: 34px; +} + +.queue-section-header { + display: flex; + align-items: end; + justify-content: space-between; + gap: 18px; + margin-bottom: 18px; +} + +.queue-section-kicker { + display: inline-block; + margin-bottom: 8px; + color: var(--rq-accent); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.queue-section-title { + margin: 0; + color: var(--rq-text-strong); + font-size: 28px; +} + +.queue-section-copy { + max-width: 420px; + margin: 0; + color: var(--rq-text-soft); + font-size: 14px; + line-height: 1.7; + text-align: right; +} + +.queue-storage-grid { + display: grid; + gap: 16px; +} + +.queue-storage-card { + padding: 22px; + border: 1px solid rgba(216, 226, 208, 0.92); + border-radius: 24px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 248, 242, 0.9)); + box-shadow: 0 16px 36px rgba(43, 58, 29, 0.06); +} + +.queue-storage-card-head { + margin-bottom: 14px; +} + +.queue-storage-label { + display: block; + margin-bottom: 8px; + color: #7a846e; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.queue-storage-title { + margin: 0; + color: var(--rq-text-strong); + font-size: 22px; +} + +.queue-storage-table { + margin-bottom: 0; + background: #ffffff; + border-radius: 18px; + overflow: hidden; +} + +.queue-storage-table thead th { + color: #53604b; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + border-top: 0; +} + +.queue-storage-table tbody td { + color: var(--rq-text-strong); + font-size: 14px; + vertical-align: middle; +} + a { color: #94c045; } @@ -41,6 +907,85 @@ h1, h2, h3, h4, h5, h6 { [data-aos-delay] { transition-delay: 0 !important; } + + .worker-hero { + grid-template-columns: 1fr; + padding: 26px 22px; + } + + .queue-hero { + grid-template-columns: 1fr; + padding: 26px 22px; + } + + .worker-hero-title { + font-size: 31px; + } + + .queue-hero-title { + font-size: 31px; + } + + .worker-toolbar { + flex-direction: column; + align-items: flex-start; + } + + .queue-toolbar { + flex-direction: column; + align-items: flex-start; + } + + .worker-panel-summary { + grid-template-columns: 1fr; + padding: 20px; + } + + .worker-panel-stats { + justify-content: flex-start; + } + + .worker-panel-caret { + display: none; + } + + .worker-panel-body { + padding: 0 20px 20px; + } + + .worker-queue-grid { + grid-template-columns: 1fr; + } + + .queue-card-grid { + grid-template-columns: 1fr; + } + + .queue-card-top { + flex-direction: column; + } + + .queue-card-actions { + width: 100%; + justify-content: flex-start; + } + + .queue-card-metrics { + grid-template-columns: 1fr; + } + + .queue-meta-grid { + grid-template-columns: 1fr; + } + + .queue-section-header { + flex-direction: column; + align-items: flex-start; + } + + .queue-section-copy { + text-align: left; + } } #header { @@ -408,12 +1353,11 @@ section { } .btn-poll { - min-width: 100px; + min-width: 110px; } #page-size { - min-width: 100px; - margin-left: 10px; + min-width: 96px; } .display-none { @@ -444,6 +1388,368 @@ section { max-width: 100px; } +.modal-data-explorer { + width: min(1120px, calc(100vw - 32px)); + max-width: 1120px; + margin: 1.75rem auto; +} + +.explorer-modal-content { + width: 100%; + border: 1px solid rgba(148, 192, 69, 0.18); + border-radius: 20px; + overflow: hidden; + background: + linear-gradient(180deg, rgba(248, 251, 242, 0.92) 0%, rgba(255, 255, 255, 1) 22%), + var(--rq-surface); + box-shadow: var(--rq-shadow); +} + +.explorer-modal-header { + padding: 24px 28px 18px; + border-bottom: 1px solid rgba(216, 226, 208, 0.9); + background: + radial-gradient(circle at top right, rgba(42, 155, 177, 0.16), transparent 32%), + linear-gradient(135deg, rgba(248, 251, 242, 1), rgba(255, 255, 255, 0.96)); +} + +.explorer-modal-heading { + display: flex; + flex-direction: column; + gap: 6px; +} + +.explorer-modal-kicker { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--rq-accent); +} + +.explorer-modal-header .modal-title { + margin: 0; + font-size: 34px; + line-height: 1.15; + font-weight: 600; + color: var(--rq-text-strong); +} + +.explorer-modal-close { + width: 44px; + height: 44px; + margin: 0 0 0 16px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 44px; + border-radius: 50%; + border: 1px solid rgba(148, 192, 69, 0.24); + background: rgba(255, 255, 255, 0.92); + color: #7c8870; + font-size: 34px; + line-height: 1; + opacity: 1; + transition: all 0.2s ease; +} + +.explorer-modal-close:hover, +.explorer-modal-close:focus { + outline: none; + color: var(--rq-text-strong); + background: #ffffff; + box-shadow: 0 10px 25px rgba(43, 58, 29, 0.12); +} + +.explorer-modal-body { + padding: 22px 28px 20px; +} + +.explorer-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 16px 18px; + margin-bottom: 18px; + border: 1px solid var(--rq-border); + border-radius: 16px; + background: + linear-gradient(135deg, rgba(245, 248, 241, 0.95), rgba(255, 255, 255, 0.96)); +} + +.explorer-toolbar-left, +.explorer-toolbar-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.explorer-select-group { + display: flex; + align-items: center; + gap: 10px; + margin: 0; +} + +.explorer-control-label { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--rq-text-soft); +} + +.explorer-select-group select { + height: 42px; + border: 1px solid var(--rq-border); + border-radius: 12px; + padding: 0 14px; + background: #ffffff; + color: var(--rq-text-strong); + font-weight: 600; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85); +} + +.explorer-table-shell { + border: 1px solid var(--rq-border); + border-radius: 18px; + overflow: hidden; + background: #ffffff; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); +} + +.explorer-table-wrap { + max-height: 54vh; + overflow: auto; +} + +.explorer-table { + margin-bottom: 0; + background: #ffffff; +} + +.explorer-table thead th { + position: sticky; + top: 0; + z-index: 1; + background: linear-gradient(180deg, #f7faf3, #eef5e6); + color: var(--rq-text-strong); + border-bottom-width: 1px; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.explorer-table td, +.explorer-table th { + vertical-align: middle; + padding: 14px 16px; + border-color: #e3eadb; +} + +.explorer-table tbody tr:nth-child(even) { + background: rgba(248, 251, 242, 0.56); +} + +.explorer-table tbody tr:hover { + background: rgba(223, 243, 247, 0.48); +} + +.explorer-message-block { + margin: 0 0 8px; + padding: 12px 14px; + border: 1px solid rgba(36, 45, 31, 0.08); + border-radius: 14px; + background: #1f2721; + color: #f2f7ef; + font-family: "SFMono-Regular", "Menlo", "Monaco", "Consolas", monospace; + font-size: 12px; + line-height: 1.55; + white-space: pre-wrap; + word-break: break-word; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +.explorer-message-block code { + color: inherit; + background: transparent; + padding: 0; + font-family: inherit; + font-size: inherit; + white-space: inherit; +} + +.jobs-div { + margin-top: 12px; +} + +.explorer-inline-table { + margin-bottom: 0; + background: #ffffff; + border-radius: 16px; + overflow: hidden; + border-color: #dfe7d8; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72); +} + +.explorer-inline-table thead th { + background: linear-gradient(180deg, #f7faf3, #edf3e6); + color: var(--rq-text-strong); + border-color: #dfe7d8; + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + padding: 12px 14px; +} + +.explorer-inline-table td { + border-color: #e3eadb; + padding: 12px 14px; + color: #263220; + font-size: 13px; + vertical-align: middle; +} + +.explorer-inline-table tbody tr:nth-child(even) { + background: rgba(248, 251, 242, 0.52); +} + +.explorer-inline-table tbody tr:hover { + background: rgba(223, 243, 247, 0.4); +} + +.jobs-btn, +.jobs-closer-btn { + margin-top: 8px; +} + +.explorer-table .btn-sm, +.explorer-table .btn-danger, +.explorer-table .btn-info, +.explorer-table .btn-secondary { + border-radius: 999px; + padding: 8px 12px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.02em; +} + +.explorer-action-group { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 148px; +} + +.explorer-action-group .btn { + width: 100%; + white-space: nowrap; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1.2; + box-shadow: none; +} + +.explorer-action-group .btn-info { + background: linear-gradient(135deg, #1f9ab5, #34b5c7); + border-color: #1f9ab5; +} + +.explorer-action-group .btn-info:hover, +.explorer-action-group .btn-info:focus { + background: linear-gradient(135deg, #1a839a, #2aa4b9); + border-color: #1a839a; +} + +.explorer-action-group .btn-secondary { + background: #edf1ec; + color: #31402b; + border-color: #d3dccb; +} + +.explorer-action-group .btn-secondary:hover, +.explorer-action-group .btn-secondary:focus { + background: #e4ebe1; + color: #263220; + border-color: #c5d0bc; +} + +.explorer-action-group .btn-danger { + background: linear-gradient(135deg, #dc334d, #f0485d); + border-color: #dc334d; +} + +.explorer-action-group .btn-danger:hover, +.explorer-action-group .btn-danger:focus { + background: linear-gradient(135deg, #c52b43, #e34155); + border-color: #c52b43; +} + +.explorer-modal-footer { + justify-content: center; + padding: 16px 28px 22px; + border-top: 1px solid rgba(216, 226, 208, 0.9); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(248, 251, 242, 0.96)); +} + +.explorer-pagination { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 14px; + min-width: auto !important; + max-width: none !important; +} + +.explorer-page-indicator { + min-width: 120px; + text-align: center; + font-size: 14px; + font-weight: 700; + color: var(--rq-text-soft); +} + +@media (max-width: 767px) { + .explorer-modal-header .modal-title { + font-size: 24px; + } + + .explorer-modal-header, + .explorer-modal-body, + .explorer-modal-footer { + padding-left: 16px; + padding-right: 16px; + } + + .explorer-toolbar { + flex-direction: column; + align-items: stretch; + } + + .explorer-toolbar-left, + .explorer-toolbar-actions, + .explorer-select-group, + .explorer-pagination { + width: 100%; + } + + .explorer-toolbar-actions { + justify-content: space-between; + } + + .explorer-pagination { + gap: 10px; + } + + .explorer-page-indicator { + min-width: 90px; + font-size: 12px; + } +} + .modal-confirm { color: #636363; @@ -517,6 +1823,82 @@ section { background: #ee3535; } +.modal-info { + width: 460px; + max-width: calc(100vw - 32px); +} + +.modal-info .modal-content { + padding: 20px 22px; + border: 1px solid rgba(216, 226, 208, 0.9); + border-radius: 24px; + background: + radial-gradient(circle at top right, rgba(42, 155, 177, 0.16), transparent 28%), + linear-gradient(180deg, rgba(248, 251, 242, 0.96), #ffffff 38%); + box-shadow: 0 24px 48px rgba(43, 58, 29, 0.18); +} + +.modal-info .modal-header { + border-bottom: none; + align-items: center; + padding: 0 0 8px; +} + +.modal-info .modal-title { + color: var(--rq-text-strong); + font-size: 28px; + font-weight: 700; +} + +.modal-info .modal-body { + padding: 6px 0 0; +} + +.modal-info .modal-body p { + margin: 0; + color: var(--rq-text-soft); + font-size: 15px; + line-height: 1.7; +} + +.modal-info .modal-footer { + border-top: none; + justify-content: flex-end; + padding: 18px 0 0; +} + +.modal-info .btn { + min-width: 120px; + min-height: 42px; + border-radius: 999px; + font-weight: 700; + box-shadow: none !important; +} + +.modal-info .close { + margin: 0; + padding: 0; + color: #7c8870; + opacity: 1; +} + +.modal-info .close:hover, +.modal-info .close:focus { + color: var(--rq-text-strong); +} + +.modal-info-success { + border-color: rgba(148, 192, 69, 0.28) !important; +} + +.modal-info-warning { + border-color: rgba(240, 180, 62, 0.34) !important; +} + +.modal-info-danger { + border-color: rgba(241, 94, 94, 0.36) !important; +} + .btn-delete-all { margin-left: 10px; } @@ -530,11 +1912,6 @@ section { height: 500px; } -.modal-data-explorer { - max-width: max-content; - min-width: 650px; -} - .jobs-table { } @@ -543,7 +1920,29 @@ section { } +.jobs-time-stack { + display: grid; + gap: 4px; + min-width: 210px; +} + +.jobs-time-label { + color: #6d7561; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.jobs-time-value { + color: #273126; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 14px; + line-height: 1.4; + word-break: break-word; +} + .pause-queue-btn { font-size: 35px; cursor: pointer; -} \ No newline at end of file +} diff --git a/rqueue-core/src/main/resources/public/rqueue/js/rqueue.js b/rqueue-core/src/main/resources/public/rqueue/js/rqueue.js index b92bc4d2..e140efba 100644 --- a/rqueue-core/src/main/resources/public/rqueue/js/rqueue.js +++ b/rqueue-core/src/main/resources/public/rqueue/js/rqueue.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023 Sonu Kumar + * Copyright (c) 2020-2026 Sonu Kumar * * Licensed under the Apache License, Version 2.0 (the "License"); * You may not use this file except in compliance with the License. @@ -37,6 +37,41 @@ function showError(message) { $("#global-error-message").text(message); } +function showInfoModal(title, message, variant, onClose) { + let modalEl = $('#info-modal'); + let contentEl = modalEl.find('.modal-content'); + let okButton = modalEl.find('.btn'); + modalEl.off('hidden.bs.modal.info'); + contentEl.removeClass('modal-info-success modal-info-warning modal-info-danger'); + if (variant !== undefined && variant !== null && variant !== '') { + contentEl.addClass('modal-info-' + variant); + } + $('#info-modal-title').text(title); + $('#info-modal-body').html(message); + okButton.removeClass('btn-success btn-warning btn-danger btn-info'); + switch (variant) { + case 'success': + okButton.addClass('btn-success'); + break; + case 'warning': + okButton.addClass('btn-warning'); + break; + case 'danger': + okButton.addClass('btn-danger'); + break; + default: + okButton.addClass('btn-info'); + break; + } + if (onClose !== undefined) { + modalEl.on('hidden.bs.modal.info', function () { + modalEl.off('hidden.bs.modal.info'); + onClose(); + }); + } + modalEl.modal('show'); +} + function ajaxRequest(url, type, payload, successHandler, failureHandler) { $.ajax({ url: url, @@ -60,10 +95,11 @@ function json(data) { function reloadHandler(response) { if (response.code === 0) { - alert("Success!") - window.location.replace(window.location.href); + showInfoModal('Success', 'Operation completed successfully.', 'success', function () { + window.location.replace(window.location.href); + }); } else { - alert("Failed:" + response.message + " Please retry!"); + showInfoModal('Action Failed', response.message + ' Please retry!', 'danger'); console.log(response); } } @@ -188,7 +224,7 @@ function displayHeader(response, displayPageNumberEl, pageSize) { function constructTable(table, className) { // table : { headers: ["item", "score"], "rows: [{"columns":[{type:, value:, meta:[]}]}] } // - let tableEl = $('').addClass(className); + let tableEl = $('
').addClass(className + ' explorer-inline-table'); let thead = $(''); let headers = table.headers; let rows = table.rows; @@ -204,7 +240,24 @@ function constructTable(table, className) { for (let j = 0; j < columns.length; j++) { let column = columns[j]; if (column.type === 'DISPLAY') { - tr.append($('
').text(column.value)) + let td = $(''); + if (headers[j] === 'STARTTIME/ENDTIME' && typeof column.value === 'string' + && column.value.indexOf('/') !== -1) { + let timestamps = column.value.split('/', 2); + let stack = $('
').addClass('jobs-time-stack'); + stack.append( + $('
').addClass('jobs-time-label').text('Start')) + stack.append( + $('
').addClass('jobs-time-value').text(timestamps[0])) + stack.append( + $('
').addClass('jobs-time-label').text('End')) + stack.append( + $('
').addClass('jobs-time-value').text(timestamps[1])) + td.append(stack); + } else { + td.text(column.value) + } + tr.append(td) } else { console.log("unknown column type", column); } @@ -242,7 +295,7 @@ function jobsButton() { el.parent().append(jobsDivCloser); el.parent().append(jobsDiv); } else { - alert("Failed:" + response.message + " Please retry!"); + showInfoModal('Action Failed', response.message + ' Please retry!', 'danger'); console.log(response); } }, @@ -255,7 +308,10 @@ function renderColumn(column) { if (column.type === 'DISPLAY') { if (column.meta !== undefined) { let div = $('
'); - div.append($('

').text(column.value)); + div.append( + $('

')
+              .addClass('explorer-message-block')
+              .append($('').text(column.value)));
       for (let i = 0; i < column.meta.length; i++) {
         let meta = column.meta[i];
         switch (meta.type) {
@@ -272,12 +328,33 @@ function renderColumn(column) {
       td.text(column.value);
     }
   } else if (column.type === 'ACTION') {
+    let actionGroup = $('
').addClass('explorer-action-group'); if (column.value === 'DELETE') { - td.append( - $('').addClass('delete-message-btn btn-danger').text('Delete')); + actionGroup.append( + $(' + -
- - {% endfor %} + + + +
+
Use each queue card to open detailed queue state and message explorers.
+
+ {% if hasPreviousPage %} + Previous + {% endif %} + Page {{currentPage}} of {{totalPages}} + {% if hasNextPage %} + Next + {% endif %} +
+
{% endblock %} diff --git a/rqueue-core/src/main/resources/templates/rqueue/workers.html b/rqueue-core/src/main/resources/templates/rqueue/workers.html new file mode 100644 index 00000000..3c6b7464 --- /dev/null +++ b/rqueue-core/src/main/resources/templates/rqueue/workers.html @@ -0,0 +1,161 @@ +{% extends 'base' %} +{% block main %} + + +
+
+
+ Worker Registry +

Live Pollers Across Your Fleet

+

+ Inspect poll activity, queue ownership, and recent capacity pressure without leaving the dashboard. +

+
+
+
+ Workers + {{totalWorkerCount}} +
+
+ Page + {{currentPage}} / {{totalPages}} +
+
+
+ +
+
+ {% if totalWorkerCount > 0 %} + Showing worker records for this page. + {% else %} + Waiting for worker heartbeats. + {% endif %} +
+
+ {% if hasPreviousPage %} + Previous + {% endif %} + Page {{currentPage}} + {% if hasNextPage %} + Next + {% endif %} +
+
+ + {% if workers is empty %} + + {% else %} +
+ {% for worker in workers %} +
+ +
+
+ Worker +

{{worker.workerId}}

+
+
+ Host {{worker.host}} + PID {{worker.pid}} + Last Poll {{ time(worker.lastPollAt) }} + {{worker.lastPollAge}} +
+
+
+
+ Active + {{worker.activeQueues}} +
+
+ Stale + {{worker.staleQueues}} +
+
+ Recent Exhaustion + {{worker.recentCapacityExhaustedQueues}} +
+
+ +
+ +
+
+ {% for poller in worker.pollers %} +
+
+
+ Queue +

+ {{poller.queue}} +

+
+ + {{poller.status}} + +
+ +
+
+ Last Poll + {% if poller.lastPollAt > 0 %}{{ time(poller.lastPollAt) }}{% else %}-{% endif %} + {{poller.lastPollAge}} +
+
+ Last Message + {% if poller.lastMessageAt > 0 %}{{ time(poller.lastMessageAt) }}{% else %}-{% endif %} + {{poller.lastMessageAge}} +
+
+ Last Exhausted + {% if poller.lastCapacityExhaustedAt > 0 %}{{ time(poller.lastCapacityExhaustedAt) }}{% else %}-{% endif %} + {{poller.lastCapacityExhaustedAge}} +
+
+ + +
+ {% endfor %} +
+
+
+ {% endfor %} +
+ {% endif %} + +
+
Use the queue cards to drill into the queue detail page.
+
+ {% if hasPreviousPage %} + Previous + {% endif %} + Page {{currentPage}} of {{totalPages}} + {% if hasNextPage %} + Next + {% endif %} +
+
+
+{% endblock %} diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/config/RqueueConfigTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/config/RqueueConfigTest.java index 5c797a43..a2437e8e 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/config/RqueueConfigTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/config/RqueueConfigTest.java @@ -17,9 +17,11 @@ package com.github.sonus21.rqueue.config; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.github.sonus21.TestBase; import com.github.sonus21.rqueue.CoreUnitTest; +import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,6 +43,8 @@ private void initialize(RqueueConfig rqueueConfig) { rqueueConfig.setLockKeyPrefix("lock::"); rqueueConfig.setQueueConfigKeyPrefix("q-config::"); rqueueConfig.setQueueStatKeyPrefix("q-stat::"); + rqueueConfig.setWorkerRegistryKeyPrefix("worker::"); + rqueueConfig.setWorkerRegistryQueueKeyPrefix("q-pollers::"); } @BeforeEach @@ -117,4 +121,37 @@ void getQueueConfigKey() { assertEquals("__rq::q-config::test", rqueueConfigVersion2.getQueueConfigKey("test")); assertEquals("__rq::q-config::test", rqueueConfigVersion2ClusterMode.getQueueConfigKey("test")); } + + @Test + void getWorkerRegistryKey() { + assertEquals("__rq::worker::test", rqueueConfigVersion1.getWorkerRegistryKey("test")); + assertEquals("__rq::worker::test", rqueueConfigVersion2.getWorkerRegistryKey("test")); + assertEquals( + "__rq::worker::test", rqueueConfigVersion2ClusterMode.getWorkerRegistryKey("test")); + } + + @Test + void getWorkerRegistryQueueKey() { + assertEquals("__rq::q-pollers::test", rqueueConfigVersion1.getWorkerRegistryQueueKey("test")); + assertEquals("__rq::q-pollers::test", rqueueConfigVersion2.getWorkerRegistryQueueKey("test")); + assertEquals( + "__rq::q-pollers::{test}", + rqueueConfigVersion2ClusterMode.getWorkerRegistryQueueKey("test")); + } + + @Test + void workerRegistryProperties() { + rqueueConfigVersion2.setWorkerRegistryEnabled(true); + rqueueConfigVersion2.setWorkerRegistryWorkerTtlInSeconds(300); + rqueueConfigVersion2.setWorkerRegistryWorkerHeartbeatIntervalInSeconds(60); + rqueueConfigVersion2.setWorkerRegistryQueueTtlInSeconds(3600); + rqueueConfigVersion2.setWorkerRegistryQueueHeartbeatIntervalInSeconds(15); + assertTrue(rqueueConfigVersion2.isWorkerRegistryEnabled()); + assertEquals(Duration.ofSeconds(300), rqueueConfigVersion2.getWorkerRegistryWorkerTtl()); + assertEquals( + Duration.ofSeconds(60), rqueueConfigVersion2.getWorkerRegistryWorkerHeartbeatInterval()); + assertEquals(Duration.ofSeconds(3600), rqueueConfigVersion2.getWorkerRegistryQueueTtl()); + assertEquals( + Duration.ofSeconds(15), rqueueConfigVersion2.getWorkerRegistryQueueHeartbeatInterval()); + } } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java index 6c437f7e..106bf32f 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java @@ -53,31 +53,41 @@ class RqueueMessageTemplateTest extends TestBase { @Mock private RedisConnectionFactory redisConnectionFactory; - @Mock - private RedisTemplate redisTemplate; - - @Mock private ListOperations listOperations; @Mock private DefaultScriptExecutor scriptExecutor; private RqueueMessageTemplate rqueueMessageTemplate; + private RedisTemplate redisTemplate; @BeforeEach public void init() throws Exception { MockitoAnnotations.openMocks(this); - rqueueMessageTemplate = - TestUtils.rqueueMessageTemplate(redisConnectionFactory, redisTemplate, scriptExecutor); + redisTemplate = + new RedisTemplate<>() { + @Override + public ListOperations opsForList() { + return listOperations; + } + }; + rqueueMessageTemplate = TestUtils.rqueueMessageTemplate(redisConnectionFactory, redisTemplate, + scriptExecutor); } @Test void add() { - doReturn(listOperations).when(redisTemplate).opsForList(); doReturn(1L).when(listOperations).rightPush(queueName, message); rqueueMessageTemplate.addMessage(queueName, message); } + @Test + void addAtFront() { + doReturn(1L).when(listOperations).leftPush(queueName, message); + rqueueMessageTemplate.addMessageAtFront(queueName, message); + verify(listOperations, times(1)).leftPush(queueName, message); + } + @Test void popN() { rqueueMessageTemplate.pop( diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImplTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImplTest.java index 4ccc37b3..4ee6f830 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImplTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImplTest.java @@ -16,17 +16,24 @@ package com.github.sonus21.rqueue.core.impl; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; import com.github.sonus21.TestBase; import com.github.sonus21.rqueue.CoreUnitTest; import com.github.sonus21.rqueue.config.RqueueConfig; import com.github.sonus21.rqueue.core.DefaultRqueueMessageConverter; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.EndpointRegistry; import com.github.sonus21.rqueue.core.RqueueMessageEnqueuer; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.listener.QueueDetail; import com.github.sonus21.rqueue.listener.RqueueMessageHeaders; +import com.github.sonus21.rqueue.models.db.MessageMetadata; import com.github.sonus21.rqueue.utils.Constants; import com.github.sonus21.rqueue.utils.TestUtils; import com.github.sonus21.rqueue.web.service.RqueueMessageMetadataService; @@ -36,6 +43,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.messaging.MessageHeaders; @@ -46,6 +54,7 @@ class RqueueMessageEnqueuerImplTest extends TestBase { private static final String queue = "test-queue"; private static final QueueDetail queueDetail = TestUtils.createQueueDetail(queue); + private static final RqueueMessageIdGenerator FIXED_MESSAGE_ID_GENERATOR = () -> "custom-id"; MessageConverter messageConverter = new DefaultRqueueMessageConverter(); MessageHeaders messageHeaders = RqueueMessageHeaders.emptyMessageHeaders(); @@ -54,8 +63,6 @@ class RqueueMessageEnqueuerImplTest extends TestBase { @Mock private RqueueMessageTemplate messageTemplate; - - @Mock private RqueueConfig rqueueConfig; private RqueueMessageEnqueuer rqueueMessageEnqueuer; @@ -74,11 +81,15 @@ public static void clean() { @BeforeEach public void init() throws IllegalAccessException { MockitoAnnotations.openMocks(this); + rqueueConfig = new RqueueConfig(null, null, true, 2); + rqueueConfig.setMessageDurabilityInMinute(10080); rqueueMessageEnqueuer = - new RqueueMessageEnqueuerImpl(messageTemplate, messageConverter, messageHeaders); + new RqueueMessageEnqueuerImpl( + messageTemplate, messageConverter, messageHeaders, FIXED_MESSAGE_ID_GENERATOR); FieldUtils.writeField(rqueueMessageEnqueuer, "rqueueConfig", rqueueConfig, true); - FieldUtils.writeField( - rqueueMessageEnqueuer, "rqueueMessageMetadataService", rqueueMessageMetadataService, true); + FieldUtils.writeField(rqueueMessageEnqueuer, "rqueueMessageMetadataService", + rqueueMessageMetadataService, true); + doNothing().when(rqueueMessageMetadataService).save(any(), any(), anyBoolean()); } @Test @@ -140,4 +151,14 @@ void enqueuePeriodic() { rqueueMessageEnqueuer.enqueuePeriodic(queue, id, "test-message", 5 * Constants.ONE_MILLI); } + + @Test + void enqueueUsesInjectedMessageIdGenerator() { + rqueueMessageEnqueuer.enqueue(queue, "test-message"); + + ArgumentCaptor messageMetadataCaptor = ArgumentCaptor.forClass( + MessageMetadata.class); + verify(rqueueMessageMetadataService).save(messageMetadataCaptor.capture(), any(), anyBoolean()); + assertEquals("custom-id", messageMetadataCaptor.getValue().getRqueueMessage().getId()); + } } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtilsTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtilsTest.java index 3fa593d9..f5a295d0 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtilsTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtilsTest.java @@ -27,6 +27,7 @@ import com.github.sonus21.rqueue.converter.GenericMessageConverter; import com.github.sonus21.rqueue.core.DefaultRqueueMessageConverter; import com.github.sonus21.rqueue.core.RqueueMessage; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.listener.RqueueMessageHeaders; import com.google.common.collect.ImmutableList; import java.util.UUID; @@ -43,6 +44,8 @@ @CoreUnitTest class RqueueMessageUtilsTest extends TestBase { + private static final RqueueMessageIdGenerator FIXED_MESSAGE_ID_GENERATOR = () -> "fixed-id"; + private final String queue = "test-queue"; DefaultRqueueMessageConverter messageConverter = new DefaultRqueueMessageConverter(); DefaultRqueueMessageConverter messageConverter2 = @@ -53,14 +56,16 @@ class RqueueMessageUtilsTest extends TestBase { void buildPeriodicMessage() { Email email = Email.newInstance(); long startTime = System.currentTimeMillis(); - RqueueMessage message = RqueueMessageUtils.buildPeriodicMessage( - messageConverter, - queue, - null, - email, - null, - 10_000L, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = + RqueueMessageUtils.buildPeriodicMessage( + FIXED_MESSAGE_ID_GENERATOR, + messageConverter, + queue, + null, + email, + null, + 10_000L, + RqueueMessageHeaders.emptyMessageHeaders()); assertEquals(10_000L, message.getPeriod()); long now = System.currentTimeMillis(); assertTrue( @@ -80,14 +85,16 @@ void buildMessage() { Email email = Email.newInstance(); long startTime = System.currentTimeMillis(); long startTimeInNano = System.nanoTime(); - RqueueMessage message = RqueueMessageUtils.buildMessage( - messageConverter, - queue, - null, - email, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = + RqueueMessageUtils.buildMessage( + FIXED_MESSAGE_ID_GENERATOR, + messageConverter, + queue, + null, + email, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); assertEquals(0, message.getPeriod()); long now = System.currentTimeMillis(); long nowNano = System.nanoTime(); @@ -104,19 +111,39 @@ void buildMessage() { assertEquals(convertedMessage, message.getMessage()); } + @Test + void buildMessageUsesProvidedMessageIdGenerator() { + Email email = Email.newInstance(); + + RqueueMessage message = + RqueueMessageUtils.buildMessage( + FIXED_MESSAGE_ID_GENERATOR, + messageConverter, + queue, + null, + email, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); + + assertEquals("fixed-id", message.getId()); + } + @Test void buildMessageWithDelay() { Email email = Email.newInstance(); long startTime = System.currentTimeMillis(); long startTimeInNano = System.nanoTime(); - RqueueMessage message = RqueueMessageUtils.buildMessage( - messageConverter, - queue, - null, - email, - 3, - 10_000L, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = + RqueueMessageUtils.buildMessage( + FIXED_MESSAGE_ID_GENERATOR, + messageConverter, + queue, + null, + email, + 3, + 10_000L, + RqueueMessageHeaders.emptyMessageHeaders()); assertEquals(0, message.getPeriod()); long now = System.currentTimeMillis(); long nowNano = System.nanoTime(); @@ -140,6 +167,7 @@ void buildMessageNull() { GenericClass genericClass = new GenericClass<>(); try { RqueueMessageUtils.buildMessage( + FIXED_MESSAGE_ID_GENERATOR, messageConverter, queue, null, @@ -159,6 +187,7 @@ void buildPeriodicMessageNull() { GenericClass genericClass = new GenericClass<>(); try { RqueueMessageUtils.buildPeriodicMessage( + FIXED_MESSAGE_ID_GENERATOR, messageConverter, queue, null, @@ -179,6 +208,7 @@ void buildMessageReturnInvalidType() { GenericClass genericClass = new GenericClass<>(); try { RqueueMessageUtils.buildMessage( + FIXED_MESSAGE_ID_GENERATOR, messageConverter2, queue, null, @@ -199,6 +229,7 @@ void buildPeriodicMessageReturnInvalidType() { GenericClass genericClass = new GenericClass<>(); try { RqueueMessageUtils.buildPeriodicMessage( + FIXED_MESSAGE_ID_GENERATOR, messageConverter2, queue, null, diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/JobImplTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/JobImplTest.java index 3f23abf5..05888f6f 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/JobImplTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/JobImplTest.java @@ -48,6 +48,7 @@ import com.github.sonus21.rqueue.models.enums.ExecutionStatus; import com.github.sonus21.rqueue.models.enums.JobStatus; import com.github.sonus21.rqueue.models.enums.MessageStatus; +import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; import com.github.sonus21.rqueue.utils.TestUtils; import com.github.sonus21.rqueue.web.service.RqueueMessageMetadataService; import java.time.Duration; @@ -68,7 +69,8 @@ class JobImplTest extends TestBase { private final QueueDetail queueDetail = TestUtils.createQueueDetail("test-queue"); private final MessageConverter messageConverter = new DefaultRqueueMessageConverter(); private final RqueueMessage rqueueMessage = - RqueueMessageUtils.generateMessage(messageConverter, queueDetail.getName()); + RqueueMessageUtils.generateMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, queueDetail.getName()); private final MessageMetadata messageMetadata = new MessageMetadata(rqueueMessage, MessageStatus.PROCESSING); private final Object userMessage = "Test Object"; diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueExecutorTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueExecutorTest.java index 607f3eb4..022f6813 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueExecutorTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueExecutorTest.java @@ -43,6 +43,7 @@ import com.github.sonus21.rqueue.listener.RqueueMessageListenerContainer.QueueStateMgr; import com.github.sonus21.rqueue.models.db.MessageMetadata; import com.github.sonus21.rqueue.models.enums.MessageStatus; +import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; import com.github.sonus21.rqueue.utils.Constants; import com.github.sonus21.rqueue.utils.QueueThreadPool; import com.github.sonus21.rqueue.utils.TestUtils; @@ -114,14 +115,16 @@ class RqueueExecutorTest extends TestBase { public void init() throws IllegalAccessException { MockitoAnnotations.openMocks(this); MessageConverter messageConverter = new GenericMessageConverter(); - rqueueMessage = RqueueMessageUtils.buildMessage( - messageConverter, - queueName, - null, - payload, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + rqueueMessage = + RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + queueName, + null, + payload, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); defaultMessageMetadata = new MessageMetadata(rqueueMessage, MessageStatus.ENQUEUED); MessageProcessorHandler messageProcessorHandler = new MessageProcessorHandler(null, deadLetterProcessor, discardProcessor, null); diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java index 6351adfa..f0b280c1 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java @@ -52,6 +52,7 @@ import com.github.sonus21.rqueue.listener.RqueueMessageListenerContainer.QueueStateMgr; import com.github.sonus21.rqueue.models.db.MessageMetadata; import com.github.sonus21.rqueue.models.enums.MessageStatus; +import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; import com.github.sonus21.rqueue.utils.Constants; import com.github.sonus21.rqueue.utils.QueueThreadPool; import com.github.sonus21.rqueue.utils.TestUtils; @@ -125,14 +126,16 @@ class RqueueMiddlewareTest extends TestBase { @BeforeEach public void init() throws IllegalAccessException { MockitoAnnotations.openMocks(this); - rqueueMessage = RqueueMessageUtils.buildMessage( - messageConverter, - queueName, - null, - payload, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + rqueueMessage = + RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + queueName, + null, + payload, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); defaultMessageMetadata = new MessageMetadata(rqueueMessage, MessageStatus.ENQUEUED); MessageProcessorHandler messageProcessorHandler = new MessageProcessorHandler(null, job -> true, job -> true, null); @@ -232,14 +235,16 @@ void logContextAndPermissionMiddleware() { .when(rqueueMessageMetadataService) .get(defaultMessageMetadata.getId()); QueueDetail queueDetail = TestUtils.createQueueDetail(queueName); - RqueueMessage rqueueMessage1 = RqueueMessageUtils.buildMessage( - messageConverter, - null, - queueName, - payload, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage rqueueMessage1 = + RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + queueName, + null, + payload, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); permissionMiddleware.allowedMessageIds.add(rqueueMessage.getId()); MessageMetadata messageMetadata = new MessageMetadata(rqueueMessage1, MessageStatus.ENQUEUED); doAnswer(invocation -> { @@ -294,14 +299,16 @@ void logAndRateLimiterMiddleware() throws TimedOutException { List messages = new ArrayList<>(); int jobCount = 100; for (int i = 0; i < jobCount; i++) { - RqueueMessage message = RqueueMessageUtils.buildMessage( - messageConverter, - queueName, - null, - payload, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = + RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + queueName, + null, + payload, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); MessageMetadata messageMetadata = new MessageMetadata(message, MessageStatus.ENQUEUED); messages.add(message); map.put(message.getId(), messageMetadata); diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/DateTimeUtilsTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/DateTimeUtilsTest.java index 5859cc74..b3f12576 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/DateTimeUtilsTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/DateTimeUtilsTest.java @@ -85,4 +85,21 @@ void formatMilliToString() { assertEquals("1970-04-26 17:46", DateTimeUtils.formatMilliToString(10000000000L)); assertEquals("", DateTimeUtils.formatMilliToString(null)); } + + @Test + void formatMilliToReadableString() { + assertEquals("26 Apr, 1970 at 05:46 PM", DateTimeUtils.formatMilliToReadableString(10000000000L)); + assertEquals("", DateTimeUtils.formatMilliToReadableString(null)); + } + + @Test + void formatMilliToCompactDuration() { + assertEquals("90 sec", DateTimeUtils.formatMilliToCompactDuration(90000L)); + assertEquals("15 min", DateTimeUtils.formatMilliToCompactDuration(900000L)); + assertEquals("1 hr", DateTimeUtils.formatMilliToCompactDuration(3600000L)); + assertEquals("1 hr 1 min", DateTimeUtils.formatMilliToCompactDuration(3660000L)); + assertEquals("- 15 min", DateTimeUtils.formatMilliToCompactDuration(-900000L)); + assertEquals("999 ms", DateTimeUtils.formatMilliToCompactDuration(999L)); + assertEquals("", DateTimeUtils.formatMilliToCompactDuration(null)); + } } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/MessageMetadataTestUtils.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/MessageMetadataTestUtils.java index 80c60fd7..5533f3aa 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/MessageMetadataTestUtils.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/MessageMetadataTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Sonu Kumar + * Copyright (c) 2023-2026 Sonu Kumar * * Licensed under the Apache License, Version 2.0 (the "License"); * You may not use this file except in compliance with the License. @@ -36,7 +36,8 @@ public static MessageMetadata createMessageMetadata( public static MessageMetadata createMessageMetadata( MessageConverter messageConverter, String queue, Object message) { RqueueMessage rqueueMessage = RqueueMessageUtils.generateMessages( - messageConverter, message, queue, null, null, 1) + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, message, queue, null, + null, 1) .get(0); return new MessageMetadata(rqueueMessage, MessageStatus.ENQUEUED); } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/RqueueMessageTestUtils.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/RqueueMessageTestUtils.java index 77f1b12f..00cf534a 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/RqueueMessageTestUtils.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/RqueueMessageTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Sonu Kumar + * Copyright (c) 2023-2026 Sonu Kumar * * Licensed under the Apache License, Version 2.0 (the "License"); * You may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import com.github.sonus21.rqueue.core.DefaultRqueueMessageConverter; import com.github.sonus21.rqueue.core.RqueueMessage; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; +import com.github.sonus21.rqueue.core.impl.UuidV4RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.support.RqueueMessageUtils; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -26,11 +28,14 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RqueueMessageTestUtils { + public static final RqueueMessageIdGenerator MESSAGE_ID_GENERATOR = + new UuidV4RqueueMessageIdGenerator(); + public static RqueueMessage createMessage(String queueName) { return createMessage(new DefaultRqueueMessageConverter(), queueName); } public static RqueueMessage createMessage(MessageConverter messageConverter, String queue) { - return RqueueMessageUtils.generateMessage(messageConverter, queue); + return RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, messageConverter, queue); } } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/pebble/RqueuePebbleExtensionTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/pebble/RqueuePebbleExtensionTest.java new file mode 100644 index 00000000..23337298 --- /dev/null +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/pebble/RqueuePebbleExtensionTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.utils.pebble; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.github.sonus21.TestBase; +import com.github.sonus21.rqueue.CoreUnitTest; +import io.pebbletemplates.pebble.extension.Function; +import java.util.Collections; +import java.util.Map; +import java.util.TimeZone; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@CoreUnitTest +class RqueuePebbleExtensionTest extends TestBase { + + @BeforeEach + void init() { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + + @Test + void registersReadableAndDurationFunctions() { + Map functions = new RqueuePebbleExtension().getFunctions(); + + assertTrue(functions.containsKey(DateTimeFunction.FUNCTION_NAME)); + assertTrue(functions.containsKey(ReadableDateTimeFunction.FUNCTION_NAME)); + assertTrue(functions.containsKey(DurationFunction.FUNCTION_NAME)); + } + + @Test + void readableTimeFunctionFormatsReadableTimestamp() { + ReadableDateTimeFunction function = new ReadableDateTimeFunction(); + + Object value = function.execute(Collections.singletonMap("milli", 10000000000L), null, null, 0); + + assertEquals("26 Apr, 1970 at 05:46 PM", value); + } + + @Test + void durationFunctionFormatsCompactDuration() { + DurationFunction function = new DurationFunction(); + + Object value = function.execute(Collections.singletonMap("milli", 900000L), null, null, 0); + + assertEquals("15 min", value); + } +} diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueQDetailServiceTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueQDetailServiceTest.java index 89618d13..b5d7d19c 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueQDetailServiceTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueQDetailServiceTest.java @@ -48,6 +48,8 @@ import com.github.sonus21.rqueue.models.response.RowColumnMetaType; import com.github.sonus21.rqueue.models.response.TableColumn; import com.github.sonus21.rqueue.models.response.TableRow; +import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; import com.github.sonus21.rqueue.web.service.impl.RqueueQDetailServiceImpl; import java.util.ArrayList; import java.util.Arrays; @@ -74,10 +76,8 @@ class RqueueQDetailServiceTest extends TestBase { private final MessageConverter messageConverter = new GenericMessageConverter(); - @Mock - private RedisTemplate redisTemplate; - + private RedisTemplate redisTemplate; @Mock private RqueueRedisTemplate stringRqueueRedisTemplate; @@ -89,23 +89,26 @@ class RqueueQDetailServiceTest extends TestBase { @Mock private RqueueMessageMetadataService rqueueMessageMetadataService; - + @Mock + private RqueueWorkerRegistry rqueueWorkerRegistry; private RqueueQDetailService rqueueQDetailService; private QueueConfig queueConfig; private QueueConfig queueConfig2; private List queueConfigList; private Collection queues; - private RqueueConfig rqueueConfig = mock(RqueueConfig.class); + private final RqueueConfig rqueueConfig = new RqueueConfig(null, null, false, 2); @BeforeEach public void init() { MockitoAnnotations.openMocks(this); - rqueueQDetailService = new RqueueQDetailServiceImpl( - stringRqueueRedisTemplate, - rqueueMessageTemplate, - rqueueSystemManagerService, - rqueueMessageMetadataService, - rqueueConfig); + rqueueQDetailService = + new RqueueQDetailServiceImpl( + stringRqueueRedisTemplate, + rqueueMessageTemplate, + rqueueSystemManagerService, + rqueueMessageMetadataService, + rqueueConfig, + rqueueWorkerRegistry); queueConfig = createQueueConfig("test", 10, 10000L, "test-dlq"); queueConfig2 = createQueueConfig("test2", 10, 10000L, null); queueConfigList = Arrays.asList(queueConfig, queueConfig2); @@ -188,7 +191,8 @@ void getNavTabs() { @Test void getExplorePageDataQueue() { List rqueueMessages = - RqueueMessageUtils.generateMessages(messageConverter, "test", 10); + RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); DataViewResponse expectedResponse = new DataViewResponse(); List headers = new ArrayList<>(); headers.add("Id"); @@ -222,13 +226,14 @@ void getExplorePageDataQueue() { @Test void getExplorePageDataDeadLetterQueue() { List rqueueMessages = - RqueueMessageUtils.generateMessages(messageConverter, "test", 10); + RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); DataViewResponse expectedResponse = new DataViewResponse(); List headers = new ArrayList<>(); headers.add("Id"); headers.add("Message"); headers.add("Type"); - headers.add("AddedOn"); + headers.add("Added On"); expectedResponse.setHeaders(headers); List lists = new ArrayList<>(); for (RqueueMessage message : rqueueMessages) { @@ -262,7 +267,8 @@ void getExplorePageDataTypeQueueDeleteFewItems() { QueueConfig queueConfig = createQueueConfig("test", 10, 10000L, null); queueConfig.addDeadLetterQueue(new DeadLetterQueue("test-dlq", false)); List rqueueMessages = - RqueueMessageUtils.generateMessages(messageConverter, "test", 10); + RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); List messageMetadata = new ArrayList<>(); for (int i = 0; i < 5; i++) { RqueueMessage message = rqueueMessages.get(i); @@ -311,7 +317,8 @@ void getExplorePageDataTypeScheduledQueue() { QueueConfig queueConfig = createQueueConfig("test", 10, 10000L, null); queueConfig.addDeadLetterQueue(new DeadLetterQueue("test-dlq", false)); List rqueueMessages = - RqueueMessageUtils.generateMessages(messageConverter, "test", 100000, 10); + RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 100000, 10); DataViewResponse expectedResponse = new DataViewResponse(); List headers = new ArrayList<>(); headers.add("Id"); @@ -330,7 +337,7 @@ void getExplorePageDataTypeScheduledQueue() { Collections.singletonList( new RowColumnMeta(RowColumnMetaType.JOBS_BUTTON, message.getId())))); l.add(new TableColumn("Simple")); - l.add(new TableColumn(TableColumnType.ACTION, ActionType.DELETE)); + l.add(new TableColumn(TableColumnType.ACTION, ActionType.ENQUEUE)); lists.add(new TableRow(l)); } expectedResponse.setRows(lists); @@ -353,7 +360,8 @@ void getExplorePageDataTypeProcessingQueue() { QueueConfig queueConfig = createQueueConfig("test", 10, 10000L, null); queueConfig.addDeadLetterQueue(new DeadLetterQueue("test-dlq", false)); List rqueueMessages = - RqueueMessageUtils.generateMessages(messageConverter, "test", 100000, 10); + RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 100000, 10); DataViewResponse expectedResponse = new DataViewResponse(); List headers = new ArrayList<>(); headers.add("Id"); @@ -412,8 +420,10 @@ void viewDataKey() { void viewDataList() { List objects = new ArrayList<>(); objects.add("Test"); - objects.add(RqueueMessageUtils.buildMessage( - messageConverter, "jobs", null, "buildMessage", null, null, null)); + objects.add( + RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, "jobs", null, "buildMessage", null, null, null)); objects.add(null); doReturn(objects).when(stringRqueueRedisTemplate).lrange("jobs", 0, 9); DataViewResponse response = rqueueQDetailService.viewData("jobs", DataType.LIST, null, 0, 10); @@ -431,10 +441,12 @@ void viewDataList() { void viewDataZset() { Set> objects = new HashSet<>(); objects.add(new DefaultTypedTuple<>("Test", 100.0)); - objects.add(new DefaultTypedTuple<>( - RqueueMessageUtils.buildMessage( - messageConverter, "jobs", null, "buildMessage", null, null, null), - 200.0)); + objects.add( + new DefaultTypedTuple<>( + RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, "jobs", null, "buildMessage", null, null, null), + 200.0)); List tableRows = new ArrayList<>(); for (TypedTuple typedTuple : objects) { @@ -461,8 +473,10 @@ void viewDataZset() { void viewDataSet() { Set objects = new HashSet<>(); objects.add("Test"); - objects.add(RqueueMessageUtils.buildMessage( - messageConverter, "jobs", null, "Test object", null, null, null)); + objects.add( + RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, "jobs", null, "Test object", null, null, null)); List tableRows = new ArrayList<>(); for (Object object : objects) { tableRows.add(new TableRow(new TableColumn(String.valueOf(object)))); diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java index e1446468..2d90e1f1 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java @@ -31,13 +31,16 @@ import com.github.sonus21.rqueue.CoreUnitTest; import com.github.sonus21.rqueue.config.RqueueConfig; import com.github.sonus21.rqueue.config.RqueueWebConfig; +import com.github.sonus21.rqueue.core.RqueueMessage; import com.github.sonus21.rqueue.core.RqueueInternalPubSubChannel; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.dao.RqueueStringDao; import com.github.sonus21.rqueue.dao.RqueueSystemConfigDao; import com.github.sonus21.rqueue.models.MessageMoveResult; +import com.github.sonus21.rqueue.models.db.MessageMetadata; import com.github.sonus21.rqueue.models.db.QueueConfig; import com.github.sonus21.rqueue.models.enums.DataType; +import com.github.sonus21.rqueue.models.enums.MessageStatus; import com.github.sonus21.rqueue.models.request.MessageMoveRequest; import com.github.sonus21.rqueue.models.response.BaseResponse; import com.github.sonus21.rqueue.models.response.BooleanResponse; @@ -63,22 +66,17 @@ class RqueueUtilityServiceTest extends TestBase { @Mock private RqueueWebConfig rqueueWebConfig; - - @Mock private RqueueMessageTemplate rqueueMessageTemplate; @Mock private RqueueMessageMetadataService messageMetadataService; - @Mock - private RqueueConfig rqueueConfig; - @Mock private RqueueStringDao rqueueStringDao; @Mock private RqueueInternalPubSubChannel rqueueInternalPubSubChannel; - + private final RqueueConfig rqueueConfig = new RqueueConfig(null, null, false, 2); private RqueueUtilityService rqueueUtilityService; @BeforeEach @@ -109,6 +107,57 @@ void deleteMessage() { verify(messageMetadataService, times(1)).deleteMessage("notification", id, Duration.ofDays(30)); } + @Test + void enqueueMessage() { + String id = UUID.randomUUID().toString(); + BaseResponse response = rqueueUtilityService.enqueueMessage("notification", id, "REAR"); + assertEquals(1, response.getCode()); + assertEquals("Queue config not found!", response.getMessage()); + + QueueConfig queueConfig = createQueueConfig("notification", 3, 10000L, null); + queueConfig.setScheduledQueueName(""); + doReturn(queueConfig).when(rqueueSystemConfigDao).getConfigByName("notification", true); + response = rqueueUtilityService.enqueueMessage("notification", id, "REAR"); + assertEquals(1, response.getCode()); + assertEquals("Scheduled queue not found!", response.getMessage()); + + queueConfig.setScheduledQueueName("__rq::d-queue::notification"); + response = rqueueUtilityService.enqueueMessage("notification", id, "REAR"); + assertEquals(1, response.getCode()); + assertEquals("Message not found!", response.getMessage()); + + RqueueMessage rqueueMessage = + RqueueMessage.builder() + .id(id) + .queueName("notification") + .message("test") + .queuedTime(System.nanoTime()) + .processAt(System.currentTimeMillis()) + .build(); + MessageMetadata messageMetadata = new MessageMetadata(rqueueMessage, MessageStatus.ENQUEUED); + doReturn(messageMetadata).when(messageMetadataService).getByMessageId("notification", id); + doReturn(0L) + .when(rqueueMessageTemplate) + .removeElementFromZset("__rq::d-queue::notification", rqueueMessage); + response = rqueueUtilityService.enqueueMessage("notification", id, "REAR"); + assertEquals(1, response.getCode()); + assertEquals("Message is not available in the scheduled queue.", response.getMessage()); + + doReturn(1L) + .when(rqueueMessageTemplate) + .removeElementFromZset("__rq::d-queue::notification", rqueueMessage); + response = rqueueUtilityService.enqueueMessage("notification", id, "REAR"); + assertEquals(0, response.getCode()); + assertTrue(((BooleanResponse) response).isValue()); + verify(rqueueMessageTemplate, times(1)).addMessage("__rq::queue::notification", rqueueMessage); + + response = rqueueUtilityService.enqueueMessage("notification", id, "FRONT"); + assertEquals(0, response.getCode()); + assertTrue(((BooleanResponse) response).isValue()); + verify(rqueueMessageTemplate, times(1)) + .addMessageAtFront("__rq::queue::notification", rqueueMessage); + } + @Test void moveMessageInvalidRequest() { MessageMoveRequest request = new MessageMoveRequest(); diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImplTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImplTest.java new file mode 100644 index 00000000..fe7aed31 --- /dev/null +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImplTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2026 Sonu Kumar + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package com.github.sonus21.rqueue.worker; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import com.github.sonus21.TestBase; +import com.github.sonus21.rqueue.CoreUnitTest; +import com.github.sonus21.rqueue.common.RqueueRedisTemplate; +import com.github.sonus21.rqueue.config.RqueueConfig; +import com.github.sonus21.rqueue.listener.QueueDetail; +import com.github.sonus21.rqueue.models.Concurrency; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerInfo; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerMetadata; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView; +import com.github.sonus21.rqueue.utils.RedisUtils; +import com.github.sonus21.rqueue.utils.SerializationUtils; +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import tools.jackson.databind.ObjectMapper; + +@CoreUnitTest +class RqueueWorkerRegistryImplTest extends TestBase { + + private final RedisConnectionFactory redisConnectionFactory = null; + private TestWorkerTemplate workerTemplate; + private TestStringTemplate stringTemplate; + private final RedisTemplate redisTemplate = new RedisTemplate<>(); + + private RedisUtils.RedisTemplateProvider originalRedisTemplateProvider; + private final ObjectMapper objectMapper = SerializationUtils.createObjectMapper(); + + @BeforeEach + void init() { + MockitoAnnotations.openMocks(this); + redisTemplate.setConnectionFactory(Mockito.mock(RedisConnectionFactory.class)); + originalRedisTemplateProvider = RedisUtils.redisTemplateProvider; + RedisUtils.redisTemplateProvider = + new RedisUtils.RedisTemplateProvider() { + @Override + public RedisTemplate getRedisTemplate( + RedisConnectionFactory redisConnectionFactory) { + return (RedisTemplate) redisTemplate; + } + }; + workerTemplate = new TestWorkerTemplate(); + stringTemplate = new TestStringTemplate(); + } + + @AfterEach + void cleanup() { + RedisUtils.redisTemplateProvider = originalRedisTemplateProvider; + } + + @Test + void getQueueWorkersUsesCapacityExhaustedAsActivity() throws Exception { + RqueueConfig rqueueConfig = new RqueueConfig(redisConnectionFactory, null, false, 2); + rqueueConfig.setWorkerRegistryEnabled(true); + rqueueConfig.setWorkerRegistryQueueHeartbeatIntervalInSeconds(15); + rqueueConfig.setWorkerRegistryQueueTtlInSeconds(3600); + RqueueWorkerRegistryImpl registry = new RqueueWorkerRegistryImpl(rqueueConfig); + setField(registry, "workerTemplate", workerTemplate); + setField(registry, "stringTemplate", stringTemplate); + + long now = System.currentTimeMillis(); + String workerId = "worker-1"; + RqueueWorkerPollerMetadata metadata = + RqueueWorkerPollerMetadata.builder() + .workerId(workerId) + .lastPollAt(now - Duration.ofSeconds(40).toMillis()) + .lastCapacityExhaustedAt(now) + .capacityExhaustedCount(2L) + .build(); + stringTemplate.values = + Collections.singletonMap(workerId, objectMapper.writeValueAsString(metadata)); + workerTemplate.values = + Collections.singletonList( + RqueueWorkerInfo.builder() + .workerId(workerId) + .host("host-1") + .pid("123") + .startedAt(now - Duration.ofMinutes(5).toMillis()) + .lastSeenAt(now) + .build()); + + List workers = registry.getQueueWorkers("test"); + + assertEquals(1, workers.size()); + assertEquals("ACTIVE", workers.get(0).getStatus()); + assertEquals(2L, workers.get(0).getCapacityExhaustedCount()); + assertEquals("host-1", workers.get(0).getHost()); + assertFalse(workers.get(0).getLastCapacityExhaustedAge().isEmpty()); + } + + @Test + void recordQueueCapacityExhaustedSaturatesAtLongMaxValue() throws Exception { + RqueueConfig rqueueConfig = new RqueueConfig(redisConnectionFactory, null, false, 2); + rqueueConfig.setWorkerRegistryEnabled(true); + rqueueConfig.setWorkerRegistryQueueHeartbeatIntervalInSeconds(15); + rqueueConfig.setWorkerRegistryQueueTtlInSeconds(3600); + RqueueWorkerRegistryImpl registry = new RqueueWorkerRegistryImpl(rqueueConfig); + setField(registry, "workerTemplate", workerTemplate); + setField(registry, "stringTemplate", stringTemplate); + setField( + registry, + "capacityExhaustedCountByQueue", + new java.util.concurrent.ConcurrentHashMap<>( + Collections.singletonMap("test-queue", Long.MAX_VALUE))); + + QueueDetail queueDetail = + QueueDetail.builder() + .name("test-queue") + .queueName("test-queue") + .processingQueueName("test-queue-processing") + .processingQueueChannelName("test-queue-processing-channel") + .scheduledQueueName("test-queue-scheduled") + .scheduledQueueChannelName("test-queue-scheduled-channel") + .completedQueueName("test-queue-completed") + .active(true) + .visibilityTimeout(1000L) + .batchSize(1) + .numRetry(3) + .concurrency(new Concurrency(1, 1)) + .priority(Collections.emptyMap()) + .build(); + + registry.recordQueueCapacityExhausted( + queueDetail, + new com.github.sonus21.rqueue.utils.QueueThreadPool( + new SimpleAsyncTaskExecutor(), false, 1)); + + RqueueWorkerPollerMetadata metadata = + objectMapper.readValue(stringTemplate.lastHashValue, RqueueWorkerPollerMetadata.class); + + assertEquals(Long.MAX_VALUE, metadata.getCapacityExhaustedCount()); + } + + private static void setField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + private static class TestWorkerTemplate extends RqueueRedisTemplate { + private List values = Collections.emptyList(); + private RqueueWorkerInfo lastValue; + + TestWorkerTemplate() { + super(null); + } + + @Override + public List mget(java.util.Collection keys) { + return values; + } + + @Override + public void set(String key, RqueueWorkerInfo val, Duration duration) { + lastValue = val; + } + } + + private static class TestStringTemplate extends RqueueRedisTemplate { + private Map values = Collections.emptyMap(); + private String lastHashValue; + + TestStringTemplate() { + super(null); + } + + @Override + public Map getHashEntries(String key) { + return values; + } + + @Override + public void putHashValue(String key, String hashKey, String val) { + lastHashValue = val; + } + + @Override + public Boolean expire(String key, Duration duration) { + return true; + } + + @Override + public Long deleteHashValues(String key, String... hashKeys) { + return 0L; + } + } +} diff --git a/rqueue-spring-boot-starter/src/main/java/com/github/sonus21/rqueue/spring/boot/RqueueListenerAutoConfig.java b/rqueue-spring-boot-starter/src/main/java/com/github/sonus21/rqueue/spring/boot/RqueueListenerAutoConfig.java index a2e64f75..9e0fdd7d 100644 --- a/rqueue-spring-boot-starter/src/main/java/com/github/sonus21/rqueue/spring/boot/RqueueListenerAutoConfig.java +++ b/rqueue-spring-boot-starter/src/main/java/com/github/sonus21/rqueue/spring/boot/RqueueListenerAutoConfig.java @@ -21,6 +21,7 @@ import com.github.sonus21.rqueue.core.ReactiveRqueueMessageEnqueuer; import com.github.sonus21.rqueue.core.RqueueEndpointManager; import com.github.sonus21.rqueue.core.RqueueMessageEnqueuer; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageManager; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.core.impl.ReactiveRqueueMessageEnqueuerImpl; @@ -72,41 +73,53 @@ public RqueueMessageTemplate rqueueMessageTemplate( @Bean @ConditionalOnMissingBean public RqueueMessageManager rqueueMessageManager( - RqueueMessageHandler rqueueMessageHandler, RqueueMessageTemplate rqueueMessageTemplate) { + RqueueMessageHandler rqueueMessageHandler, + RqueueMessageTemplate rqueueMessageTemplate, + RqueueMessageIdGenerator rqueueMessageIdGenerator) { return new RqueueMessageManagerImpl( rqueueMessageTemplate, rqueueMessageHandler.getMessageConverter(), - simpleRqueueListenerContainerFactory.getMessageHeaders()); + simpleRqueueListenerContainerFactory.getMessageHeaders(), + rqueueMessageIdGenerator); } @Bean @ConditionalOnMissingBean public RqueueEndpointManager rqueueEndpointManager( - RqueueMessageHandler rqueueMessageHandler, RqueueMessageTemplate rqueueMessageTemplate) { + RqueueMessageHandler rqueueMessageHandler, + RqueueMessageTemplate rqueueMessageTemplate, + RqueueMessageIdGenerator rqueueMessageIdGenerator) { return new RqueueEndpointManagerImpl( rqueueMessageTemplate, rqueueMessageHandler.getMessageConverter(), - simpleRqueueListenerContainerFactory.getMessageHeaders()); + simpleRqueueListenerContainerFactory.getMessageHeaders(), + rqueueMessageIdGenerator); } @Bean @ConditionalOnMissingBean public RqueueMessageEnqueuer rqueueMessageEnqueuer( - RqueueMessageHandler rqueueMessageHandler, RqueueMessageTemplate rqueueMessageTemplate) { + RqueueMessageHandler rqueueMessageHandler, + RqueueMessageTemplate rqueueMessageTemplate, + RqueueMessageIdGenerator rqueueMessageIdGenerator) { return new RqueueMessageEnqueuerImpl( rqueueMessageTemplate, rqueueMessageHandler.getMessageConverter(), - simpleRqueueListenerContainerFactory.getMessageHeaders()); + simpleRqueueListenerContainerFactory.getMessageHeaders(), + rqueueMessageIdGenerator); } @Bean @ConditionalOnMissingBean @Conditional(ReactiveEnabled.class) public ReactiveRqueueMessageEnqueuer reactiveRqueueMessageEnqueuer( - RqueueMessageHandler rqueueMessageHandler, RqueueMessageTemplate rqueueMessageTemplate) { + RqueueMessageHandler rqueueMessageHandler, + RqueueMessageTemplate rqueueMessageTemplate, + RqueueMessageIdGenerator rqueueMessageIdGenerator) { return new ReactiveRqueueMessageEnqueuerImpl( rqueueMessageTemplate, rqueueMessageHandler.getMessageConverter(), - simpleRqueueListenerContainerFactory.getMessageHeaders()); + simpleRqueueListenerContainerFactory.getMessageHeaders(), + rqueueMessageIdGenerator); } } diff --git a/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/integration/RqueueMessageTemplateTest.java b/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/integration/RqueueMessageTemplateTest.java index cbc45661..df4e4743 100644 --- a/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/integration/RqueueMessageTemplateTest.java +++ b/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/integration/RqueueMessageTemplateTest.java @@ -23,6 +23,7 @@ import com.github.sonus21.rqueue.core.DefaultRqueueMessageConverter; import com.github.sonus21.rqueue.core.RqueueMessage; +import com.github.sonus21.rqueue.core.impl.UuidV4RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.support.RqueueMessageUtils; import com.github.sonus21.rqueue.spring.boot.application.Application; import com.github.sonus21.rqueue.spring.boot.tests.SpringBootIntegrationTest; @@ -43,6 +44,8 @@ @TestPropertySource(properties = {"use.system.redis=false", "spring.data.redis.port:8004"}) @SpringBootIntegrationTest class RqueueMessageTemplateTest extends SpringTestBase { + private static final UuidV4RqueueMessageIdGenerator MESSAGE_ID_GENERATOR = + new UuidV4RqueueMessageIdGenerator(); @Test void moveMessageFromDeadLetterQueueToOriginalQueue() { @@ -105,11 +108,11 @@ void moveMessageZsetToZset() { void getScore() { String tgtZset = "getScoreZSet"; MessageConverter converter = new DefaultRqueueMessageConverter(); - RqueueMessage message = RqueueMessageUtils.generateMessage(converter, tgtZset); - RqueueMessage message2 = RqueueMessageUtils.generateMessage(converter, tgtZset); - RqueueMessage message3 = RqueueMessageUtils.generateMessage(converter, tgtZset); - RqueueMessage message4 = RqueueMessageUtils.generateMessage(converter, tgtZset); - RqueueMessage message5 = RqueueMessageUtils.generateMessage(converter, tgtZset); + RqueueMessage message = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message2 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message3 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message4 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message5 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); long score = System.currentTimeMillis(); rqueueMessageTemplate.addToZset(tgtZset, message2, score); rqueueMessageTemplate.addToZset(tgtZset, message3, 0); @@ -126,9 +129,9 @@ void getScore() { void updateScore() { String tgtZset = "updateScoreZSet"; MessageConverter converter = new DefaultRqueueMessageConverter(); - RqueueMessage message = RqueueMessageUtils.generateMessage(converter, tgtZset); - RqueueMessage message2 = RqueueMessageUtils.generateMessage(converter, tgtZset); - RqueueMessage message3 = RqueueMessageUtils.generateMessage(converter, tgtZset); + RqueueMessage message = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message2 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message3 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); long score = System.currentTimeMillis(); rqueueMessageTemplate.addToZset(tgtZset, message, score); assertTrue(rqueueMessageTemplate.addScore(tgtZset, message, 10_000)); diff --git a/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/unit/RqueueListenerAutoConfigTest.java b/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/unit/RqueueListenerAutoConfigTest.java index 88e2125b..1752efaf 100644 --- a/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/unit/RqueueListenerAutoConfigTest.java +++ b/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/unit/RqueueListenerAutoConfigTest.java @@ -29,6 +29,7 @@ import com.github.sonus21.rqueue.core.DefaultRqueueMessageConverter; import com.github.sonus21.rqueue.core.RqueueMessageEnqueuer; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; +import com.github.sonus21.rqueue.core.impl.UuidV4RqueueMessageIdGenerator; import com.github.sonus21.rqueue.listener.RqueueMessageHandler; import com.github.sonus21.rqueue.spring.boot.RqueueListenerAutoConfig; import com.github.sonus21.rqueue.spring.boot.tests.SpringBootUnitTest; @@ -121,7 +122,9 @@ void rqueueMessageSenderWithMessageTemplate() throws IllegalAccessException { doReturn(new DefaultRqueueMessageConverter()).when(rqueueMessageHandler).getMessageConverter(); RqueueListenerAutoConfig messageAutoConfig = new RqueueListenerAutoConfig(); FieldUtils.writeField(messageAutoConfig, "simpleRqueueListenerContainerFactory", factory, true); - assertNotNull(messageAutoConfig.rqueueMessageEnqueuer(rqueueMessageHandler, messageTemplate)); + assertNotNull( + messageAutoConfig.rqueueMessageEnqueuer( + rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator())); assertEquals(factory.getRqueueMessageTemplate().hashCode(), messageTemplate.hashCode()); } @@ -135,9 +138,12 @@ void rqueueMessageSenderWithMessageConverters() throws IllegalAccessException { factory.setRqueueMessageTemplate(messageTemplate); FieldUtils.writeField(messageAutoConfig, "simpleRqueueListenerContainerFactory", factory, true); doReturn(messageConverter).when(rqueueMessageHandler).getMessageConverter(); - assertNotNull(messageAutoConfig.rqueueMessageEnqueuer(rqueueMessageHandler, messageTemplate)); + assertNotNull( + messageAutoConfig.rqueueMessageEnqueuer( + rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator())); RqueueMessageEnqueuer messageSender = - messageAutoConfig.rqueueMessageEnqueuer(rqueueMessageHandler, messageTemplate); + messageAutoConfig.rqueueMessageEnqueuer( + rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator()); MessageConverter converter = messageSender.getMessageConverter(); assertTrue(converter.hashCode() == messageConverter.hashCode()); } diff --git a/rqueue-spring-common-test/src/main/java/com/github/sonus21/rqueue/test/common/SpringTestBase.java b/rqueue-spring-common-test/src/main/java/com/github/sonus21/rqueue/test/common/SpringTestBase.java index 0e946b8a..699f68e6 100644 --- a/rqueue-spring-common-test/src/main/java/com/github/sonus21/rqueue/test/common/SpringTestBase.java +++ b/rqueue-spring-common-test/src/main/java/com/github/sonus21/rqueue/test/common/SpringTestBase.java @@ -24,8 +24,10 @@ import com.github.sonus21.rqueue.core.RqueueEndpointManager; import com.github.sonus21.rqueue.core.RqueueMessage; import com.github.sonus21.rqueue.core.RqueueMessageEnqueuer; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageManager; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; +import com.github.sonus21.rqueue.core.impl.UuidV4RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.support.RqueueMessageUtils; import com.github.sonus21.rqueue.dao.RqueueJobDao; import com.github.sonus21.rqueue.exception.TimedOutException; @@ -55,29 +57,17 @@ @Slf4j public abstract class SpringTestBase extends TestBase { - @Autowired - protected RqueueMessageTemplate rqueueMessageTemplate; - - @Autowired - protected RqueueConfig rqueueConfig; - - @Autowired - protected RqueueRedisTemplate stringRqueueRedisTemplate; - - @Autowired - protected ConsumedMessageStore consumedMessageStore; - - @Autowired - protected RqueueMessageListenerContainer rqueueMessageListenerContainer; - - @Autowired - protected FailureManager failureManager; - - @Autowired - protected RqueueMessageEnqueuer rqueueMessageEnqueuer; - - @Autowired - protected RqueueEventListener rqueueEventListener; + private static final RqueueMessageIdGenerator MESSAGE_ID_GENERATOR = + new UuidV4RqueueMessageIdGenerator(); + + @Autowired protected RqueueMessageTemplate rqueueMessageTemplate; + @Autowired protected RqueueConfig rqueueConfig; + @Autowired protected RqueueRedisTemplate stringRqueueRedisTemplate; + @Autowired protected ConsumedMessageStore consumedMessageStore; + @Autowired protected RqueueMessageListenerContainer rqueueMessageListenerContainer; + @Autowired protected FailureManager failureManager; + @Autowired protected RqueueMessageEnqueuer rqueueMessageEnqueuer; + @Autowired protected RqueueEventListener rqueueEventListener; @Autowired(required = false) protected ReactiveRqueueMessageEnqueuer reactiveRqueueMessageEnqueuer; @@ -158,8 +148,10 @@ public abstract class SpringTestBase extends TestBase { protected boolean reactiveEnabled; protected void enqueue(Object message, String queueName) { - RqueueMessage rqueueMessage = RqueueMessageUtils.buildMessage( - rqueueMessageManager.getMessageConverter(), queueName, null, message, null, null, null); + RqueueMessage rqueueMessage = + RqueueMessageUtils.buildMessage( + MESSAGE_ID_GENERATOR, + rqueueMessageManager.getMessageConverter(), queueName, null, message, null, null, null); rqueueMessageTemplate.addMessage(queueName, rqueueMessage); } @@ -167,14 +159,16 @@ protected void enqueue(String queueName, Factory factory, int n, boolean useMess for (int i = 0; i < n; i++) { Object message = factory.next(i); if (useMessageTemplate) { - RqueueMessage rqueueMessage = RqueueMessageUtils.buildMessage( - rqueueMessageManager.getMessageConverter(), - queueName, - null, - message, - null, - null, - rqueueMessageListenerContainer.getMessageHeaders()); + RqueueMessage rqueueMessage = + RqueueMessageUtils.buildMessage( + MESSAGE_ID_GENERATOR, + rqueueMessageManager.getMessageConverter(), + queueName, + null, + message, + null, + null, + rqueueMessageListenerContainer.getMessageHeaders()); rqueueMessageTemplate.addMessage(queueName, rqueueMessage); } else { enqueue(queueName, message); @@ -188,14 +182,16 @@ protected void enqueueIn( Object message = factory.next(i); long delay = delayFunc.getDelay(i); if (useMessageTemplate) { - RqueueMessage rqueueMessage = RqueueMessageUtils.buildMessage( - rqueueMessageManager.getMessageConverter(), - queueName, - null, - message, - null, - delay, - null); + RqueueMessage rqueueMessage = + RqueueMessageUtils.buildMessage( + MESSAGE_ID_GENERATOR, + rqueueMessageManager.getMessageConverter(), + queueName, + null, + message, + null, + delay, + null); rqueueMessageTemplate.addToZset(queueName, rqueueMessage, rqueueMessage.getProcessAt()); } else { enqueueIn(queueName, message, delay); @@ -204,8 +200,10 @@ protected void enqueueIn( } protected void enqueueIn(Object message, String zsetName, long delay) { - RqueueMessage rqueueMessage = RqueueMessageUtils.buildMessage( - rqueueMessageManager.getMessageConverter(), zsetName, null, message, null, delay, null); + RqueueMessage rqueueMessage = + RqueueMessageUtils.buildMessage( + MESSAGE_ID_GENERATOR, + rqueueMessageManager.getMessageConverter(), zsetName, null, message, null, delay, null); rqueueMessageTemplate.addToZset(zsetName, rqueueMessage, rqueueMessage.getProcessAt()); } diff --git a/rqueue-spring/src/main/java/com/github/sonus21/rqueue/spring/RqueueListenerConfig.java b/rqueue-spring/src/main/java/com/github/sonus21/rqueue/spring/RqueueListenerConfig.java index b4c36c7a..cb92a170 100644 --- a/rqueue-spring/src/main/java/com/github/sonus21/rqueue/spring/RqueueListenerConfig.java +++ b/rqueue-spring/src/main/java/com/github/sonus21/rqueue/spring/RqueueListenerConfig.java @@ -21,6 +21,7 @@ import com.github.sonus21.rqueue.core.ReactiveRqueueMessageEnqueuer; import com.github.sonus21.rqueue.core.RqueueEndpointManager; import com.github.sonus21.rqueue.core.RqueueMessageEnqueuer; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageManager; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.core.impl.ReactiveRqueueMessageEnqueuerImpl; @@ -66,29 +67,38 @@ public RqueueMessageTemplate rqueueMessageTemplate(RqueueConfig rqueueConfig) { @Bean public RqueueMessageManager rqueueMessageManager( - RqueueMessageHandler rqueueMessageHandler, RqueueMessageTemplate rqueueMessageTemplate) { + RqueueMessageHandler rqueueMessageHandler, + RqueueMessageTemplate rqueueMessageTemplate, + RqueueMessageIdGenerator rqueueMessageIdGenerator) { return new RqueueMessageManagerImpl( rqueueMessageTemplate, rqueueMessageHandler.getMessageConverter(), - simpleRqueueListenerContainerFactory.getMessageHeaders()); + simpleRqueueListenerContainerFactory.getMessageHeaders(), + rqueueMessageIdGenerator); } @Bean public RqueueEndpointManager rqueueEndpointManager( - RqueueMessageHandler rqueueMessageHandler, RqueueMessageTemplate rqueueMessageTemplate) { + RqueueMessageHandler rqueueMessageHandler, + RqueueMessageTemplate rqueueMessageTemplate, + RqueueMessageIdGenerator rqueueMessageIdGenerator) { return new RqueueEndpointManagerImpl( rqueueMessageTemplate, rqueueMessageHandler.getMessageConverter(), - simpleRqueueListenerContainerFactory.getMessageHeaders()); + simpleRqueueListenerContainerFactory.getMessageHeaders(), + rqueueMessageIdGenerator); } @Bean public RqueueMessageEnqueuer rqueueMessageEnqueuer( - RqueueMessageHandler rqueueMessageHandler, RqueueMessageTemplate rqueueMessageTemplate) { + RqueueMessageHandler rqueueMessageHandler, + RqueueMessageTemplate rqueueMessageTemplate, + RqueueMessageIdGenerator rqueueMessageIdGenerator) { return new RqueueMessageEnqueuerImpl( rqueueMessageTemplate, rqueueMessageHandler.getMessageConverter(), - simpleRqueueListenerContainerFactory.getMessageHeaders()); + simpleRqueueListenerContainerFactory.getMessageHeaders(), + rqueueMessageIdGenerator); } @Bean @@ -108,10 +118,13 @@ public RqueueMetricsCounter rqueueMetricsCounter(RqueueMetricsRegistry rqueueMet @Bean @Conditional(ReactiveEnabled.class) public ReactiveRqueueMessageEnqueuer reactiveRqueueMessageEnqueuer( - RqueueMessageHandler rqueueMessageHandler, RqueueMessageTemplate rqueueMessageTemplate) { + RqueueMessageHandler rqueueMessageHandler, + RqueueMessageTemplate rqueueMessageTemplate, + RqueueMessageIdGenerator rqueueMessageIdGenerator) { return new ReactiveRqueueMessageEnqueuerImpl( rqueueMessageTemplate, rqueueMessageHandler.getMessageConverter(), - simpleRqueueListenerContainerFactory.getMessageHeaders()); + simpleRqueueListenerContainerFactory.getMessageHeaders(), + rqueueMessageIdGenerator); } } diff --git a/rqueue-spring/src/test/java/com/github/sonus21/rqueue/spring/tests/unit/RqueueMessageConfigTest.java b/rqueue-spring/src/test/java/com/github/sonus21/rqueue/spring/tests/unit/RqueueMessageConfigTest.java index 804e3319..1dfd10c1 100644 --- a/rqueue-spring/src/test/java/com/github/sonus21/rqueue/spring/tests/unit/RqueueMessageConfigTest.java +++ b/rqueue-spring/src/test/java/com/github/sonus21/rqueue/spring/tests/unit/RqueueMessageConfigTest.java @@ -25,6 +25,7 @@ import com.github.sonus21.rqueue.converter.DefaultMessageConverterProvider; import com.github.sonus21.rqueue.converter.GenericMessageConverter; import com.github.sonus21.rqueue.core.DefaultRqueueMessageConverter; +import com.github.sonus21.rqueue.core.impl.UuidV4RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.listener.RqueueMessageHandler; import com.github.sonus21.rqueue.spring.RqueueListenerConfig; @@ -119,7 +120,11 @@ void rqueueMessageSenderWithMessageTemplate() throws IllegalAccessException { doReturn(new DefaultRqueueMessageConverter()).when(rqueueMessageHandler).getMessageConverter(); RqueueListenerConfig messageConfig = new RqueueListenerConfig(); FieldUtils.writeField(messageConfig, "simpleRqueueListenerContainerFactory", factory, true); - assertNotNull(messageConfig.rqueueMessageEnqueuer(rqueueMessageHandler, rqueueMessageTemplate)); + assertNotNull( + messageConfig.rqueueMessageEnqueuer( + rqueueMessageHandler, + rqueueMessageTemplate, + new UuidV4RqueueMessageIdGenerator())); assertEquals(factory.getRqueueMessageTemplate().hashCode(), rqueueMessageTemplate.hashCode()); } } From 9cb7a5bc659a35cbc0d0b4fc7c19439ea006ed0a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:17:34 +0000 Subject: [PATCH 2/5] Apply Palantir Java Format --- .../sonus21/rqueue/config/RqueueConfig.java | 1 + .../config/RqueueListenerBaseConfig.java | 4 +- .../rqueue/core/RqueueBeanProvider.java | 2 +- .../rqueue/core/impl/BaseMessageSender.java | 50 +++++----- .../ReactiveRqueueMessageEnqueuerImpl.java | 25 ++--- .../core/impl/RqueueEndpointManagerImpl.java | 6 +- .../core/impl/RqueueMessageEnqueuerImpl.java | 6 +- .../core/impl/RqueueMessageManagerImpl.java | 6 +- .../core/support/RqueueMessageUtils.java | 66 ++++--------- .../rqueue/listener/RqueueMessagePoller.java | 5 +- .../sonus21/rqueue/utils/DateTimeUtils.java | 36 +++---- .../MissingRqueueMessageIdGenerator.java | 3 +- .../ReactiveRqueueViewController.java | 2 +- .../web/controller/RqueueViewController.java | 2 +- .../web/service/RqueueQDetailService.java | 2 +- .../impl/RqueueQDetailServiceImpl.java | 11 +-- .../impl/RqueueUtilityServiceImpl.java | 5 +- .../impl/RqueueViewControllerServiceImpl.java | 59 ++++++------ .../rqueue/worker/RqueueWorkerRegistry.java | 3 +- .../worker/RqueueWorkerRegistryImpl.java | 86 +++++++++-------- .../core/RqueueMessageTemplateTest.java | 17 ++-- .../impl/RqueueMessageEnqueuerImplTest.java | 19 ++-- .../core/support/RqueueMessageUtilsTest.java | 76 +++++++-------- .../sonus21/rqueue/listener/JobImplTest.java | 5 +- .../rqueue/listener/RqueueExecutorTest.java | 21 ++-- .../rqueue/listener/RqueueMiddlewareTest.java | 59 ++++++------ .../rqueue/utils/DateTimeUtilsTest.java | 3 +- .../utils/MessageMetadataTestUtils.java | 9 +- .../web/service/RqueueQDetailServiceTest.java | 90 ++++++++++-------- .../web/service/RqueueUtilityServiceTest.java | 19 ++-- .../worker/RqueueWorkerRegistryImplTest.java | 76 +++++++-------- .../RqueueMessageTemplateTest.java | 24 +++-- .../unit/RqueueListenerAutoConfigTest.java | 15 ++- .../rqueue/test/common/SpringTestBase.java | 95 ++++++++++++------- .../tests/unit/RqueueMessageConfigTest.java | 9 +- 35 files changed, 456 insertions(+), 461 deletions(-) diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java index 6266b523..ea3dcef5 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueConfig.java @@ -45,6 +45,7 @@ public class RqueueConfig { @Getter private static final String brokerId = UUID.randomUUID().toString(); + private static final AtomicLong counter = new AtomicLong(1); private final RedisConnectionFactory connectionFactory; private final ReactiveRedisConnectionFactory reactiveRedisConnectionFactory; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java index 8fc13dd2..3f6ee035 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/config/RqueueListenerBaseConfig.java @@ -35,13 +35,13 @@ import com.github.sonus21.rqueue.dao.impl.RqueueStringDaoImpl; import com.github.sonus21.rqueue.listener.RqueueMessageListenerContainer; import com.github.sonus21.rqueue.metrics.RqueueQueueMetrics; -import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; -import com.github.sonus21.rqueue.worker.RqueueWorkerRegistryImpl; import com.github.sonus21.rqueue.utils.RedisUtils; import com.github.sonus21.rqueue.utils.condition.MissingRqueueMessageIdGenerator; import com.github.sonus21.rqueue.utils.condition.ReactiveEnabled; import com.github.sonus21.rqueue.utils.pebble.ResourceLoader; import com.github.sonus21.rqueue.utils.pebble.RqueuePebbleExtension; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistryImpl; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.spring.extension.SpringExtension; import io.pebbletemplates.spring.reactive.PebbleReactiveViewResolver; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueBeanProvider.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueBeanProvider.java index f0248a3f..cf95221e 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueBeanProvider.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/RqueueBeanProvider.java @@ -24,8 +24,8 @@ import com.github.sonus21.rqueue.dao.RqueueSystemConfigDao; import com.github.sonus21.rqueue.listener.RqueueMessageHandler; import com.github.sonus21.rqueue.metrics.RqueueMetricsCounter; -import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; import com.github.sonus21.rqueue.web.service.RqueueMessageMetadataService; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; import lombok.Getter; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/BaseMessageSender.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/BaseMessageSender.java index d7f499cc..acc4b5d0 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/BaseMessageSender.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/BaseMessageSender.java @@ -53,9 +53,15 @@ abstract class BaseMessageSender { protected final MessageConverter messageConverter; protected final RqueueMessageTemplate messageTemplate; protected final RqueueMessageIdGenerator messageIdGenerator; - @Autowired protected RqueueStringDao rqueueStringDao; - @Autowired protected RqueueConfig rqueueConfig; - @Autowired protected RqueueMessageMetadataService rqueueMessageMetadataService; + + @Autowired + protected RqueueStringDao rqueueStringDao; + + @Autowired + protected RqueueConfig rqueueConfig; + + @Autowired + protected RqueueMessageMetadataService rqueueMessageMetadataService; BaseMessageSender( RqueueMessageTemplate messageTemplate, @@ -118,16 +124,15 @@ protected String pushMessage( Long delayInMilliSecs, boolean isUnique) { QueueDetail queueDetail = EndpointRegistry.get(queueName); - RqueueMessage rqueueMessage = - buildMessage( - messageIdGenerator, - messageConverter, - queueName, - messageId, - message, - retryCount, - delayInMilliSecs, - messageHeaders); + RqueueMessage rqueueMessage = buildMessage( + messageIdGenerator, + messageConverter, + queueName, + messageId, + message, + retryCount, + delayInMilliSecs, + messageHeaders); try { storeMessageMetadata(rqueueMessage, delayInMilliSecs, false, isUnique); enqueue(queueDetail, rqueueMessage, delayInMilliSecs, false); @@ -147,16 +152,15 @@ protected String pushMessage( protected String pushPeriodicMessage( String queueName, String messageId, Object message, long periodInMilliSeconds) { QueueDetail queueDetail = EndpointRegistry.get(queueName); - RqueueMessage rqueueMessage = - buildPeriodicMessage( - messageIdGenerator, - messageConverter, - queueName, - messageId, - message, - null, - periodInMilliSeconds, - messageHeaders); + RqueueMessage rqueueMessage = buildPeriodicMessage( + messageIdGenerator, + messageConverter, + queueName, + messageId, + message, + null, + periodInMilliSeconds, + messageHeaders); try { storeMessageMetadata(rqueueMessage, periodInMilliSeconds, false, false); enqueue(queueDetail, rqueueMessage, periodInMilliSeconds, false); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/ReactiveRqueueMessageEnqueuerImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/ReactiveRqueueMessageEnqueuerImpl.java index 26be7dbd..6c138b8a 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/ReactiveRqueueMessageEnqueuerImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/ReactiveRqueueMessageEnqueuerImpl.java @@ -42,11 +42,7 @@ public ReactiveRqueueMessageEnqueuerImpl( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, MessageHeaders messageHeaders) { - this( - messageTemplate, - messageConverter, - messageHeaders, - new UuidV4RqueueMessageIdGenerator()); + this(messageTemplate, messageConverter, messageHeaders, new UuidV4RqueueMessageIdGenerator()); } public ReactiveRqueueMessageEnqueuerImpl( @@ -68,16 +64,15 @@ private Mono pushReactiveMessage( boolean isUnique, Function> monoConverter) { QueueDetail queueDetail = EndpointRegistry.get(queueName); - RqueueMessage rqueueMessage = - builder.build( - messageIdGenerator, - messageConverter, - queueName, - messageId, - message, - retryCount, - delayInMilliSecs, - messageHeaders); + RqueueMessage rqueueMessage = builder.build( + messageIdGenerator, + messageConverter, + queueName, + messageId, + message, + retryCount, + delayInMilliSecs, + messageHeaders); try { Mono storeResult = (Mono) storeMessageMetadata(rqueueMessage, delayInMilliSecs, true, isUnique); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueEndpointManagerImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueEndpointManagerImpl.java index e2402447..907dbe8f 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueEndpointManagerImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueEndpointManagerImpl.java @@ -52,11 +52,7 @@ public RqueueEndpointManagerImpl( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, MessageHeaders messageHeaders) { - this( - messageTemplate, - messageConverter, - messageHeaders, - new UuidV4RqueueMessageIdGenerator()); + this(messageTemplate, messageConverter, messageHeaders, new UuidV4RqueueMessageIdGenerator()); } public RqueueEndpointManagerImpl( diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImpl.java index af0bf04a..6e17fa5e 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImpl.java @@ -40,11 +40,7 @@ public RqueueMessageEnqueuerImpl( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, MessageHeaders messageHeaders) { - this( - messageTemplate, - messageConverter, - messageHeaders, - new UuidV4RqueueMessageIdGenerator()); + this(messageTemplate, messageConverter, messageHeaders, new UuidV4RqueueMessageIdGenerator()); } public RqueueMessageEnqueuerImpl( diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageManagerImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageManagerImpl.java index 914a3983..932f363c 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageManagerImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/impl/RqueueMessageManagerImpl.java @@ -51,11 +51,7 @@ public RqueueMessageManagerImpl( RqueueMessageTemplate messageTemplate, MessageConverter messageConverter, MessageHeaders messageHeaders) { - this( - messageTemplate, - messageConverter, - messageHeaders, - new UuidV4RqueueMessageIdGenerator()); + this(messageTemplate, messageConverter, messageHeaders, new UuidV4RqueueMessageIdGenerator()); } public RqueueMessageManagerImpl( diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtils.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtils.java index 2ea3a7f9..1d1bf9df 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtils.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtils.java @@ -72,15 +72,14 @@ public static RqueueMessage buildPeriodicMessage( } else { throw new MessageConversionException("Message payload is neither String nor byte[]"); } - RqueueMessage rqueueMessage = - RqueueMessage.builder() - .id(messageIdGenerator.generate()) - .queueName(queueName) - .message(strMessage) - .processAt(processAt) - .retryCount(retryCount) - .period(period) - .build(); + RqueueMessage rqueueMessage = RqueueMessage.builder() + .id(messageIdGenerator.generate()) + .queueName(queueName) + .message(strMessage) + .processAt(processAt) + .retryCount(retryCount) + .period(period) + .build(); if (messageId != null) { rqueueMessage.setId(messageId); } @@ -114,15 +113,14 @@ public static RqueueMessage buildMessage( } else { throw new MessageConversionException("Message payload is neither String nor byte[]"); } - RqueueMessage rqueueMessage = - RqueueMessage.builder() - .retryCount(retryCount) - .queuedTime(queuedTime) - .id(messageIdGenerator.generate()) - .queueName(queueName) - .message(strMessage) - .processAt(processAt) - .build(); + RqueueMessage rqueueMessage = RqueueMessage.builder() + .retryCount(retryCount) + .queuedTime(queuedTime) + .id(messageIdGenerator.generate()) + .queueName(queueName) + .message(strMessage) + .processAt(processAt) + .build(); if (messageId != null) { rqueueMessage.setId(messageId); } @@ -135,27 +133,13 @@ public static List generateMessages( String queueName, int count) { return generateMessages( - messageIdGenerator, - converter, - messageIdGenerator.generate(), - queueName, - null, - null, - count); + messageIdGenerator, converter, messageIdGenerator.generate(), queueName, null, null, count); } public static RqueueMessage generateMessage( - RqueueMessageIdGenerator messageIdGenerator, - MessageConverter converter, - String queueName) { + RqueueMessageIdGenerator messageIdGenerator, MessageConverter converter, String queueName) { return generateMessages( - messageIdGenerator, - converter, - messageIdGenerator.generate(), - queueName, - null, - null, - 1) + messageIdGenerator, converter, messageIdGenerator.generate(), queueName, null, null, 1) .get(0); } @@ -185,16 +169,8 @@ public static List generateMessages( int count) { List messages = new ArrayList<>(); for (int i = 0; i < count; i++) { - messages.add( - buildMessage( - messageIdGenerator, - converter, - queueName, - null, - object, - retryCount, - delay, - null)); + messages.add(buildMessage( + messageIdGenerator, converter, queueName, null, object, retryCount, delay, null)); } return messages; } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/RqueueMessagePoller.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/RqueueMessagePoller.java index f2ead0f2..c6b483ea 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/RqueueMessagePoller.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/listener/RqueueMessagePoller.java @@ -105,8 +105,9 @@ protected boolean hasAvailableThreads(QueueDetail queueDetail, QueueThreadPool q protected void recordCapacityExhausted(QueueDetail queueDetail, QueueThreadPool queueThreadPool) { if (rqueueBeanProvider.getRqueueWorkerRegistry() != null) { - rqueueBeanProvider.getRqueueWorkerRegistry().recordQueueCapacityExhausted( - queueDetail, queueThreadPool); + rqueueBeanProvider + .getRqueueWorkerRegistry() + .recordQueueCapacityExhausted(queueDetail, queueThreadPool); } } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/DateTimeUtils.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/DateTimeUtils.java index 4a23f17c..a40f892f 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/DateTimeUtils.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/DateTimeUtils.java @@ -29,10 +29,10 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class DateTimeUtils { - private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern( - "yyyy-MM-dd HH:mm"); - private static final DateTimeFormatter dateTimeFormatterWithSecond = DateTimeFormatter.ofPattern( - "yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter dateTimeFormatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + private static final DateTimeFormatter dateTimeFormatterWithSecond = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter readableDateTimeFormatter = DateTimeFormatter.ofPattern("dd MMM, yyyy 'at' hh:mm a", Locale.ENGLISH); @@ -151,30 +151,30 @@ public static String formatMilliToCompactDuration(Long milli) { long seconds = absMillis / Constants.ONE_MILLI; return prefix + seconds + " sec"; } - if (absMillis < Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI + if (absMillis + < Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI && absMillis % (Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI) == 0) { long minutes = absMillis / (Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI); return prefix + minutes + " min"; } - if (absMillis < Constants.HOURS_IN_A_DAY - * Constants.MINUTES_IN_AN_HOUR - * Constants.SECONDS_IN_A_MINUTE - * Constants.ONE_MILLI + if (absMillis + < Constants.HOURS_IN_A_DAY + * Constants.MINUTES_IN_AN_HOUR + * Constants.SECONDS_IN_A_MINUTE + * Constants.ONE_MILLI && absMillis - % (Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI) + % (Constants.MINUTES_IN_AN_HOUR + * Constants.SECONDS_IN_A_MINUTE + * Constants.ONE_MILLI) == 0) { - long hours = - absMillis - / (Constants.MINUTES_IN_AN_HOUR - * Constants.SECONDS_IN_A_MINUTE - * Constants.ONE_MILLI); + long hours = absMillis + / (Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE * Constants.ONE_MILLI); return prefix + hours + " hr"; } long totalSeconds = absMillis / Constants.ONE_MILLI; long hours = totalSeconds / (Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE); - long minutes = - (totalSeconds % (Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE)) - / Constants.SECONDS_IN_A_MINUTE; + long minutes = (totalSeconds % (Constants.MINUTES_IN_AN_HOUR * Constants.SECONDS_IN_A_MINUTE)) + / Constants.SECONDS_IN_A_MINUTE; long seconds = totalSeconds % Constants.SECONDS_IN_A_MINUTE; if (hours > 0) { if (minutes > 0) { diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/condition/MissingRqueueMessageIdGenerator.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/condition/MissingRqueueMessageIdGenerator.java index 3700b125..5c587aed 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/condition/MissingRqueueMessageIdGenerator.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/utils/condition/MissingRqueueMessageIdGenerator.java @@ -30,7 +30,8 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) return true; } ListableBeanFactory beanFactory = (ListableBeanFactory) context.getBeanFactory(); - String[] beanNames = beanFactory.getBeanNamesForType(RqueueMessageIdGenerator.class, false, false); + String[] beanNames = + beanFactory.getBeanNamesForType(RqueueMessageIdGenerator.class, false, false); return beanNames.length == 0; } } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueViewController.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueViewController.java index 88370a91..c22c3692 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueViewController.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/ReactiveRqueueViewController.java @@ -29,8 +29,8 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.reactive.result.view.View; import org.springframework.web.reactive.result.view.ViewResolver; import reactor.core.publisher.Mono; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueViewController.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueViewController.java index 4f495c8e..6e4dafd1 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueViewController.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/controller/RqueueViewController.java @@ -29,8 +29,8 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueQDetailService.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueQDetailService.java index 580ff40d..3153dbef 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueQDetailService.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/RqueueQDetailService.java @@ -19,9 +19,9 @@ import com.github.sonus21.rqueue.models.db.QueueConfig; import com.github.sonus21.rqueue.models.enums.DataType; import com.github.sonus21.rqueue.models.enums.NavTab; +import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView; import com.github.sonus21.rqueue.models.response.DataViewResponse; import com.github.sonus21.rqueue.models.response.RedisDataDetail; -import com.github.sonus21.rqueue.models.registry.RqueueWorkerPollerView; import java.util.List; import java.util.Map; import java.util.Map.Entry; diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueQDetailServiceImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueQDetailServiceImpl.java index 5249bdad..be1e044d 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueQDetailServiceImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueQDetailServiceImpl.java @@ -597,12 +597,11 @@ public TableRow row(RqueueMessage rqueueMessage, boolean deleted, Double score) } if (!completionQueue) { if (!deleted) { - row.addColumn( - new TableColumn( - TableColumnType.ACTION, - scheduledQueue && !rqueueMessage.isPeriodic() - ? ActionType.ENQUEUE - : ActionType.DELETE)); + row.addColumn(new TableColumn( + TableColumnType.ACTION, + scheduledQueue && !rqueueMessage.isPeriodic() + ? ActionType.ENQUEUE + : ActionType.DELETE)); } else { row.addColumn(new TableColumn(Constants.BLANK)); } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueUtilityServiceImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueUtilityServiceImpl.java index e1cd7641..ccd770e7 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueUtilityServiceImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueUtilityServiceImpl.java @@ -123,9 +123,8 @@ public BooleanResponse enqueueMessage(String queueName, String id, String positi return booleanResponse; } RqueueMessage rqueueMessage = messageMetadata.getRqueueMessage(); - Long removed = - rqueueMessageTemplate.removeElementFromZset( - queueConfig.getScheduledQueueName(), rqueueMessage); + Long removed = rqueueMessageTemplate.removeElementFromZset( + queueConfig.getScheduledQueueName(), rqueueMessage); if (removed == null || removed == 0) { booleanResponse.setCode(1); booleanResponse.setMessage("Message is not available in the scheduled queue."); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueViewControllerServiceImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueViewControllerServiceImpl.java index b72e1a4b..ac8b59df 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueViewControllerServiceImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/web/service/impl/RqueueViewControllerServiceImpl.java @@ -39,7 +39,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.ui.Model; @@ -144,16 +143,15 @@ public void workers(Model model, String xForwardedPrefix, int pageNumber) { List queueWorkers = rqueueQDetailService.getQueueWorkers(queueConfig.getName()); for (RqueueWorkerPollerView queueWorker : queueWorkers) { - RqueueWorkerView workerView = - workerIdToView.getOrDefault( - queueWorker.getWorkerId(), - RqueueWorkerView.builder() - .workerId(queueWorker.getWorkerId()) - .host(queueWorker.getHost()) - .pid(queueWorker.getPid()) - .lastPollAt(queueWorker.getLastPollAt()) - .lastPollAge(queueWorker.getLastPollAge()) - .build()); + RqueueWorkerView workerView = workerIdToView.getOrDefault( + queueWorker.getWorkerId(), + RqueueWorkerView.builder() + .workerId(queueWorker.getWorkerId()) + .host(queueWorker.getHost()) + .pid(queueWorker.getPid()) + .lastPollAt(queueWorker.getLastPollAt()) + .lastPollAge(queueWorker.getLastPollAge()) + .build()); workerView.setHost(queueWorker.getHost()); workerView.setPid(queueWorker.getPid()); if (queueWorker.getLastPollAt() > workerView.getLastPollAt()) { @@ -170,21 +168,20 @@ public void workers(Model model, String xForwardedPrefix, int pageNumber) { } } List allWorkers = new ArrayList<>(workerIdToView.values()); - allWorkers.forEach( - e -> { - e.getPollers().sort(Comparator.comparing(RqueueWorkerPollerView::getQueue)); - int recentCapacityExhaustedQueues = 0; - long recentThreshold = - 2L * rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); - for (RqueueWorkerPollerView poller : e.getPollers()) { - Long lastCapacityExhaustedAt = poller.getLastCapacityExhaustedAt(); - if (lastCapacityExhaustedAt != null - && System.currentTimeMillis() - lastCapacityExhaustedAt <= recentThreshold) { - recentCapacityExhaustedQueues += 1; - } - } - e.setRecentCapacityExhaustedQueues(recentCapacityExhaustedQueues); - }); + allWorkers.forEach(e -> { + e.getPollers().sort(Comparator.comparing(RqueueWorkerPollerView::getQueue)); + int recentCapacityExhaustedQueues = 0; + long recentThreshold = + 2L * rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); + for (RqueueWorkerPollerView poller : e.getPollers()) { + Long lastCapacityExhaustedAt = poller.getLastCapacityExhaustedAt(); + if (lastCapacityExhaustedAt != null + && System.currentTimeMillis() - lastCapacityExhaustedAt <= recentThreshold) { + recentCapacityExhaustedQueues += 1; + } + } + e.setRecentCapacityExhaustedQueues(recentCapacityExhaustedQueues); + }); allWorkers.sort(Comparator.comparingLong(RqueueWorkerView::getLastPollAt).reversed()); int pageSize = Math.max(1, rqueueWebConfig.getWorkerPageSize()); int totalPages = Math.max(1, (allWorkers.size() + pageSize - 1) / pageSize); @@ -227,15 +224,13 @@ public void queueDetail(Model model, String xForwardedPrefix, String queueName) model.addAttribute( "staleQueueWorkers", queueWorkers.stream().filter(e -> "STALE".equals(e.getStatus())).count()); - long recentThreshold = 2L * rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); + long recentThreshold = + 2L * rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); model.addAttribute( "queueWorkerRecentCapacityExhausted", queueWorkers.stream() - .filter( - e -> - e.getLastCapacityExhaustedAt() != null - && System.currentTimeMillis() - e.getLastCapacityExhaustedAt() - <= recentThreshold) + .filter(e -> e.getLastCapacityExhaustedAt() != null + && System.currentTimeMillis() - e.getLastCapacityExhaustedAt() <= recentThreshold) .count()); } diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistry.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistry.java index e59dab26..8cb168a2 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistry.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistry.java @@ -23,7 +23,8 @@ public interface RqueueWorkerRegistry { - void recordQueuePoll(QueueDetail queueDetail, QueueThreadPool queueThreadPool, boolean messageReceived); + void recordQueuePoll( + QueueDetail queueDetail, QueueThreadPool queueThreadPool, boolean messageReceived); void recordQueueCapacityExhausted(QueueDetail queueDetail, QueueThreadPool queueThreadPool); diff --git a/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImpl.java b/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImpl.java index 0fc9b4db..70a9de9b 100644 --- a/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImpl.java +++ b/rqueue-core/src/main/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImpl.java @@ -104,7 +104,8 @@ public void recordQueuePoll( } @Override - public void recordQueueCapacityExhausted(QueueDetail queueDetail, QueueThreadPool queueThreadPool) { + public void recordQueueCapacityExhausted( + QueueDetail queueDetail, QueueThreadPool queueThreadPool) { if (!rqueueConfig.isWorkerRegistryEnabled()) { return; } @@ -112,17 +113,15 @@ public void recordQueueCapacityExhausted(QueueDetail queueDetail, QueueThreadPoo long now = System.currentTimeMillis(); refreshWorkerInfoIfRequired(now); lastCapacityExhaustedAtByQueue.put(registryQueueName, now); - capacityExhaustedCountByQueue.compute( - registryQueueName, - (key, count) -> { - if (count == null) { - return 1L; - } - if (count == Long.MAX_VALUE) { - return Long.MAX_VALUE; - } - return count + 1L; - }); + capacityExhaustedCountByQueue.compute(registryQueueName, (key, count) -> { + if (count == null) { + return 1L; + } + if (count == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + return count + 1L; + }); if (!queueHeartbeatRequired(registryQueueName, now)) { return; } @@ -162,7 +161,8 @@ public List getQueueWorkers(String queueName) { continue; } // Lazy cleanup for entries that are far older than the queue hash retention window. - if (now - metadata.getLastPollAt() > rqueueConfig.getWorkerRegistryQueueTtl().toMillis()) { + if (now - metadata.getLastPollAt() + > rqueueConfig.getWorkerRegistryQueueTtl().toMillis()) { toDelete.add(entry.getKey()); continue; } @@ -184,24 +184,27 @@ public List getQueueWorkers(String queueName) { String workerId = entry.getKey(); RqueueWorkerPollerMetadata metadata = entry.getValue(); RqueueWorkerInfo workerInfo = workerInfoById.get(workerId); - long lastActivityAt = Math.max(metadata.getLastPollAt(), - metadata.getLastCapacityExhaustedAt() == null ? 0 : metadata.getLastCapacityExhaustedAt()); + long lastActivityAt = Math.max( + metadata.getLastPollAt(), + metadata.getLastCapacityExhaustedAt() == null + ? 0 + : metadata.getLastCapacityExhaustedAt()); boolean stale = now - lastActivityAt > staleAfter || workerInfo == null; - rows.add( - RqueueWorkerPollerView.builder() - .queue(queueName) - .workerId(workerId) - .host(workerInfo == null ? "unknown" : workerInfo.getHost()) - .pid(workerInfo == null ? "" : workerInfo.getPid()) - .status(stale ? "STALE" : "ACTIVE") - .lastPollAt(metadata.getLastPollAt()) - .lastPollAge(formatAge(now, metadata.getLastPollAt() == 0 ? null : metadata.getLastPollAt())) - .lastMessageAt(metadata.getLastMessageAt()) - .lastMessageAge(formatAge(now, metadata.getLastMessageAt())) - .lastCapacityExhaustedAt(metadata.getLastCapacityExhaustedAt()) - .lastCapacityExhaustedAge(formatAge(now, metadata.getLastCapacityExhaustedAt())) - .capacityExhaustedCount(metadata.getCapacityExhaustedCount()) - .build()); + rows.add(RqueueWorkerPollerView.builder() + .queue(queueName) + .workerId(workerId) + .host(workerInfo == null ? "unknown" : workerInfo.getHost()) + .pid(workerInfo == null ? "" : workerInfo.getPid()) + .status(stale ? "STALE" : "ACTIVE") + .lastPollAt(metadata.getLastPollAt()) + .lastPollAge( + formatAge(now, metadata.getLastPollAt() == 0 ? null : metadata.getLastPollAt())) + .lastMessageAt(metadata.getLastMessageAt()) + .lastMessageAge(formatAge(now, metadata.getLastMessageAt())) + .lastCapacityExhaustedAt(metadata.getLastCapacityExhaustedAt()) + .lastCapacityExhaustedAge(formatAge(now, metadata.getLastCapacityExhaustedAt())) + .capacityExhaustedCount(metadata.getCapacityExhaustedCount()) + .build()); } rows.sort(Comparator.comparingLong(RqueueWorkerPollerView::getLastPollAt).reversed()); return rows; @@ -220,22 +223,22 @@ public void onApplicationEvent(RqueueBootstrapEvent event) { } private void refreshWorkerInfoIfRequired(long now) { - if (now - lastWorkerHeartbeatAt < rqueueConfig.getWorkerRegistryWorkerHeartbeatInterval().toMillis()) { + if (now - lastWorkerHeartbeatAt + < rqueueConfig.getWorkerRegistryWorkerHeartbeatInterval().toMillis()) { return; } refreshWorkerInfo(now); } private void refreshWorkerInfo(long now) { - RqueueWorkerInfo workerInfo = - RqueueWorkerInfo.builder() - .workerId(workerId) - .host(host) - .pid(pid) - .version(rqueueConfig.getLibVersion()) - .startedAt(startedAt) - .lastSeenAt(now) - .build(); + RqueueWorkerInfo workerInfo = RqueueWorkerInfo.builder() + .workerId(workerId) + .host(host) + .pid(pid) + .version(rqueueConfig.getLibVersion()) + .startedAt(startedAt) + .lastSeenAt(now) + .build(); workerTemplate.set( rqueueConfig.getWorkerRegistryKey(workerId), workerInfo, @@ -248,7 +251,8 @@ private boolean queueHeartbeatRequired(String queueName, long now) { if (lastHeartbeat == null) { return true; } - return now - lastHeartbeat >= rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); + return now - lastHeartbeat + >= rqueueConfig.getWorkerRegistryQueueHeartbeatInterval().toMillis(); } private void refreshQueueTtlIfRequired(String queueName, long now) { diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java index 106bf32f..08d8db1f 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java @@ -64,15 +64,14 @@ class RqueueMessageTemplateTest extends TestBase { @BeforeEach public void init() throws Exception { MockitoAnnotations.openMocks(this); - redisTemplate = - new RedisTemplate<>() { - @Override - public ListOperations opsForList() { - return listOperations; - } - }; - rqueueMessageTemplate = TestUtils.rqueueMessageTemplate(redisConnectionFactory, redisTemplate, - scriptExecutor); + redisTemplate = new RedisTemplate<>() { + @Override + public ListOperations opsForList() { + return listOperations; + } + }; + rqueueMessageTemplate = + TestUtils.rqueueMessageTemplate(redisConnectionFactory, redisTemplate, scriptExecutor); } @Test diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImplTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImplTest.java index 4ee6f830..0c817570 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImplTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/impl/RqueueMessageEnqueuerImplTest.java @@ -27,9 +27,9 @@ import com.github.sonus21.rqueue.CoreUnitTest; import com.github.sonus21.rqueue.config.RqueueConfig; import com.github.sonus21.rqueue.core.DefaultRqueueMessageConverter; -import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.EndpointRegistry; import com.github.sonus21.rqueue.core.RqueueMessageEnqueuer; +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.listener.QueueDetail; import com.github.sonus21.rqueue.listener.RqueueMessageHeaders; @@ -63,6 +63,7 @@ class RqueueMessageEnqueuerImplTest extends TestBase { @Mock private RqueueMessageTemplate messageTemplate; + private RqueueConfig rqueueConfig; private RqueueMessageEnqueuer rqueueMessageEnqueuer; @@ -83,12 +84,11 @@ public void init() throws IllegalAccessException { MockitoAnnotations.openMocks(this); rqueueConfig = new RqueueConfig(null, null, true, 2); rqueueConfig.setMessageDurabilityInMinute(10080); - rqueueMessageEnqueuer = - new RqueueMessageEnqueuerImpl( - messageTemplate, messageConverter, messageHeaders, FIXED_MESSAGE_ID_GENERATOR); + rqueueMessageEnqueuer = new RqueueMessageEnqueuerImpl( + messageTemplate, messageConverter, messageHeaders, FIXED_MESSAGE_ID_GENERATOR); FieldUtils.writeField(rqueueMessageEnqueuer, "rqueueConfig", rqueueConfig, true); - FieldUtils.writeField(rqueueMessageEnqueuer, "rqueueMessageMetadataService", - rqueueMessageMetadataService, true); + FieldUtils.writeField( + rqueueMessageEnqueuer, "rqueueMessageMetadataService", rqueueMessageMetadataService, true); doNothing().when(rqueueMessageMetadataService).save(any(), any(), anyBoolean()); } @@ -156,9 +156,10 @@ void enqueuePeriodic() { void enqueueUsesInjectedMessageIdGenerator() { rqueueMessageEnqueuer.enqueue(queue, "test-message"); - ArgumentCaptor messageMetadataCaptor = ArgumentCaptor.forClass( - MessageMetadata.class); + ArgumentCaptor messageMetadataCaptor = + ArgumentCaptor.forClass(MessageMetadata.class); verify(rqueueMessageMetadataService).save(messageMetadataCaptor.capture(), any(), anyBoolean()); - assertEquals("custom-id", messageMetadataCaptor.getValue().getRqueueMessage().getId()); + assertEquals( + "custom-id", messageMetadataCaptor.getValue().getRqueueMessage().getId()); } } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtilsTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtilsTest.java index f5a295d0..8807a8be 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtilsTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/support/RqueueMessageUtilsTest.java @@ -56,16 +56,15 @@ class RqueueMessageUtilsTest extends TestBase { void buildPeriodicMessage() { Email email = Email.newInstance(); long startTime = System.currentTimeMillis(); - RqueueMessage message = - RqueueMessageUtils.buildPeriodicMessage( - FIXED_MESSAGE_ID_GENERATOR, - messageConverter, - queue, - null, - email, - null, - 10_000L, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = RqueueMessageUtils.buildPeriodicMessage( + FIXED_MESSAGE_ID_GENERATOR, + messageConverter, + queue, + null, + email, + null, + 10_000L, + RqueueMessageHeaders.emptyMessageHeaders()); assertEquals(10_000L, message.getPeriod()); long now = System.currentTimeMillis(); assertTrue( @@ -85,16 +84,15 @@ void buildMessage() { Email email = Email.newInstance(); long startTime = System.currentTimeMillis(); long startTimeInNano = System.nanoTime(); - RqueueMessage message = - RqueueMessageUtils.buildMessage( - FIXED_MESSAGE_ID_GENERATOR, - messageConverter, - queue, - null, - email, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = RqueueMessageUtils.buildMessage( + FIXED_MESSAGE_ID_GENERATOR, + messageConverter, + queue, + null, + email, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); assertEquals(0, message.getPeriod()); long now = System.currentTimeMillis(); long nowNano = System.nanoTime(); @@ -115,16 +113,15 @@ void buildMessage() { void buildMessageUsesProvidedMessageIdGenerator() { Email email = Email.newInstance(); - RqueueMessage message = - RqueueMessageUtils.buildMessage( - FIXED_MESSAGE_ID_GENERATOR, - messageConverter, - queue, - null, - email, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = RqueueMessageUtils.buildMessage( + FIXED_MESSAGE_ID_GENERATOR, + messageConverter, + queue, + null, + email, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); assertEquals("fixed-id", message.getId()); } @@ -134,16 +131,15 @@ void buildMessageWithDelay() { Email email = Email.newInstance(); long startTime = System.currentTimeMillis(); long startTimeInNano = System.nanoTime(); - RqueueMessage message = - RqueueMessageUtils.buildMessage( - FIXED_MESSAGE_ID_GENERATOR, - messageConverter, - queue, - null, - email, - 3, - 10_000L, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = RqueueMessageUtils.buildMessage( + FIXED_MESSAGE_ID_GENERATOR, + messageConverter, + queue, + null, + email, + 3, + 10_000L, + RqueueMessageHeaders.emptyMessageHeaders()); assertEquals(0, message.getPeriod()); long now = System.currentTimeMillis(); long nowNano = System.nanoTime(); diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/JobImplTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/JobImplTest.java index 05888f6f..0bdf16d6 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/JobImplTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/JobImplTest.java @@ -68,9 +68,8 @@ class JobImplTest extends TestBase { private final QueueDetail queueDetail = TestUtils.createQueueDetail("test-queue"); private final MessageConverter messageConverter = new DefaultRqueueMessageConverter(); - private final RqueueMessage rqueueMessage = - RqueueMessageUtils.generateMessage( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, queueDetail.getName()); + private final RqueueMessage rqueueMessage = RqueueMessageUtils.generateMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, queueDetail.getName()); private final MessageMetadata messageMetadata = new MessageMetadata(rqueueMessage, MessageStatus.PROCESSING); private final Object userMessage = "Test Object"; diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueExecutorTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueExecutorTest.java index 022f6813..046cf4d0 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueExecutorTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueExecutorTest.java @@ -43,9 +43,9 @@ import com.github.sonus21.rqueue.listener.RqueueMessageListenerContainer.QueueStateMgr; import com.github.sonus21.rqueue.models.db.MessageMetadata; import com.github.sonus21.rqueue.models.enums.MessageStatus; -import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; import com.github.sonus21.rqueue.utils.Constants; import com.github.sonus21.rqueue.utils.QueueThreadPool; +import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; import com.github.sonus21.rqueue.utils.TestUtils; import com.github.sonus21.rqueue.utils.backoff.FixedTaskExecutionBackOff; import com.github.sonus21.rqueue.utils.backoff.TaskExecutionBackOff; @@ -115,16 +115,15 @@ class RqueueExecutorTest extends TestBase { public void init() throws IllegalAccessException { MockitoAnnotations.openMocks(this); MessageConverter messageConverter = new GenericMessageConverter(); - rqueueMessage = - RqueueMessageUtils.buildMessage( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, - messageConverter, - queueName, - null, - payload, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + rqueueMessage = RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + queueName, + null, + payload, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); defaultMessageMetadata = new MessageMetadata(rqueueMessage, MessageStatus.ENQUEUED); MessageProcessorHandler messageProcessorHandler = new MessageProcessorHandler(null, deadLetterProcessor, discardProcessor, null); diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java index f0b280c1..ce5e0487 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java @@ -52,9 +52,9 @@ import com.github.sonus21.rqueue.listener.RqueueMessageListenerContainer.QueueStateMgr; import com.github.sonus21.rqueue.models.db.MessageMetadata; import com.github.sonus21.rqueue.models.enums.MessageStatus; -import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; import com.github.sonus21.rqueue.utils.Constants; import com.github.sonus21.rqueue.utils.QueueThreadPool; +import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; import com.github.sonus21.rqueue.utils.TestUtils; import com.github.sonus21.rqueue.utils.TimeoutUtils; import com.github.sonus21.rqueue.utils.backoff.FixedTaskExecutionBackOff; @@ -126,16 +126,15 @@ class RqueueMiddlewareTest extends TestBase { @BeforeEach public void init() throws IllegalAccessException { MockitoAnnotations.openMocks(this); - rqueueMessage = - RqueueMessageUtils.buildMessage( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, - messageConverter, - queueName, - null, - payload, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + rqueueMessage = RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + queueName, + null, + payload, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); defaultMessageMetadata = new MessageMetadata(rqueueMessage, MessageStatus.ENQUEUED); MessageProcessorHandler messageProcessorHandler = new MessageProcessorHandler(null, job -> true, job -> true, null); @@ -235,16 +234,15 @@ void logContextAndPermissionMiddleware() { .when(rqueueMessageMetadataService) .get(defaultMessageMetadata.getId()); QueueDetail queueDetail = TestUtils.createQueueDetail(queueName); - RqueueMessage rqueueMessage1 = - RqueueMessageUtils.buildMessage( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, - messageConverter, - queueName, - null, - payload, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage rqueueMessage1 = RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + queueName, + null, + payload, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); permissionMiddleware.allowedMessageIds.add(rqueueMessage.getId()); MessageMetadata messageMetadata = new MessageMetadata(rqueueMessage1, MessageStatus.ENQUEUED); doAnswer(invocation -> { @@ -299,16 +297,15 @@ void logAndRateLimiterMiddleware() throws TimedOutException { List messages = new ArrayList<>(); int jobCount = 100; for (int i = 0; i < jobCount; i++) { - RqueueMessage message = - RqueueMessageUtils.buildMessage( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, - messageConverter, - queueName, - null, - payload, - null, - null, - RqueueMessageHeaders.emptyMessageHeaders()); + RqueueMessage message = RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + queueName, + null, + payload, + null, + null, + RqueueMessageHeaders.emptyMessageHeaders()); MessageMetadata messageMetadata = new MessageMetadata(message, MessageStatus.ENQUEUED); messages.add(message); map.put(message.getId(), messageMetadata); diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/DateTimeUtilsTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/DateTimeUtilsTest.java index b3f12576..54a2db20 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/DateTimeUtilsTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/DateTimeUtilsTest.java @@ -88,7 +88,8 @@ void formatMilliToString() { @Test void formatMilliToReadableString() { - assertEquals("26 Apr, 1970 at 05:46 PM", DateTimeUtils.formatMilliToReadableString(10000000000L)); + assertEquals( + "26 Apr, 1970 at 05:46 PM", DateTimeUtils.formatMilliToReadableString(10000000000L)); assertEquals("", DateTimeUtils.formatMilliToReadableString(null)); } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/MessageMetadataTestUtils.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/MessageMetadataTestUtils.java index 5533f3aa..afaa5320 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/MessageMetadataTestUtils.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/MessageMetadataTestUtils.java @@ -36,8 +36,13 @@ public static MessageMetadata createMessageMetadata( public static MessageMetadata createMessageMetadata( MessageConverter messageConverter, String queue, Object message) { RqueueMessage rqueueMessage = RqueueMessageUtils.generateMessages( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, message, queue, null, - null, 1) + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + message, + queue, + null, + null, + 1) .get(0); return new MessageMetadata(rqueueMessage, MessageStatus.ENQUEUED); } diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueQDetailServiceTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueQDetailServiceTest.java index b5d7d19c..9521b86c 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueQDetailServiceTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueQDetailServiceTest.java @@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import com.github.sonus21.TestBase; import com.github.sonus21.rqueue.CoreUnitTest; @@ -49,8 +48,8 @@ import com.github.sonus21.rqueue.models.response.TableColumn; import com.github.sonus21.rqueue.models.response.TableRow; import com.github.sonus21.rqueue.utils.RqueueMessageTestUtils; -import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; import com.github.sonus21.rqueue.web.service.impl.RqueueQDetailServiceImpl; +import com.github.sonus21.rqueue.worker.RqueueWorkerRegistry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -76,8 +75,10 @@ class RqueueQDetailServiceTest extends TestBase { private final MessageConverter messageConverter = new GenericMessageConverter(); + @Mock private RedisTemplate redisTemplate; + @Mock private RqueueRedisTemplate stringRqueueRedisTemplate; @@ -89,8 +90,10 @@ class RqueueQDetailServiceTest extends TestBase { @Mock private RqueueMessageMetadataService rqueueMessageMetadataService; + @Mock private RqueueWorkerRegistry rqueueWorkerRegistry; + private RqueueQDetailService rqueueQDetailService; private QueueConfig queueConfig; private QueueConfig queueConfig2; @@ -101,14 +104,13 @@ class RqueueQDetailServiceTest extends TestBase { @BeforeEach public void init() { MockitoAnnotations.openMocks(this); - rqueueQDetailService = - new RqueueQDetailServiceImpl( - stringRqueueRedisTemplate, - rqueueMessageTemplate, - rqueueSystemManagerService, - rqueueMessageMetadataService, - rqueueConfig, - rqueueWorkerRegistry); + rqueueQDetailService = new RqueueQDetailServiceImpl( + stringRqueueRedisTemplate, + rqueueMessageTemplate, + rqueueSystemManagerService, + rqueueMessageMetadataService, + rqueueConfig, + rqueueWorkerRegistry); queueConfig = createQueueConfig("test", 10, 10000L, "test-dlq"); queueConfig2 = createQueueConfig("test2", 10, 10000L, null); queueConfigList = Arrays.asList(queueConfig, queueConfig2); @@ -190,9 +192,8 @@ void getNavTabs() { @Test void getExplorePageDataQueue() { - List rqueueMessages = - RqueueMessageUtils.generateMessages( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); + List rqueueMessages = RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); DataViewResponse expectedResponse = new DataViewResponse(); List headers = new ArrayList<>(); headers.add("Id"); @@ -225,9 +226,8 @@ void getExplorePageDataQueue() { @Test void getExplorePageDataDeadLetterQueue() { - List rqueueMessages = - RqueueMessageUtils.generateMessages( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); + List rqueueMessages = RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); DataViewResponse expectedResponse = new DataViewResponse(); List headers = new ArrayList<>(); headers.add("Id"); @@ -266,9 +266,8 @@ void getExplorePageDataDeadLetterQueue() { void getExplorePageDataTypeQueueDeleteFewItems() { QueueConfig queueConfig = createQueueConfig("test", 10, 10000L, null); queueConfig.addDeadLetterQueue(new DeadLetterQueue("test-dlq", false)); - List rqueueMessages = - RqueueMessageUtils.generateMessages( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); + List rqueueMessages = RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 10); List messageMetadata = new ArrayList<>(); for (int i = 0; i < 5; i++) { RqueueMessage message = rqueueMessages.get(i); @@ -316,9 +315,8 @@ void getExplorePageDataTypeQueueDeleteFewItems() { void getExplorePageDataTypeScheduledQueue() { QueueConfig queueConfig = createQueueConfig("test", 10, 10000L, null); queueConfig.addDeadLetterQueue(new DeadLetterQueue("test-dlq", false)); - List rqueueMessages = - RqueueMessageUtils.generateMessages( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 100000, 10); + List rqueueMessages = RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 100000, 10); DataViewResponse expectedResponse = new DataViewResponse(); List headers = new ArrayList<>(); headers.add("Id"); @@ -359,9 +357,8 @@ void getExplorePageDataTypeScheduledQueue() { void getExplorePageDataTypeProcessingQueue() { QueueConfig queueConfig = createQueueConfig("test", 10, 10000L, null); queueConfig.addDeadLetterQueue(new DeadLetterQueue("test-dlq", false)); - List rqueueMessages = - RqueueMessageUtils.generateMessages( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 100000, 10); + List rqueueMessages = RqueueMessageUtils.generateMessages( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, messageConverter, "test", 100000, 10); DataViewResponse expectedResponse = new DataViewResponse(); List headers = new ArrayList<>(); headers.add("Id"); @@ -420,10 +417,15 @@ void viewDataKey() { void viewDataList() { List objects = new ArrayList<>(); objects.add("Test"); - objects.add( - RqueueMessageUtils.buildMessage( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, - messageConverter, "jobs", null, "buildMessage", null, null, null)); + objects.add(RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + "jobs", + null, + "buildMessage", + null, + null, + null)); objects.add(null); doReturn(objects).when(stringRqueueRedisTemplate).lrange("jobs", 0, 9); DataViewResponse response = rqueueQDetailService.viewData("jobs", DataType.LIST, null, 0, 10); @@ -441,12 +443,17 @@ void viewDataList() { void viewDataZset() { Set> objects = new HashSet<>(); objects.add(new DefaultTypedTuple<>("Test", 100.0)); - objects.add( - new DefaultTypedTuple<>( - RqueueMessageUtils.buildMessage( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, - messageConverter, "jobs", null, "buildMessage", null, null, null), - 200.0)); + objects.add(new DefaultTypedTuple<>( + RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + "jobs", + null, + "buildMessage", + null, + null, + null), + 200.0)); List tableRows = new ArrayList<>(); for (TypedTuple typedTuple : objects) { @@ -473,10 +480,15 @@ void viewDataZset() { void viewDataSet() { Set objects = new HashSet<>(); objects.add("Test"); - objects.add( - RqueueMessageUtils.buildMessage( - RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, - messageConverter, "jobs", null, "Test object", null, null, null)); + objects.add(RqueueMessageUtils.buildMessage( + RqueueMessageTestUtils.MESSAGE_ID_GENERATOR, + messageConverter, + "jobs", + null, + "Test object", + null, + null, + null)); List tableRows = new ArrayList<>(); for (Object object : objects) { tableRows.add(new TableRow(new TableColumn(String.valueOf(object)))); diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java index 2d90e1f1..bd60c4c5 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java @@ -31,8 +31,8 @@ import com.github.sonus21.rqueue.CoreUnitTest; import com.github.sonus21.rqueue.config.RqueueConfig; import com.github.sonus21.rqueue.config.RqueueWebConfig; -import com.github.sonus21.rqueue.core.RqueueMessage; import com.github.sonus21.rqueue.core.RqueueInternalPubSubChannel; +import com.github.sonus21.rqueue.core.RqueueMessage; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; import com.github.sonus21.rqueue.dao.RqueueStringDao; import com.github.sonus21.rqueue.dao.RqueueSystemConfigDao; @@ -66,6 +66,7 @@ class RqueueUtilityServiceTest extends TestBase { @Mock private RqueueWebConfig rqueueWebConfig; + private RqueueMessageTemplate rqueueMessageTemplate; @Mock @@ -76,6 +77,7 @@ class RqueueUtilityServiceTest extends TestBase { @Mock private RqueueInternalPubSubChannel rqueueInternalPubSubChannel; + private final RqueueConfig rqueueConfig = new RqueueConfig(null, null, false, 2); private RqueueUtilityService rqueueUtilityService; @@ -126,14 +128,13 @@ void enqueueMessage() { assertEquals(1, response.getCode()); assertEquals("Message not found!", response.getMessage()); - RqueueMessage rqueueMessage = - RqueueMessage.builder() - .id(id) - .queueName("notification") - .message("test") - .queuedTime(System.nanoTime()) - .processAt(System.currentTimeMillis()) - .build(); + RqueueMessage rqueueMessage = RqueueMessage.builder() + .id(id) + .queueName("notification") + .message("test") + .queuedTime(System.nanoTime()) + .processAt(System.currentTimeMillis()) + .build(); MessageMetadata messageMetadata = new MessageMetadata(rqueueMessage, MessageStatus.ENQUEUED); doReturn(messageMetadata).when(messageMetadataService).getByMessageId("notification", id); doReturn(0L) diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImplTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImplTest.java index fe7aed31..87463e26 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImplTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/worker/RqueueWorkerRegistryImplTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; + import com.github.sonus21.TestBase; import com.github.sonus21.rqueue.CoreUnitTest; import com.github.sonus21.rqueue.common.RqueueRedisTemplate; @@ -60,14 +61,13 @@ void init() { MockitoAnnotations.openMocks(this); redisTemplate.setConnectionFactory(Mockito.mock(RedisConnectionFactory.class)); originalRedisTemplateProvider = RedisUtils.redisTemplateProvider; - RedisUtils.redisTemplateProvider = - new RedisUtils.RedisTemplateProvider() { - @Override - public RedisTemplate getRedisTemplate( - RedisConnectionFactory redisConnectionFactory) { - return (RedisTemplate) redisTemplate; - } - }; + RedisUtils.redisTemplateProvider = new RedisUtils.RedisTemplateProvider() { + @Override + public RedisTemplate getRedisTemplate( + RedisConnectionFactory redisConnectionFactory) { + return (RedisTemplate) redisTemplate; + } + }; workerTemplate = new TestWorkerTemplate(); stringTemplate = new TestStringTemplate(); } @@ -89,24 +89,21 @@ void getQueueWorkersUsesCapacityExhaustedAsActivity() throws Exception { long now = System.currentTimeMillis(); String workerId = "worker-1"; - RqueueWorkerPollerMetadata metadata = - RqueueWorkerPollerMetadata.builder() - .workerId(workerId) - .lastPollAt(now - Duration.ofSeconds(40).toMillis()) - .lastCapacityExhaustedAt(now) - .capacityExhaustedCount(2L) - .build(); + RqueueWorkerPollerMetadata metadata = RqueueWorkerPollerMetadata.builder() + .workerId(workerId) + .lastPollAt(now - Duration.ofSeconds(40).toMillis()) + .lastCapacityExhaustedAt(now) + .capacityExhaustedCount(2L) + .build(); stringTemplate.values = Collections.singletonMap(workerId, objectMapper.writeValueAsString(metadata)); - workerTemplate.values = - Collections.singletonList( - RqueueWorkerInfo.builder() - .workerId(workerId) - .host("host-1") - .pid("123") - .startedAt(now - Duration.ofMinutes(5).toMillis()) - .lastSeenAt(now) - .build()); + workerTemplate.values = Collections.singletonList(RqueueWorkerInfo.builder() + .workerId(workerId) + .host("host-1") + .pid("123") + .startedAt(now - Duration.ofMinutes(5).toMillis()) + .lastSeenAt(now) + .build()); List workers = registry.getQueueWorkers("test"); @@ -132,22 +129,21 @@ void recordQueueCapacityExhaustedSaturatesAtLongMaxValue() throws Exception { new java.util.concurrent.ConcurrentHashMap<>( Collections.singletonMap("test-queue", Long.MAX_VALUE))); - QueueDetail queueDetail = - QueueDetail.builder() - .name("test-queue") - .queueName("test-queue") - .processingQueueName("test-queue-processing") - .processingQueueChannelName("test-queue-processing-channel") - .scheduledQueueName("test-queue-scheduled") - .scheduledQueueChannelName("test-queue-scheduled-channel") - .completedQueueName("test-queue-completed") - .active(true) - .visibilityTimeout(1000L) - .batchSize(1) - .numRetry(3) - .concurrency(new Concurrency(1, 1)) - .priority(Collections.emptyMap()) - .build(); + QueueDetail queueDetail = QueueDetail.builder() + .name("test-queue") + .queueName("test-queue") + .processingQueueName("test-queue-processing") + .processingQueueChannelName("test-queue-processing-channel") + .scheduledQueueName("test-queue-scheduled") + .scheduledQueueChannelName("test-queue-scheduled-channel") + .completedQueueName("test-queue-completed") + .active(true) + .visibilityTimeout(1000L) + .batchSize(1) + .numRetry(3) + .concurrency(new Concurrency(1, 1)) + .priority(Collections.emptyMap()) + .build(); registry.recordQueueCapacityExhausted( queueDetail, diff --git a/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/integration/RqueueMessageTemplateTest.java b/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/integration/RqueueMessageTemplateTest.java index df4e4743..e3ba617f 100644 --- a/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/integration/RqueueMessageTemplateTest.java +++ b/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/integration/RqueueMessageTemplateTest.java @@ -108,11 +108,16 @@ void moveMessageZsetToZset() { void getScore() { String tgtZset = "getScoreZSet"; MessageConverter converter = new DefaultRqueueMessageConverter(); - RqueueMessage message = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); - RqueueMessage message2 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); - RqueueMessage message3 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); - RqueueMessage message4 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); - RqueueMessage message5 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message = + RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message2 = + RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message3 = + RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message4 = + RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message5 = + RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); long score = System.currentTimeMillis(); rqueueMessageTemplate.addToZset(tgtZset, message2, score); rqueueMessageTemplate.addToZset(tgtZset, message3, 0); @@ -129,9 +134,12 @@ void getScore() { void updateScore() { String tgtZset = "updateScoreZSet"; MessageConverter converter = new DefaultRqueueMessageConverter(); - RqueueMessage message = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); - RqueueMessage message2 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); - RqueueMessage message3 = RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message = + RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message2 = + RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); + RqueueMessage message3 = + RqueueMessageUtils.generateMessage(MESSAGE_ID_GENERATOR, converter, tgtZset); long score = System.currentTimeMillis(); rqueueMessageTemplate.addToZset(tgtZset, message, score); assertTrue(rqueueMessageTemplate.addScore(tgtZset, message, 10_000)); diff --git a/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/unit/RqueueListenerAutoConfigTest.java b/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/unit/RqueueListenerAutoConfigTest.java index 1752efaf..aab258bb 100644 --- a/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/unit/RqueueListenerAutoConfigTest.java +++ b/rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/tests/unit/RqueueListenerAutoConfigTest.java @@ -122,9 +122,8 @@ void rqueueMessageSenderWithMessageTemplate() throws IllegalAccessException { doReturn(new DefaultRqueueMessageConverter()).when(rqueueMessageHandler).getMessageConverter(); RqueueListenerAutoConfig messageAutoConfig = new RqueueListenerAutoConfig(); FieldUtils.writeField(messageAutoConfig, "simpleRqueueListenerContainerFactory", factory, true); - assertNotNull( - messageAutoConfig.rqueueMessageEnqueuer( - rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator())); + assertNotNull(messageAutoConfig.rqueueMessageEnqueuer( + rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator())); assertEquals(factory.getRqueueMessageTemplate().hashCode(), messageTemplate.hashCode()); } @@ -138,12 +137,10 @@ void rqueueMessageSenderWithMessageConverters() throws IllegalAccessException { factory.setRqueueMessageTemplate(messageTemplate); FieldUtils.writeField(messageAutoConfig, "simpleRqueueListenerContainerFactory", factory, true); doReturn(messageConverter).when(rqueueMessageHandler).getMessageConverter(); - assertNotNull( - messageAutoConfig.rqueueMessageEnqueuer( - rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator())); - RqueueMessageEnqueuer messageSender = - messageAutoConfig.rqueueMessageEnqueuer( - rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator()); + assertNotNull(messageAutoConfig.rqueueMessageEnqueuer( + rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator())); + RqueueMessageEnqueuer messageSender = messageAutoConfig.rqueueMessageEnqueuer( + rqueueMessageHandler, messageTemplate, new UuidV4RqueueMessageIdGenerator()); MessageConverter converter = messageSender.getMessageConverter(); assertTrue(converter.hashCode() == messageConverter.hashCode()); } diff --git a/rqueue-spring-common-test/src/main/java/com/github/sonus21/rqueue/test/common/SpringTestBase.java b/rqueue-spring-common-test/src/main/java/com/github/sonus21/rqueue/test/common/SpringTestBase.java index 699f68e6..29303dbd 100644 --- a/rqueue-spring-common-test/src/main/java/com/github/sonus21/rqueue/test/common/SpringTestBase.java +++ b/rqueue-spring-common-test/src/main/java/com/github/sonus21/rqueue/test/common/SpringTestBase.java @@ -60,14 +60,29 @@ public abstract class SpringTestBase extends TestBase { private static final RqueueMessageIdGenerator MESSAGE_ID_GENERATOR = new UuidV4RqueueMessageIdGenerator(); - @Autowired protected RqueueMessageTemplate rqueueMessageTemplate; - @Autowired protected RqueueConfig rqueueConfig; - @Autowired protected RqueueRedisTemplate stringRqueueRedisTemplate; - @Autowired protected ConsumedMessageStore consumedMessageStore; - @Autowired protected RqueueMessageListenerContainer rqueueMessageListenerContainer; - @Autowired protected FailureManager failureManager; - @Autowired protected RqueueMessageEnqueuer rqueueMessageEnqueuer; - @Autowired protected RqueueEventListener rqueueEventListener; + @Autowired + protected RqueueMessageTemplate rqueueMessageTemplate; + + @Autowired + protected RqueueConfig rqueueConfig; + + @Autowired + protected RqueueRedisTemplate stringRqueueRedisTemplate; + + @Autowired + protected ConsumedMessageStore consumedMessageStore; + + @Autowired + protected RqueueMessageListenerContainer rqueueMessageListenerContainer; + + @Autowired + protected FailureManager failureManager; + + @Autowired + protected RqueueMessageEnqueuer rqueueMessageEnqueuer; + + @Autowired + protected RqueueEventListener rqueueEventListener; @Autowired(required = false) protected ReactiveRqueueMessageEnqueuer reactiveRqueueMessageEnqueuer; @@ -148,10 +163,15 @@ public abstract class SpringTestBase extends TestBase { protected boolean reactiveEnabled; protected void enqueue(Object message, String queueName) { - RqueueMessage rqueueMessage = - RqueueMessageUtils.buildMessage( - MESSAGE_ID_GENERATOR, - rqueueMessageManager.getMessageConverter(), queueName, null, message, null, null, null); + RqueueMessage rqueueMessage = RqueueMessageUtils.buildMessage( + MESSAGE_ID_GENERATOR, + rqueueMessageManager.getMessageConverter(), + queueName, + null, + message, + null, + null, + null); rqueueMessageTemplate.addMessage(queueName, rqueueMessage); } @@ -159,16 +179,15 @@ protected void enqueue(String queueName, Factory factory, int n, boolean useMess for (int i = 0; i < n; i++) { Object message = factory.next(i); if (useMessageTemplate) { - RqueueMessage rqueueMessage = - RqueueMessageUtils.buildMessage( - MESSAGE_ID_GENERATOR, - rqueueMessageManager.getMessageConverter(), - queueName, - null, - message, - null, - null, - rqueueMessageListenerContainer.getMessageHeaders()); + RqueueMessage rqueueMessage = RqueueMessageUtils.buildMessage( + MESSAGE_ID_GENERATOR, + rqueueMessageManager.getMessageConverter(), + queueName, + null, + message, + null, + null, + rqueueMessageListenerContainer.getMessageHeaders()); rqueueMessageTemplate.addMessage(queueName, rqueueMessage); } else { enqueue(queueName, message); @@ -182,16 +201,15 @@ protected void enqueueIn( Object message = factory.next(i); long delay = delayFunc.getDelay(i); if (useMessageTemplate) { - RqueueMessage rqueueMessage = - RqueueMessageUtils.buildMessage( - MESSAGE_ID_GENERATOR, - rqueueMessageManager.getMessageConverter(), - queueName, - null, - message, - null, - delay, - null); + RqueueMessage rqueueMessage = RqueueMessageUtils.buildMessage( + MESSAGE_ID_GENERATOR, + rqueueMessageManager.getMessageConverter(), + queueName, + null, + message, + null, + delay, + null); rqueueMessageTemplate.addToZset(queueName, rqueueMessage, rqueueMessage.getProcessAt()); } else { enqueueIn(queueName, message, delay); @@ -200,10 +218,15 @@ protected void enqueueIn( } protected void enqueueIn(Object message, String zsetName, long delay) { - RqueueMessage rqueueMessage = - RqueueMessageUtils.buildMessage( - MESSAGE_ID_GENERATOR, - rqueueMessageManager.getMessageConverter(), zsetName, null, message, null, delay, null); + RqueueMessage rqueueMessage = RqueueMessageUtils.buildMessage( + MESSAGE_ID_GENERATOR, + rqueueMessageManager.getMessageConverter(), + zsetName, + null, + message, + null, + delay, + null); rqueueMessageTemplate.addToZset(zsetName, rqueueMessage, rqueueMessage.getProcessAt()); } diff --git a/rqueue-spring/src/test/java/com/github/sonus21/rqueue/spring/tests/unit/RqueueMessageConfigTest.java b/rqueue-spring/src/test/java/com/github/sonus21/rqueue/spring/tests/unit/RqueueMessageConfigTest.java index 1dfd10c1..60e94372 100644 --- a/rqueue-spring/src/test/java/com/github/sonus21/rqueue/spring/tests/unit/RqueueMessageConfigTest.java +++ b/rqueue-spring/src/test/java/com/github/sonus21/rqueue/spring/tests/unit/RqueueMessageConfigTest.java @@ -25,8 +25,8 @@ import com.github.sonus21.rqueue.converter.DefaultMessageConverterProvider; import com.github.sonus21.rqueue.converter.GenericMessageConverter; import com.github.sonus21.rqueue.core.DefaultRqueueMessageConverter; -import com.github.sonus21.rqueue.core.impl.UuidV4RqueueMessageIdGenerator; import com.github.sonus21.rqueue.core.RqueueMessageTemplate; +import com.github.sonus21.rqueue.core.impl.UuidV4RqueueMessageIdGenerator; import com.github.sonus21.rqueue.listener.RqueueMessageHandler; import com.github.sonus21.rqueue.spring.RqueueListenerConfig; import com.github.sonus21.rqueue.spring.tests.SpringUnitTest; @@ -120,11 +120,8 @@ void rqueueMessageSenderWithMessageTemplate() throws IllegalAccessException { doReturn(new DefaultRqueueMessageConverter()).when(rqueueMessageHandler).getMessageConverter(); RqueueListenerConfig messageConfig = new RqueueListenerConfig(); FieldUtils.writeField(messageConfig, "simpleRqueueListenerContainerFactory", factory, true); - assertNotNull( - messageConfig.rqueueMessageEnqueuer( - rqueueMessageHandler, - rqueueMessageTemplate, - new UuidV4RqueueMessageIdGenerator())); + assertNotNull(messageConfig.rqueueMessageEnqueuer( + rqueueMessageHandler, rqueueMessageTemplate, new UuidV4RqueueMessageIdGenerator())); assertEquals(factory.getRqueueMessageTemplate().hashCode(), rqueueMessageTemplate.hashCode()); } } From a4b051973ffd9f6db10aa7cf5f67b911974ca754 Mon Sep 17 00:00:00 2001 From: Sonu Kumar Date: Tue, 24 Mar 2026 01:54:44 +0530 Subject: [PATCH 3/5] wip --- docs/CHANGELOG.md | 24 +++++++++++++++++ docs/configuration/configuration.md | 41 +++++++++++++++++++++++++++- docs/dashboard.md | 42 +++++++++++++++++++++++++++++ docs/index.md | 3 ++- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b07204b3..df8e43bd 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,30 @@ layout: default All notable user-facing changes to this project are documented in this file. +## Release [4.0.0.RC2] 24-Mar-2026 + +{: .highlight} +This is a release candidate for 4.0.0. It targets Spring Boot 4.x and Spring Framework 7.x. +Please test thoroughly before using in production. + +### Features +* **Pluggable message ID generation** — added `RqueueMessageIdGenerator` with a + default UUIDv4 implementation so applications can override message ID generation + with a custom bean, including time-ordered strategies such as UUIDv7. +* **Worker registry for dashboard visibility** — added an optional + `rqueue.worker.registry.enabled` registry that tracks worker metadata and + queue-level poller activity for dashboard use. +* **Workers dashboard page** — added a dedicated workers view showing worker + identity, queue pollers, last poll activity, and recent capacity exhaustion. +* **Queue and workers pagination** — added server-side pagination for dashboard + queue and worker listings, with configurable page sizes. +* **Dashboard enqueue controls for scheduled messages** — messages in scheduled + queues can now be moved back to the main queue from the dashboard, including + explicit front/rear enqueue options for non-periodic messages. +* **Dashboard refresh and usability improvements** — refreshed queue, worker, and + explorer UI with improved layouts, duration formatting, feedback modals, and + more readable queue metadata. + ## Release [4.0.0.RC1] 18-Mar-2026 {: .highlight} diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index c9851693..49676387 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -242,10 +242,49 @@ The serialized form encodes both the envelope class and the type parameter: `List>` are not supported. - Multi-level nesting (e.g. `Wrapper>`) is not supported. +## Message ID Generator + +Rqueue now resolves message IDs through the `RqueueMessageIdGenerator` abstraction. +By default, Rqueue registers a UUIDv4-based implementation, but applications can +override it by defining their own bean. + +This is useful when you need: + +- time-ordered IDs such as UUIDv7 +- custom prefixes or tenant-aware IDs +- IDs generated by an external system + +```java +import com.github.sonus21.rqueue.core.RqueueMessageIdGenerator; + +@Configuration +public class RqueueConfiguration { + + @Bean + public RqueueMessageIdGenerator rqueueMessageIdGenerator() { + return () -> java.util.UUID.randomUUID().toString(); + } +} +``` + +{: .note} +The default implementation is still UUIDv4. Custom generators are applied to the +normal enqueue APIs such as `enqueue`, `enqueueIn`, `enqueueAt`, and `enqueuePeriodic` +whenever Rqueue generates the message ID internally. + ## Additional Configuration - **`rqueue.retry.per.poll`**: Determines how many times a polled message is retried immediately if processing fails, before it is moved back to the queue for a subsequent poll. The default value is `1`. If increased to `N`, the message will be retried `N` times consecutively within the same polling cycle. - +- **`rqueue.worker.registry.enabled`**: Enables worker and queue-poller tracking for + the dashboard. Default: `true`. +- **`rqueue.worker.registry.worker.ttl`**: TTL in seconds for worker metadata stored + in Redis. Default: `300`. +- **`rqueue.worker.registry.worker.heartbeat.interval`**: Interval in seconds for + refreshing worker metadata. Default: `60`. +- **`rqueue.worker.registry.queue.ttl`**: TTL in seconds for queue poller hashes. + Default: `3600`. +- **`rqueue.worker.registry.queue.heartbeat.interval`**: Interval in seconds for + queue poller heartbeats. Default: `15`. diff --git a/docs/dashboard.md b/docs/dashboard.md index 83609e03..613d41ad 100644 --- a/docs/dashboard.md +++ b/docs/dashboard.md @@ -25,6 +25,12 @@ message processing. * **Task Operations**: Facilitates moving tasks between different queues. +* **Worker Visibility**: Shows which worker is polling a queue, when it last polled, + and whether the queue recently ran out of execution capacity. + +* **Scheduled Message Recovery**: Allows non-periodic scheduled messages to be moved + back to the main queue from the dashboard, either at the front or rear. + Access the dashboard at: [http://localhost:8080/rqueue](http://localhost:8080/rqueue) ## Configuration @@ -71,6 +77,8 @@ Example URL with a configured prefix: * `rqueue.web.enable`: Enable or disable the web dashboard (default: `true`). * `rqueue.web.max.message.move.count`: Maximum number of messages to move in a single request from the utility tab (default: `1000`). +* `rqueue.web.queue.page.size`: Number of queue cards shown per page (default: `12`). +* `rqueue.web.worker.page.size`: Number of worker cards shown per page (default: `10`). * `rqueue.web.collect.listener.stats`: Enable collection of task execution statistics (default: `false`). * `rqueue.web.collect.listener.stats.thread.count`: Number of threads used for metrics aggregation. @@ -82,6 +90,40 @@ Example URL with a configured prefix: * `rqueue.web.collect.statistic.aggregate.shutdown.wait.time`: Wait time in milliseconds for forced aggregation of pending events during application shutdown. +## Worker Registry + +The dashboard can optionally maintain lightweight worker metadata in Redis to show: + +- worker host and process ID +- queue-level polling activity +- recent queue capacity exhaustion +- worker and queue drill-down views + +This feature is controlled by the following properties: + +- `rqueue.worker.registry.enabled` +- `rqueue.worker.registry.worker.ttl` +- `rqueue.worker.registry.worker.heartbeat.interval` +- `rqueue.worker.registry.queue.ttl` +- `rqueue.worker.registry.queue.heartbeat.interval` + +{: .note} +The worker registry is intended for dashboard visibility. Instance-level liveness +should still be monitored through your infrastructure or platform health checks. + +## Queue Explorer Actions + +The queue explorer supports queue-specific administrative actions: + +- delete pending, running, dead-letter, or scheduled messages +- move messages between Redis collections from the Utility tab +- enqueue scheduled messages back to the main queue + +For scheduled messages: + +- periodic messages can be deleted, but are not offered queue-to-front or queue-to-rear actions +- non-periodic scheduled messages can be queued to the front or rear of the main queue + ### Dashboard Screenshots #### Latency Graph diff --git a/docs/index.md b/docs/index.md index 94863274..0be06573 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,7 +52,8 @@ model through annotation-driven APIs and minimal setup. * Use callbacks for dead-letter, discard, and related flows * Subscribe to bootstrap and task execution events * Monitor in-flight, queued, and scheduled messages with metrics - * Use the built-in web dashboard for queue visibility and monitoring + * Use the built-in web dashboard for queue visibility, worker activity, and message operations + * Override message ID generation with a custom `RqueueMessageIdGenerator` bean * **Redis and platform support** * Support Redis standalone, Sentinel, and Cluster setups From 384391211daa8685d6c6c062561cbc939ccc2286 Mon Sep 17 00:00:00 2001 From: Sonu Kumar Date: Tue, 24 Mar 2026 01:59:06 +0530 Subject: [PATCH 4/5] fixed: failing test --- .../core/RqueueMessageTemplateTest.java | 1 + .../rqueue/listener/RqueueMiddlewareTest.java | 30 ++++++++++++------- .../web/service/RqueueUtilityServiceTest.java | 2 ++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java index 106bf32f..b1cda6eb 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/core/RqueueMessageTemplateTest.java @@ -53,6 +53,7 @@ class RqueueMessageTemplateTest extends TestBase { @Mock private RedisConnectionFactory redisConnectionFactory; + @Mock private ListOperations listOperations; @Mock diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java index f0b280c1..1afb917c 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java @@ -70,6 +70,7 @@ import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; @@ -328,19 +329,26 @@ void logAndRateLimiterMiddleware() throws TimedOutException { }) .when(messageHandler) .handleMessage(any()); - Executor executor = Executors.newSingleThreadExecutor(); + ExecutorService executor = Executors.newFixedThreadPool(4); long startTime = System.currentTimeMillis(); - for (RqueueMessage message : messages) { - executor.execute(new RqueueExecutor( - rqueueBeanProvider, - queueStateMgr, - newArrayList(logMiddleware, testRateLimiter), - postProcessingHandler, - message, - queueDetail, - queueThreadPool)); + try { + for (RqueueMessage message : messages) { + executor.execute(new RqueueExecutor( + rqueueBeanProvider, + queueStateMgr, + newArrayList(logMiddleware, testRateLimiter), + postProcessingHandler, + message, + queueDetail, + queueThreadPool)); + } + TimeoutUtils.waitFor( + () -> testRateLimiter.jobs.size() == jobCount, + 20_000L, + "all jobs to proceed"); + } finally { + executor.shutdownNow(); } - TimeoutUtils.waitFor(() -> testRateLimiter.jobs.size() == jobCount, "all jobs to proceed"); long endTime = System.currentTimeMillis(); // we need to round since rate is at second resolution and execution in millis so we round this // to next second diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java index 2d90e1f1..21f54d07 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/web/service/RqueueUtilityServiceTest.java @@ -66,6 +66,8 @@ class RqueueUtilityServiceTest extends TestBase { @Mock private RqueueWebConfig rqueueWebConfig; + + @Mock private RqueueMessageTemplate rqueueMessageTemplate; @Mock From 2aeee48edc17b5e7da7449074db601df587b20ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:31:19 +0000 Subject: [PATCH 5/5] Apply Palantir Java Format --- .../github/sonus21/rqueue/listener/RqueueMiddlewareTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java index d96ad0db..ea0103d1 100644 --- a/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java +++ b/rqueue-core/src/test/java/com/github/sonus21/rqueue/listener/RqueueMiddlewareTest.java @@ -69,7 +69,6 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import lombok.extern.slf4j.Slf4j; @@ -340,9 +339,7 @@ void logAndRateLimiterMiddleware() throws TimedOutException { queueThreadPool)); } TimeoutUtils.waitFor( - () -> testRateLimiter.jobs.size() == jobCount, - 20_000L, - "all jobs to proceed"); + () -> testRateLimiter.jobs.size() == jobCount, 20_000L, "all jobs to proceed"); } finally { executor.shutdownNow(); }