Snapshot of nats-backend branch progress and what's left to land. Kept here so a fresh session can resume cleanly.
com.github.sonus21.rqueue.core.spipackage with:MessageBrokerinterface —enqueue / enqueueWithDelay / pop / ack / nack / moveExpired / peek / size / subscribe / publish / capabilitiesand thedefaultreactive overloads (enqueueReactive,enqueueWithDelayReactive).Capabilitiesrecord (supportsDelayedEnqueue,supportsScheduledIntrospection,supportsCronJobs,usesPrimaryHandlerDispatch).MessageBrokerFactory+MessageBrokerLoader(ServiceLoader).
RedisMessageBrokerthin delegate over the existingRqueueMessageTemplate.- Public-API additions only — no removals:
setMessageBroker/getMessageBrokeronRqueueMessageTemplateImpl,SimpleRqueueListenerContainerFactory,RqueueMessageListenerContainer.RqueueMessageTemplateinterface frozen. - Existing 461+
:rqueue-core:testcases pass unchanged; 14 newRedisMessageBrokerDelegationTestcases lock the delegation contract.
- New module
rqueue-nats(broker-impl only, no Spring/Boot deps; jnats 2.25.2 asapi). Auto-config and@Conditionalwiring live inrqueue-spring-boot-starterandrqueue-spring, gated by@ConditionalOnClass(io.nats.client.JetStream.class)+@ConditionalOnProperty(rqueue.backend=nats). JetStreamMessageBroker:- Builder API (
builder().connection(...).jetStream(...).management(...).config(...).build()). enqueue→js.publish(subject, headers, payload)withNats-Msg-Idheader for dedup.enqueueWithDelay→ throwsUnsupportedOperationException(NATS v1 doesn't support arbitrary delay).pop→ ensures stream + durable consumer, cachesJetStreamSubscription,sub.fetch(batch, wait), stashes rawMessageininFlightkeyed byRqueueMessage.idfor ack/nack lookup.ack/nack(delayMs)→Message.ack()/Message.nakWithDelay(...).peek→ ephemeral pull consumer withAckPolicy.None, fetch + unsubscribe (no perturbation of the durable's ack-pending state).size→jsm.getStreamInfo(stream).getStreamState().getMsgCount().subscribe/publish→ core NATSDispatcher, returnsAutoCloseablethat callscloseDispatcher.- Reactive overrides via
js.publishAsync(...)wrapped inMono.fromFuture. - DLQ bridge:
installDeadLetterBridge(QueueDetail, consumerName)subscribes to$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>and republishes exhausted messages to the DLQ subject.
- Builder API (
RqueueNatsConfigPOJO + nestedStreamDefaults/ConsumerDefaultsfor stream replication/storage/retention/dedup-window and consumer ack-wait/max-deliver/max-ack-pending.NatsProvisioner(inrqueue-nats/.../internal/) — idempotentensureStream,ensureConsumer,ensureDlqStream. Logs WARN if existing config drifts from desired (doesn't mutate).JetStreamMessageBrokerFactory+META-INF/services/com.github.sonus21.rqueue.core.spi.MessageBrokerFactoryfor ServiceLoader discovery (name() == "nats").RqueueNatsException(RuntimeException) wrapsIOException/JetStreamApiExceptionwith stream/subject/consumer context in the message.- 9 Docker-gated ITs in
rqueue-nats/src/test/:EnqueueAck,Retry+DLQ,CompetingConsumers,IndependentConsumers,Dedup,Peek,PubSub,ReactiveEnqueue,DelayThrows. All pass againstnats:2.10-alpine -jsvia Testcontainers.
RqueueNatsAutoConfig(Boot) registered viaMETA-INF/spring/...AutoConfiguration.imports, gated@ConditionalOnClass(JetStream.class) + @ConditionalOnProperty(rqueue.backend=nats). ProvidesConnection,JetStream,JetStreamManagement,MessageBroker,RqueueQueueMetricsProviderbeans.RqueueNatsListenerConfig(non-Boot) activated byNatsBackendCondition.@EnableRqueue(backend=NATS)opts in via the newBackendenum.RqueueListenerAutoConfig's default Redis broker uses@ConditionalOnMissingBean(MessageBroker.class); the NATS bean wins when present.@RqueueListener.consumerName()attribute (additive).ConsumerNameResolverresolves:consumerNameif set, else"rqueue-" + queue + "-" + bean + "_" + methodwith everything outside[A-Za-z0-9_-]collapsed to_(NATS durable-name constraint).RqueueMessageHandlerskips primary-validation and logs one boot WARN listing@RqueueHandlerannotated methods when capability says no primary dispatch.- Cross-handler validation:
(queue, consumerName)collisions fail boot fast.
BrokerMessagePollerinrqueue-core/listener/. One thread per(queue, consumerName, priority)triple per@RqueueListener.concurrency.max. Loop:broker.pop→ deserialize viaMessageConverter→ reflection-invoke the boundHandlerMethod(callscreateWithResolvedBean()so bean-name lookup works) →broker.ack/broker.nack(delayMs)withTaskExecutionBackOff.RqueueMessageListenerContainerbranches onmessageBroker != null && !capabilities.usesPrimaryHandlerDispatch():startBrokerPollers()enumerates active queues + handler methods, resolves consumer names, spawns pollers.MessageSchedulernot started;RqueueMessageHandlerprimary loop bypassed.doStop()signals every poller;doDestroy()callsbroker.close()ifAutoCloseable.
BaseMessageSender.enqueueroutes throughMessageBroker.enqueuewhen the active broker has!usesPrimaryHandlerDispatch.storeMessageMetadatashort-circuits on the same flag.RqueueListenerAutoConfig.rqueueMessageTemplatepropagates the autowiredMessageBrokeronto the template bean — without that,BaseMessageSender#enqueuewould silently fall back to the Redis publish path.SimpleRqueueListenerContainerFactory.createMessageListenerContainer()skips theredisConnectionFactory != nullassertion when a non-Redis broker is wired.
QueueDetailadds nullable NATS fields withresolved*helpers:natsStream,natsSubject,natsDlqStream,natsDlqSubject,natsAckWaitOverride,natsMaxDeliverOverride,natsDedupWindow. Defaults derived fromqueueNamewhen null.RqueueQDetailServiceImplroutessize/peekthroughMessageBrokerwhen set; falls back to existing Redis path otherwise.DataViewResponseaddshideScheduledPanel/hideCronJobsflags. Pebble templatebase.htmlhides the "Scheduled" sidebar entry when the flag is set.- Dashboard chain (
RqueueRestController,RqueueDashboardChartServiceImpl, etc.) gated@Conditional(RedisBackendCondition); on NATS the dashboard reports broker-derived sizes only.
- Pluggable selection via
rqueue.backend=redis|nats(defaultredis) and classpath presence. RqueueConfigcarries the activeBackendenum; downstream beans branch on that instead of probing the classpath.Backend.AUTOremoved;@EnableRqueue.backend()defaults toREDIS.
- New module
rqueue-rediswith the Redis-shaped impls (DAOs, lock manager, KV-shaped beans). Backendenum +RedisBackendCondition/NatsBackendConditioninrqueue-core.RqueueConfig.backendfield (defaultREDIS) bound from therqueue.backendproperty;Backend.AUTOremoved.RqueueListenerBaseConfig.rqueueConfig(...)factory tolerates a missingRedisConnectionFactory.RqueueRedisTemplateandRqueueMessageTemplateImplconstructors tolerate null Redis connection factory (NATS path constructs them for type satisfaction but never invokes Redis ops on them).SimpleRqueueListenerContainerFactoryskips itsredisConnectionFactory != nullassertion when a non-Redis broker is set.BaseMessageSenderroutes producer enqueue throughMessageBroker.enqueuewhen broker has!usesPrimaryHandlerDispatch;storeMessageMetadatashort-circuits on the same flag.RqueueQueueMetricsProvideris the new backend-agnostic interface for queue-depth gauges;RedisRqueueQueueMetricsProvider(rqueue-redis) andNatsRqueueQueueMetricsProvider(rqueue-nats) supply impls.RqueueMetricsnow reads through this provider, decoupled fromRqueueStringDao.
NatsRqueueLockManager— KV bucketrqueue-locks, atomic create/release with revisioned delete, 6 ITs.NatsRqueueSystemConfigDao— KV bucketrqueue-queue-config, in-process cache, 6 ITs.NatsRqueueJobDao— KV bucketrqueue-jobs, scan-by-message-id, 7 ITs.NatsRqueueMessageMetadataService— KV bucketrqueue-message-metadata, 8 ITs.NatsRqueueUtilityService— admin-only stub returning "not supported" responses.NatsRqueueStringDaodeleted — no consumer on the NATS path needs it.
RqueueStringDaoconsumers (RqueueLockManagerImpl,RqueueJobDaoImpl,RqueueMessageMetadataServiceImpl,RqueueSystemManagerServiceImpl,RqueueUtilityServiceImpl,RqueueMetrics's old size getter) all gated@Conditional(RedisBackendCondition.class)or refactored to useRqueueQueueMetricsProvider.RqueueStringDaois now strictly internal to the Redis backend; no NATS-path bean autowires it.BaseMessageSender,RqueueMessageManagerImpl,RqueueEndpointManagerImpl,RqueueBeanProviderreverted to plain@Autowired(no morerequired=falseshotgun) — every required interface has either a Redis impl or aNatsRqueueXxximpl.
nats_integration_testjob in.github/workflows/java-ci.yamlinstalls nats-server v2.10.22 binary directly (no Docker), setsNATS_RUNNING=true+NATS_URL, mirrors theredis_cluster_testpattern.AbstractNatsBootITandAbstractJetStreamIThonorNATS_RUNNING(CI path) and fall back to Testcontainers (local Docker path).- Tests are tagged
@Tag("nats")viaNatsIntegrationTest/NatsUnitTestmeta-annotations.
- DAO impls:
RqueueStringDaoImpl,RqueueJobDaoImpl,RqueueMessageMetadataDaoImpl,RqueueQStatsDaoImpl,RqueueSystemConfigDaoImpl→rqueue-redis/src/main/java/com/github/sonus21/rqueue/redis/dao/. - Metrics:
RedisRqueueQueueMetricsProvider→rqueue-redis/.../redis/metrics/. - 5
@Beanfactories fromRqueueListenerBaseConfig→RqueueRedisListenerConfig:rqueueRedisLongTemplate,rqueueRedisListenerContainerFactory,stringRqueueRedisTemplate,rqueueInternalPubSubChannel,rqueueStringDao. - 5 more
@Beanfactories:scheduledMessageScheduler,processingMessageScheduler,rqueueWorkerRegistry,rqueueLockManager,rqueueQueueMetrics. - 6 service impls:
RqueueDashboardChartServiceImpl,RqueueJobServiceImpl,RqueueMessageMetadataServiceImpl,RqueueQDetailServiceImpl,RqueueSystemManagerServiceImpl,RqueueUtilityServiceImpl→rqueue-redis/.../redis/web/service/impl/.
- New module
rqueue-webregistered insettings.gradle.rqueue-spring-boot-starter,rqueue-spring,rqueue-redis, andrqueue-spring-common-testdeclareapi project(":rqueue-web")so the dashboard ships by default; consumers<exclude>it for headless workers. - Moved out of
rqueue-core/web/...intorqueue-web/web/...: 5 controllers (Base,BaseReactive,RqueueRest,RqueueView,ReactiveRqueueRest,ReactiveRqueueView),RqueueWebExceptionAdvice,RqueueViewControllerServiceImpl, and the 6 web-only service interfaces (RqueueDashboardChartService,RqueueJobMetricsAggregatorService,RqueueJobService,RqueueQDetailService,RqueueSystemManagerService,RqueueViewControllerService). Stayed in core:RqueueMessageMetadataServiceandRqueueUtilityServiceinterfaces (consumed by listener / endpoint manager). - Moved out of
rqueue-core/utils/pebble/: 7 Pebble extension classes →rqueue-web/utils/pebble/(same package, no import changes). - Moved out of
rqueue-core/src/main/resources/:templates/rqueue/**,public/rqueue/**(CSS, JS, vendor assets) →rqueue-web/src/main/resources/. - Moved out of
rqueue-core/src/test/:web/**andutils/pebble/**test files →rqueue-web/src/test/. Currently unbuildable — see Pending. - Pebble view-resolver
@Beans extracted fromRqueueListenerBaseConfiginto a newRqueueWebViewConfiginrqueue-web/web/config/. Picked up via the existingcom.github.sonus21.rqueue.webcomponent scan. rqueue-core/build.gradledropped:spring-webmvc,spring-webflux,jakarta.servlet-api,pebble-spring7,seruco/base62,hibernate-validator,org.glassfish:jakarta.el. Addedreactor-coredirectly (no longer comes viaspring-webflux).jakarta.validation-apiretained for DTO annotations.
HttpUtils.readUrlrewritten to usejava.net.http.HttpClient+ Jackson;org.springframework.web.client.RestTemplateremoved.spring-webdropped fromrqueue-coredeps.joinPathretained unchanged;RqueueWebConfig.getUrlPrefixstill calls it.
- New SPI in
rqueue-core:WorkerRegistryStore(7 narrow KV-shaped methods).RqueueWorkerRegistryImplrelocated fromrqueue-redis/redis/worker/torqueue-core/worker/; takes(RqueueConfig, WorkerRegistryStore)in the constructor. All heartbeat scheduling / view assembly logic backend-neutral. RedisWorkerRegistryStore(rqueue-redis) wrapsRqueueRedisTemplateoverset/get/mget/hash ops.NatsWorkerRegistryStore(rqueue-nats) wraps two JetStream KV buckets:rqueue-workers(TTL =workerRegistry.workerTtl),rqueue-worker-heartbeats(TTL =workerRegistry.queueTtl). Hash-of-strings emulated as flattened keys<sanitizedQueueKey>__<sanitizedWorkerId>.refreshQueueTtlis a no-op since NATS resets per-entry age on each write.- Wired in
RqueueRedisListenerConfigandRqueueNatsAutoConfigunder@ConditionalOnMissingBean.
- New
com.github.sonus21.rqueue.nats.kv.NatsKvBucketsconstants class — single source of truth for the 6 bucket names (QUEUE_CONFIG,JOBS,LOCKS,MESSAGE_METADATA,WORKERS,WORKER_HEARTBEATS) +ALL_BUCKETSlist. Every store / dao now references this constant instead of a private string. - New
com.github.sonus21.rqueue.nats.kv.NatsKvBucketValidator— config-source-agnostic class; constructor takes(Connection, boolean autoCreate). Staticvalidate(Connection, boolean)walksALL_BUCKETSviakvm.getStatus(name)and aborts withIllegalStateExceptionlisting missing buckets. ImplementsInitializingBeanso the bean form re-runs the same check. - New
rqueue.nats.autoCreateKvBucketsfield onRqueueNatsProperties(defaulttrue).rqueue-natsitself never readsrqueue.nats.*keys directly — the property flows in only through the auto-config. - Two enforcement layers in
RqueueNatsAutoConfig:- Inline call to
NatsKvBucketValidator.validate(connection, props.isAutoCreateKvBuckets())inside thenatsConnection@Beanfactory, so validation completes duringConnectionbean creation (strictly before any other NATS bean can inject the connection). @Bean public NatsKvBucketValidator natsKvBucketValidator(...)declared fromRqueueNatsProperties. Five NATS components (NatsRqueueSystemConfigDao,NatsRqueueJobDao,NatsRqueueLockManager,NatsRqueueMessageMetadataService,NatsWorkerRegistryStore) plus theWorkerRegistryStore@Beanfactory carry@DependsOn("natsKvBucketValidator")so they wait on it even when the inline path is bypassed.
- Inline call to
BackendCapabilityException(inrqueue-core/exception/) — carries{backend, operation, reason}. Mapped to HTTP 501 with structured JSON body byRqueueWebExceptionAdvice(inrqueue-web/web/controller/, scoped@RestControllerAdvice(basePackageClasses = ...)). No callers yet — landed as scaffolding for the upcoming web-service repository-interface refactor.
- New "NATS backend" section in
README.mdcovering: the 6 KV buckets (table with name, purpose, TTL behaviour, code link), how buckets are configured (lazy / immutablettl/ connection wiring), pre-create commands for restricted JetStream accounts, therqueue.nats.autoCreateKvBuckets=falseflag and its two-layer enforcement, and a recreate-with-new-TTL recipe.
- Branch
nats-backendpushed toorigin. ~50 commits, all carryAssisted-By: Claude Codeonly (noCo-Authored-By:).CLAUDE.mddocuments the rule.
Same root cause for two of three (cross-module visibility of test fixtures). Pick option 1 below: promote the offenders to rqueue-test-util/src/main.
rqueue-redis:compileTestJava— moved tests referenceCoreUnitTest(annotation) andQueueStatisticsTest(fixture data) still living inrqueue-core/src/test.rqueue-web:compileTestJava—DateTimeFunctionTest,RqueueTaskMetricsAggregatorServiceTest,RqueuePebbleExtensionTestreferenceCoreUnitTestandTestUtils.createQueueDetail, both still inrqueue-core/src/test.rqueue-nats:compileTestJava—JetStreamMessageBrokerDelayThrowsTest:36callsnew JetStreamMessageBroker(Connection, JetStream, JetStreamManagement, RqueueNatsConfig, ObjectMapper)from outside the broker's package. The constructor is package-private (regression from a recent visibility tighten). Fix: widen the constructor topublic, or use the existingJetStreamMessageBroker.builder()API in the test.
Plan for items 1 + 2:
- Move
rqueue-core/src/test/java/com/github/sonus21/rqueue/CoreUnitTest.java→rqueue-test-util/src/main/java/com/github/sonus21/rqueue/CoreUnitTest.java. - Move
rqueue-core/src/test/java/com/github/sonus21/rqueue/utils/TestUtils.java→rqueue-test-util/src/main/java/com/github/sonus21/rqueue/utils/TestUtils.java. - (Optional)
QueueStatisticsTestfixture data — promote helpers if the moved Redis tests still need them. - All consumer modules already pull
rqueue-test-utilastestImplementation, so no build wiring needed.
Then re-run:
./gradlew :rqueue-core:test :rqueue-redis:test :rqueue-web:test :rqueue-nats:test -DincludeTags=unit
./gradlew :rqueue-spring-boot-starter:test --tests "com.github.sonus21.rqueue.spring.boot.integration.NatsBackendEndToEndIT"
All 4 controllers and the 5 web service impls (RqueueDashboardChartService*, RqueueQDetailService*, RqueueJobService*, RqueueSystemManagerService*, RqueueUtilityService*) are still gated @Conditional(RedisBackendCondition). On NATS the dashboard reports broker-derived sizes only; no charts, no message browse, no admin ops. Plan to fix:
- Introduce repository interfaces in
rqueue-core/repository/for the few storage primitives the web services share (queue browsing, time-series counters, atomic move). Web service impls move into core /rqueue-weband depend only on the repos. - Redis impls of the repos stay in
rqueue-redis; NATS impls go inrqueue-natsand throwBackendCapabilityException("nats", "operation", "reason")for primitives JetStream can't model (positional message moves, time-bucket charts). - Drop
@Conditional(RedisBackendCondition)from controllers; the advice already maps the exception to HTTP 501 with a structured body. - Extend the existing
Capabilitiesrecord (MessageBroker.capabilities()) with dashboard-op flags so the front-end can hide unsupported panels instead of relying on 501s. ExposeGET /rqueue/api/capabilities.
Order of operations: easiest first — RqueueSystemManagerService (already mostly goes through RqueueSystemConfigDao), then RqueueJobService, RqueueViewControllerService, then RqueueQDetailService (needs new MessageBrowsingRepository), then RqueueDashboardChartService and RqueueUtilityService.move/enqueue last (these throw on NATS).
spring-configuration-metadata.json has no entry for rqueue.nats.autoCreateKvBuckets. IDE autocomplete won't show it. Easy follow-up: add spring-boot-configuration-processor to the starter's annotation processors if not already wired.
RqueueStringDaointerface — keep Redis-only; document as Redis-internal in javadoc.RqueueMessageMetadataDao,RqueueQStatsDao— no NATS impls needed; all consumers are Redis-only gated. Re-verify in light of the web-layer refactor above.- Reactive listener container — only enqueue side is reactive in v1. Phase 5 territory.
- Delayed/scheduled/cron messages on NATS — throws
UnsupportedOperationException. Out of scope for v1. - Cross-queue
priorityGroupweighting on NATS — boot WARN, not honored. Acceptable for v1. - Elastic
@RqueueListener.concurrency(min < max) — falls back to fixedmaxon NATS. Acceptable. @RqueueHandler(primary)on NATS — ignored, single boot WARN.- PR open on
sonus21/rqueue:nats-backend— branch pushed; user opened the PR through the GitHub UI.
./gradlew :rqueue-core:test :rqueue-redis:test :rqueue-nats:test -DincludeTags=unit
./gradlew :rqueue-spring-boot-starter:test --tests "com.github.sonus21.rqueue.spring.boot.integration.NatsBackendEndToEndIT"
./gradlew :rqueue-nats:test --tests "com.github.sonus21.rqueue.nats.lock.NatsRqueueLockManagerIT"
./gradlew :rqueue-nats:test --tests "com.github.sonus21.rqueue.nats.dao.NatsRqueueSystemConfigDaoIT"
./gradlew :rqueue-nats:test --tests "com.github.sonus21.rqueue.nats.dao.NatsRqueueJobDaoIT"
./gradlew :rqueue-nats:test --tests "com.github.sonus21.rqueue.nats.service.NatsRqueueMessageMetadataServiceIT"
CLAUDE.md at the repo root forbids Co-Authored-By: for any AI tool. Use Assisted-By: Claude Code as a single trailer per commit. The trailer rewrite has already been applied to historical commits; new commits just need the right form.