diff --git a/build.gradle b/build.gradle
index 1b56b7e783..69dd3fbd53 100644
--- a/build.gradle
+++ b/build.gradle
@@ -73,7 +73,8 @@ dependencies {
implementation 'io.seqera:lib-crypto:1.0.0'
implementation 'io.seqera:jedis-lock:1.0.0'
implementation 'io.seqera:lib-data-queue-redis:1.1.2'
- implementation 'io.seqera:lib-data-stream-redis:1.2.0'
+ implementation 'io.seqera:lib-data-workqueue:2.0.0'
+ implementation 'io.seqera:lib-data-workqueue-redis:2.0.0'
implementation 'io.seqera:lib-data-range-redis:1.1.0'
implementation 'io.seqera:lib-jedis-pool:1.0.0'
implementation 'io.micronaut:micronaut-http-client'
diff --git a/docs/install/reference.md b/docs/install/reference.md
index eb60037939..7480ddac2b 100644
--- a/docs/install/reference.md
+++ b/docs/install/reference.md
@@ -425,6 +425,9 @@ Configure Kubernetes-specific settings for Wave. Build and scan processes share
+`wave.build.k8s.read-timeout` *(optional)*
+: Maximum time to wait for data from the Kubernetes API server on an open connection before the request fails (default: `50s`).
+
`wave.build.k8s.resources.requests.cpu` *(optional)*
: [CPU resources](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes) allocated to Wave build processes.
For example, set to `2` (2 CPU cores) or `1500Mi` (1.5 CPU cores).
@@ -745,6 +748,10 @@ Configure the connection to the Seqera license management server:
Configure polling and scheduling intervals for Wave's asynchronous job processing.
+`wave.job-manager.drain-timeout` *(optional)*
+: Maximum time the application shutdown waits for the jobs being launched or checked for completion. Jobs not completed within this time are handed over to another replica (default: `15s`).
+ It must be shorter than `micronaut.lifecycle.graceful-shutdown.grace-period` (default: `20s`).
+
`wave.job-manager.max-running-jobs` *(optional)*
: Maximum number of build, scan, and mirror jobs Wave runs concurrently (default: `20`).
Pair this with a `ResourceQuota` on the build namespace to bound build resource usage.
@@ -755,15 +762,21 @@ Configure polling and scheduling intervals for Wave's asynchronous job processin
`wave.job-manager.scheduler-interval` *(optional)*
: Interval for the job scheduler to process queued jobs (default: `1s`).
-## Message stream
+## Work queue
+
+Configure how Wave consumes messages from the Redis backed work queue used for internal job processing.
+
+Each setting falls back to the legacy `wave.message-stream.*` property it replaces, so a deployment configured for the previous message-stream implementation keeps its values after upgrading with no configuration change.
-Configure how Wave consumes messages from the Redis stream used for internal event processing.
+`wave.work-queue.consumer-group-name` *(optional)*
+: Name of the Redis consumer group used to read the work queues. Falls back to `wave.message-stream.consumer-group-name` (default: `wave-message-stream`).
-`wave.message-stream.claim-timeout` *(optional)*
-: Timeout for claiming messages from the Redis stream (default: `5s`).
+`wave.work-queue.visibility-timeout` *(optional)*
+: How long a claimed message stays invisible to other consumers before it is redelivered. Falls back to `wave.message-stream.claim-timeout` (default: `20s`, previously `5s`).
+ A job that is still running is checked again once this timeout elapses, so a longer value delays the detection of completed jobs. It must exceed a few seconds, as the lease of a message being processed is renewed every quarter of this timeout.
-`wave.message-stream.consume-warn-timeout` *(optional)*
-: Threshold duration after which a slow message consumer triggers a warning (default: `4s`).
+`wave.work-queue.consumer-warn-timeout` *(optional)*
+: Threshold duration after which a slow message consumer triggers a warning. Falls back to `wave.message-stream.consume-warn-timeout` (default: `45s`, previously `4s`).
## Thread monitor
diff --git a/src/main/groovy/io/seqera/wave/configuration/JobManagerConfig.groovy b/src/main/groovy/io/seqera/wave/configuration/JobManagerConfig.groovy
index 4d7ce182c9..7766d8fc12 100644
--- a/src/main/groovy/io/seqera/wave/configuration/JobManagerConfig.groovy
+++ b/src/main/groovy/io/seqera/wave/configuration/JobManagerConfig.groovy
@@ -49,4 +49,12 @@ class JobManagerConfig {
@Value('${wave.job-manager.max-running-jobs:20}')
int maxRunningJobs
+
+ /**
+ * How long the application shutdown waits for the jobs being processed. Keep it below
+ * {@code micronaut.lifecycle.graceful-shutdown.grace-period}: when that expires Micronaut
+ * proceeds to destroy the beans regardless
+ */
+ @Value('${wave.job-manager.drain-timeout:15s}')
+ Duration drainTimeout
}
diff --git a/src/main/groovy/io/seqera/wave/service/data/stream/RedisStreamConfigBean.groovy b/src/main/groovy/io/seqera/wave/service/data/stream/RedisStreamConfigBean.groovy
deleted file mode 100644
index 785574cbd3..0000000000
--- a/src/main/groovy/io/seqera/wave/service/data/stream/RedisStreamConfigBean.groovy
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Wave, containers provisioning service
- * Copyright (c) 2023-2025, Seqera Labs
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see
This configuration provides centralized settings for Redis Stream-based - * messaging components, including consumer group management and timeout - * configurations. The configuration values are injected from application - * properties with sensible defaults. - * - *
Configuration properties: - *
Example application configuration: - *
- * # application.yml - * wave: - * message-stream: - * consumer-group-name: "wave-prod-stream" - * claim-timeout: "10s" - * consume-warn-timeout: "8s" - *- * - *
This bean implements {@link RedisStreamConfig} to provide configuration
- * values to Redis Stream components throughout the application.
- *
- * @author Paolo Di Tommaso This class extends {@link AbstractMessageStream} and provides a foundation for
- * creating type-safe message streams with automatic JSON serialization/deserialization
+ * Base abstract class for implementing work queues in the Wave application.
+ *
+ * This class extends {@link AbstractWorkQueue} and provides a foundation for
+ * creating type-safe work queues with automatic JSON serialization/deserialization
* using the Moshi library. It handles the encoding strategy configuration and
- * provides a consistent interface for message stream implementations. Message streams are used for real-time message processing and event handling,
- * allowing for continuous data flow and stream-based operations. Concrete implementations
- * should extend this class to define specific streaming behavior and message processing logic.
Work queues deliver each message to exactly one consumer at a time under a + * message lease: a message is removed from the queue only when the consumer settles + * it as acknowledged, and is otherwise redelivered once the lease expires. Concrete + * implementations should extend this class to define specific queueing behaviour and + * message processing logic.
+ * + * @paramThis method uses reflection to determine the generic type parameter {@code M} * and creates a Moshi-based encoding strategy that can handle the automatic * conversion between the strongly-typed message objects and their JSON string - * representations for streaming operations.
- * + * representations for queueing operations. + * * @return a new instance of {@link StringEncodingStrategy} configured for type {@code M} */ @Override diff --git a/src/main/groovy/io/seqera/wave/service/data/workqueue/RedisWorkQueueConfigBean.groovy b/src/main/groovy/io/seqera/wave/service/data/workqueue/RedisWorkQueueConfigBean.groovy new file mode 100644 index 0000000000..d1eccb08e0 --- /dev/null +++ b/src/main/groovy/io/seqera/wave/service/data/workqueue/RedisWorkQueueConfigBean.groovy @@ -0,0 +1,90 @@ +/* + * Wave, containers provisioning service + * Copyright (c) 2023-2025, Seqera Labs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, seeThis configuration provides centralized settings for Redis work queue + * components, including consumer group management and lease timings. The + * configuration values are injected from application properties with sensible + * defaults. + * + *
Configuration properties (each falls back to the legacy {@code wave.message-stream.*} + * property it replaces, then to the default shown): + *
The fallback to the legacy {@code message-stream} keys means a deployment configured for the + * message-stream implementation keeps its values after the work-queue upgrade with no config change; + * the visibility timeout default (20s) bounds the redelivery delay of a job re-check, while leaving + * room for missed lease renewals (renewed every visibility timeout / 4). + * + *
Note the consumer group name default is intentionally {@code wave-message-stream}: + * the work queue reuses the Redis streams and consumer group created by the message + * stream implementation it replaces, so messages queued before an upgrade are still + * delivered afterwards. + * + *
Example application configuration: + *
+ * # application.yml + * wave: + * work-queue: + * consumer-group-name: "wave-prod-queue" + * visibility-timeout: "10s" + * consumer-warn-timeout: "8s" + *+ * + *
This bean implements {@link RedisWorkQueueConfig} to provide configuration
+ * values to Redis work queue components throughout the application.
+ *
+ * @author Paolo Di Tommaso Jobs not completed within the drain timeout keep their queue entry and are
+ * redelivered to another replica once their lease expires.
+ */
+ @Override
+ CompletionStage> shutdownGracefully() {
+ log.info "Draining job queues - timeout=${config.drainTimeout}"
+ // close both queues in parallel, Micronaut invokes each shutdown hook on the same thread
+ return CompletableFuture.allOf(
+ CompletableFuture.runAsync(() -> pendingQueue.close(config.drainTimeout)),
+ CompletableFuture.runAsync(() -> processingQueue.close(config.drainTimeout)) )
}
@TraceElapsedTime(thresholdMillis = '${wave.trace.k8s.threshold:500}')
protected boolean launchJob0(JobSpec job) {
+ // the processing queue is shared by all replicas and holds one entry per running job, so
+ // `maxRunningJobs` is a cluster-wide limit, not a per-replica one. It is a soft limit: the
+ // length is read before launching, so replicas launching concurrently can overshoot it
final processingQueueLen = processingQueue.length()
final canLaunchNewJobs = processingQueueLen < config.maxRunningJobs
if( canLaunchNewJobs ) {
diff --git a/src/main/groovy/io/seqera/wave/service/job/JobPendingQueue.groovy b/src/main/groovy/io/seqera/wave/service/job/JobPendingQueue.groovy
index 5b0712de98..ed99dd5232 100644
--- a/src/main/groovy/io/seqera/wave/service/job/JobPendingQueue.groovy
+++ b/src/main/groovy/io/seqera/wave/service/job/JobPendingQueue.groovy
@@ -23,13 +23,11 @@ import java.time.Duration
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.context.annotation.Requires
-import io.seqera.data.stream.MessageConsumer
-import io.seqera.data.stream.MessageStream
-import io.seqera.serde.encode.StringEncodingStrategy
+import io.seqera.data.workqueue.MessageConsumer
+import io.seqera.data.workqueue.WorkQueue
import io.seqera.wave.configuration.JobManagerConfig
import io.seqera.wave.configuration.WaveLite
-import io.seqera.serde.moshi.MoshiEncodeStrategy
-import io.seqera.wave.service.data.stream.BaseMessageStream
+import io.seqera.wave.service.data.workqueue.BaseWorkQueue
import jakarta.annotation.PreDestroy
import jakarta.inject.Singleton
/**
@@ -42,17 +40,14 @@ import jakarta.inject.Singleton
@Slf4j
@Singleton
@CompileStatic
-class JobPendingQueue extends BaseMessageStream