From bb64fa81b35de36a79a6c1a4d7000b8ea56da306 Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 1 Oct 2026 10:46:13 -0700 Subject: [PATCH 1/2] Java 1.2 documentation - Upgrade guide: new "Upgrading to v1.2" section covering the 1.1 waypoint, migrations 113-114, payloads moving to workflow_input / workflow_output, the debouncer's DELAYED-workflow design and 1.1 takeover, child workflows inheriting the parent's deadline, export compatibility, new record components, the database clock, and a deprecations table. - QueueOptions: every example uses new QueueOptions() and the plain-value with... builders; the reference documents withRateLimitMax/Period, the (Integer) null clearing idiom, and the deprecated set/and/Field forms. - Timeouts: children inherit the parent's deadline, the precedence rules, and withDeadline marked deprecated on StartWorkflowOptions, EnqueueOptions and WorkflowOptions. - Debouncer: Debouncer.withTimeout, setter-time validation, internal queue default, withDeduplicationId ignored, how a debounce is stored. - ListWorkflowsInput.withIsFork; WorkflowStatus.isDebounced and debounceDeadline. - Version pins 1.1.0 -> 1.2.0; deprecations now say "a future release" instead of 2.0. --- docs/explanations/migrating-from-temporal.md | 2 +- docs/java/integrating-dbos.md | 4 +- docs/java/programming-guide.md | 6 +- docs/java/prompting.md | 93 ++++++++------- docs/java/reference/client.md | 12 +- docs/java/reference/methods.md | 35 ++++-- docs/java/reference/plugins.md | 4 +- docs/java/reference/queues.md | 89 +++++++------- docs/java/reference/workflows-steps.md | 23 ++-- docs/java/tutorials/kotlin.md | 4 +- docs/java/tutorials/queue-tutorial.md | 49 ++++---- .../java/tutorials/spring-boot-integration.md | 6 +- docs/java/tutorials/workflow-tutorial.md | 3 +- docs/java/upgrading.md | 109 +++++++++++++++++- 14 files changed, 294 insertions(+), 145 deletions(-) diff --git a/docs/explanations/migrating-from-temporal.md b/docs/explanations/migrating-from-temporal.md index f0e9d8731..cdea0b7ba 100644 --- a/docs/explanations/migrating-from-temporal.md +++ b/docs/explanations/migrating-from-temporal.md @@ -831,7 +831,7 @@ Learn more in the [queues tutorial](../golang/tutorials/queue-tutorial.md). ```java // Register a queue with concurrency limits (after dbos.launch()) -dbos.registerQueue("order-processing", QueueOptions.setConcurrency(10)); +dbos.registerQueue("order-processing", new QueueOptions().withConcurrency(10)); // Enqueue a workflow WorkflowHandle handle = dbos.startWorkflow( diff --git a/docs/java/integrating-dbos.md b/docs/java/integrating-dbos.md index f8c901279..87984aa44 100644 --- a/docs/java/integrating-dbos.md +++ b/docs/java/integrating-dbos.md @@ -13,7 +13,7 @@ Add DBOS to your application by including it in your build configuration. ```groovy dependencies { - implementation 'dev.dbos:transact:1.1.0' + implementation 'dev.dbos:transact:1.2.0' } ``` @@ -23,7 +23,7 @@ dependencies { dev.dbos transact - 1.1.0 + 1.2.0 ``` diff --git a/docs/java/programming-guide.md b/docs/java/programming-guide.md index c2039ad7c..a819a25e3 100644 --- a/docs/java/programming-guide.md +++ b/docs/java/programming-guide.md @@ -25,7 +25,7 @@ Then, install DBOS (plus Logback for logging) by adding the following to your `a ```kotlin dependencies { - implementation("dev.dbos:transact:1.1.0") + implementation("dev.dbos:transact:1.2.0") implementation("org.slf4j:slf4j-simple:2.0.17") // needed to see DBOS log messages implementation("io.javalin:javalin:7.0.1") // needed for creating HTTP endpoint later in the guide @@ -120,7 +120,7 @@ Now, build and run this code with: Your program should print output like: ```shell -[main] INFO dev.dbos.transact.DBOS - Launching DBOS v1.1.0 +[main] INFO dev.dbos.transact.DBOS - Launching DBOS v1.2.0 [main] INFO dev.dbos.transact.execution.DBOSExecutor - DBOS Executor starting [main] INFO dev.dbos.transact.execution.DBOSExecutor - System Database: jdbc:postgresql://localhost:5432/dbos_java_starter [main] INFO dev.dbos.transact.execution.DBOSExecutor - System Database User name: postgres @@ -312,7 +312,7 @@ public class App { config.events.serverStarting(() -> { dbos.launch(); // Queues are stored in the system database, so register them after launch - dbos.registerQueue("example-queue", QueueOptions.empty()); + dbos.registerQueue("example-queue", new QueueOptions()); }); config.events.serverStopping(dbos::shutdown); config.routes.get("/", ctx -> { diff --git a/docs/java/prompting.md b/docs/java/prompting.md index 168631011..ec58a11a9 100644 --- a/docs/java/prompting.md +++ b/docs/java/prompting.md @@ -69,7 +69,7 @@ The main addition to any project is additional dependencies. For **Gradle** (`build.gradle`): ```groovy dependencies { - implementation 'dev.dbos:transact:1.1.0' + implementation 'dev.dbos:transact:1.2.0' } ``` @@ -79,7 +79,7 @@ For **Maven** (`pom.xml`): dev.dbos transact - 1.1.0 + 1.2.0 ``` @@ -409,11 +409,11 @@ Create workflow options with all fields set to their defaults. Timeouts are **start-to-completion**: if a workflow is enqueued, the timeout does not begin until the workflow is dequeued and starts execution. Also, timeouts are **durable**: they are stored in the database and persist across restarts, so workflows can have very long timeouts. - Timeout deadlines are propagated to child workflows by default. To detach a child workflow from its parent's timeout, start it with its own explicit timeout or use `withNoTimeout()`. + A child workflow without a timeout of its own inherits its parent's deadline (not its timeout), so it can't outlive its parent, even while waiting in a queue. To detach a child workflow from its parent's deadline, start it with its own explicit timeout or use `withNoTimeout()`. - **`withNoTimeout()`** - Explicitly remove any inherited timeout from this workflow. -- **`withDeadline(Instant deadline)`** - Set an absolute deadline for this workflow. The workflow and all its children are cancelled if still running at the deadline. +- Do NOT use `withDeadline`; it is deprecated since 1.2. Use `withTimeout`. - **`withDeduplicationId(String deduplicationId)`** - May only be used when enqueuing. At any given time, only one workflow with a specific deduplication ID can be enqueued in the specified queue. @@ -861,7 +861,7 @@ Do NOT use `new Queue(...)`, `dbos.registerQueue(Queue)`, `dbos.registerQueues(. ```java dbos.launch(); -dbos.registerQueue("example-queue", QueueOptions.setWorkerConcurrency(5)); +dbos.registerQueue("example-queue", new QueueOptions().withWorkerConcurrency(5)); ``` ### dbos.registerQueue @@ -873,7 +873,7 @@ void registerQueue(String name, QueueOptions options, QueueConflictResolution on Register a queue in the system database. Must be called after `dbos.launch()`. Queue names must be unique within the system database; `_dbos_internal_queue` is reserved. -To register a queue with default settings (no limits), pass `QueueOptions.empty()`. +To register a queue with default settings (no limits), pass `new QueueOptions()`. If a queue with this name already exists in the database, `onConflict` controls whether its configuration is overwritten: - `QueueConflictResolution.UPDATE_IF_LATEST_VERSION` (default): overwrite only when this process runs the latest registered application version. Safe for rolling deploys. @@ -883,27 +883,30 @@ If a queue with this name already exists in the database, `onConflict` controls ### QueueOptions `QueueOptions` holds a queue's configuration. -Build it with a static `set...` factory and chain `and...` methods: +Build it with `new QueueOptions()` and chain `with...` methods: ```java -QueueOptions.setConcurrency(10) - .andWorkerConcurrency(2) - .andRateLimit(100, Duration.ofSeconds(60)); +new QueueOptions() + .withConcurrency(10) + .withWorkerConcurrency(2) + .withRateLimit(100, Duration.ofSeconds(60)); ``` -Static factories (each has a matching `and...` method for chaining): +Builders: -- **`setConcurrency(Integer value)`**: The maximum number of workflows from this queue that may run concurrently across all DBOS processes. -- **`setWorkerConcurrency(Integer value)`**: The maximum number of workflows from this queue that may run concurrently within a single DBOS process. If `concurrency` is also set, must not exceed it. -- **`setRateLimit(Integer max, Duration period)`** / **`setRateLimit(int limit, long period, TimeUnit unit)`**: The maximum number of workflows that may be started from this queue in a rolling period, across all processes. -- **`setPartitionConcurrency(Integer value)`**: The maximum number of workflows from any one partition that may run concurrently across all processes. -- **`setPartitionWorkerConcurrency(Integer value)`**: The maximum number of workflows from any one partition that may run concurrently within a single process. -- **`setPartitionRateLimit(Integer max, Duration period)`** / **`setPartitionRateLimit(int limit, long period, TimeUnit unit)`**: The maximum number of workflows that may be started from any one partition in a rolling period. -- **`setPriorityEnabled(boolean value)`**: Deprecated since 1.1 and ignored. Every queue already dequeues in priority order; do not set it. -- **`setPollingInterval(Duration value)`**: How often workers poll the database for new workflows on this queue. Defaults to 1 second. -- **`empty()`**: No settings. +- **`withConcurrency(Integer value)`**: The maximum number of workflows from this queue that may run concurrently across all DBOS processes. +- **`withWorkerConcurrency(Integer value)`**: The maximum number of workflows from this queue that may run concurrently within a single DBOS process. If `concurrency` is also set, must not exceed it. +- **`withRateLimit(Integer max, Duration period)`** / **`withRateLimit(int limit, long period, TimeUnit unit)`**: The maximum number of workflows that may be started from this queue in a rolling period, across all processes. +- **`withPartitionConcurrency(Integer value)`**: The maximum number of workflows from any one partition that may run concurrently across all processes. +- **`withPartitionWorkerConcurrency(Integer value)`**: The maximum number of workflows from any one partition that may run concurrently within a single process. +- **`withPartitionRateLimit(Integer max, Duration period)`** / **`withPartitionRateLimit(int limit, long period, TimeUnit unit)`**: The maximum number of workflows that may be started from any one partition in a rolling period. +- **`withRateLimitMax(Integer max)`** / **`withRateLimitPeriod(Duration period)`** (and `withPartitionRateLimitMax` / `withPartitionRateLimitPeriod`): Only for `updateQueue` on a queue that already has that rate limit: change one half and keep the other. +- **`withPollingInterval(Duration value)`**: How often workers poll the database for new workflows on this queue. Defaults to 1 second. +- `new QueueOptions()` with no builders: no settings. -Do NOT use `setPartitionQueue`/`andPartitionQueue`; they are deprecated. Setting any per-partition limit is what partitions a queue. +On `updateQueue`, passing `null` clears a limit. Write it with a cast, such as `withConcurrency((Integer) null)`, because a bare `null` is ambiguous with the deprecated overloads. + +Do NOT use `QueueOptions.empty()`, the static `set...` factories, or the `and...` methods; they are deprecated since 1.2. Do NOT use `setPartitionQueue`/`andPartitionQueue`/`withPartitionQueue` or the `priorityEnabled` options; they are deprecated since 1.1. Setting any per-partition limit is what partitions a queue, and every queue dequeues in priority order. ### Enqueueing Workflows @@ -979,7 +982,7 @@ ExampleImpl impl = new ExampleImpl(dbos); Example proxy = dbos.registerProxy(Example.class, impl); impl.setProxy(proxy); dbos.launch(); -dbos.registerQueue("task-queue", QueueOptions.setWorkerConcurrency(5)); +dbos.registerQueue("task-queue", new QueueOptions().withWorkerConcurrency(5)); ``` ### Reconfiguring Queues at Runtime @@ -997,8 +1000,8 @@ boolean deleteQueue(String name) `updateQueue` writes only the fields set in `options`; fields not set are left unchanged. To clear a limit, set it to `null`: ```java -dbos.updateQueue("example-queue", QueueOptions.setConcurrency(20)); -dbos.updateQueue("example-queue", QueueOptions.setRateLimit(null, null)); // remove the rate limit +dbos.updateQueue("example-queue", new QueueOptions().withConcurrency(20)); +dbos.updateQueue("example-queue", new QueueOptions().withRateLimit(null, null)); // remove the rate limit ``` `findQueue` returns a `Queue` record, a snapshot of the queue's configuration with accessors such as `concurrency()`, `workerConcurrency()`, `rateLimit()`, `partitionConcurrency()`, `partitionWorkerConcurrency()`, `partitionRateLimit()`, and `pollingInterval()`. @@ -1027,7 +1030,7 @@ var client = new DBOSClient( // The name of the application that runs the data pipeline "data-processing-service"); -client.registerQueue("pipeline-queue", QueueOptions.empty()); +client.registerQueue("pipeline-queue", new QueueOptions()); var options = new EnqueueOptions( "dataPipeline", "com.example.PipelineImpl", QueueName.of("pipeline-queue")); @@ -1051,7 +1054,7 @@ This is particularly useful for resource-intensive workflows to avoid exhausting For example, this queue has a worker concurrency of 5, so each process will run at most 5 workflows from this queue simultaneously: ```java -dbos.registerQueue("example-queue", QueueOptions.setWorkerConcurrency(5)); +dbos.registerQueue("example-queue", new QueueOptions().withWorkerConcurrency(5)); ``` #### Global Concurrency @@ -1065,7 +1068,7 @@ Take care when using a global concurrency limit as any `PENDING` workflow on the ::: ```java -dbos.registerQueue("example-queue", QueueOptions.setConcurrency(10)); +dbos.registerQueue("example-queue", new QueueOptions().withConcurrency(10)); ``` #### In-Order Processing @@ -1076,7 +1079,7 @@ For example, this processes events sequentially in the order of their arrival: ```java dbos.launch(); -dbos.registerQueue("in-order-queue", QueueOptions.setConcurrency(1)); +dbos.registerQueue("in-order-queue", new QueueOptions().withConcurrency(1)); // Called for each incoming event, for example from an HTTP handler public void onEvent(String event) { @@ -1094,7 +1097,7 @@ For example, this queue has a limit of 100 workflows with a period of 60 seconds ```java dbos.registerQueue("example-queue", - QueueOptions.setRateLimit(100, Duration.ofSeconds(60))); + new QueueOptions().withRateLimit(100, Duration.ofSeconds(60))); ``` Rate limits are especially useful when working with a rate-limited API, such as many LLM APIs. @@ -1119,7 +1122,7 @@ WorkflowHandle handle = dbos.startWorkflow( ### Partitioning Queues You can partition a queue to apply flow control separately to each partition key, for example to run at most one workflow at a time per user. -Setting any per-partition limit (`setPartitionConcurrency`, `setPartitionWorkerConcurrency`, or `setPartitionRateLimit`) partitions the queue. +Setting any per-partition limit (`withPartitionConcurrency`, `withPartitionWorkerConcurrency`, or `withPartitionRateLimit`) partitions the queue. Every workflow enqueued on a partitioned queue must have a partition key, set with `withQueuePartitionKey`; workflows on other queues must not have one. A partitioned queue can also have queue-wide limits, which bound the queue as a whole across all partitions. @@ -1127,7 +1130,7 @@ For example, this queue runs at most one workflow per user at a time, and at mos ```java dbos.registerQueue("per-user-queue", - QueueOptions.setConcurrency(20).andPartitionConcurrency(1)); + new QueueOptions().withConcurrency(20).withPartitionConcurrency(1)); dbos.startWorkflow( () -> proxy.processTask(task), @@ -1140,7 +1143,7 @@ For example, this "fair queue" runs at most one task per user, but no more than ```java dbos.registerQueue("fair-queue", - QueueOptions.setPartitionConcurrency(1).andWorkerConcurrency(10)); + new QueueOptions().withPartitionConcurrency(1).withWorkerConcurrency(10)); ``` Each queue-wide limit has a per-partition counterpart, so you can mix and match them freely: @@ -1150,12 +1153,13 @@ Each queue-wide limit has a per-partition counterpart, so you can mix and match // at most 10 tasks running per process and 2 per tenant per process, // and at most 1000 tasks started per minute globally and 50 per tenant. dbos.registerQueue("tenant-queue", - QueueOptions.setConcurrency(100) - .andWorkerConcurrency(10) - .andRateLimit(1000, Duration.ofSeconds(60)) - .andPartitionConcurrency(25) - .andPartitionWorkerConcurrency(2) - .andPartitionRateLimit(50, Duration.ofSeconds(60))); + new QueueOptions() + .withConcurrency(100) + .withWorkerConcurrency(10) + .withRateLimit(1000, Duration.ofSeconds(60)) + .withPartitionConcurrency(25) + .withPartitionWorkerConcurrency(2) + .withPartitionRateLimit(50, Duration.ofSeconds(60))); ``` When both are set, each per-partition concurrency limit must be less than or equal to its queue-wide counterpart, and `partitionWorkerConcurrency` must be less than or equal to `partitionConcurrency`; limits that are not set are not compared. @@ -1255,8 +1259,8 @@ if ("gpu".equals(workerType)) { DBOS dbos = new DBOS(config); // ... register workflows ... dbos.launch(); -dbos.registerQueue("cpu-queue", QueueOptions.empty()); -dbos.registerQueue("gpu-queue", QueueOptions.empty()); +dbos.registerQueue("cpu-queue", new QueueOptions()); +dbos.registerQueue("gpu-queue", new QueueOptions()); ``` Note that `withListenQueues` only controls what workflows are dequeued, not what workflows can be enqueued, so you can freely enqueue tasks onto the GPU queue from a CPU worker for execution on a GPU worker, and vice versa. @@ -1335,8 +1339,8 @@ The constructors fix what to run and where: the workflow name, optionally its cl - **`withWorkflowId(String workflowId)`**: Specify the idempotency ID to assign to the enqueued workflow. - **`withAppVersion(String appVersion)`**: The version of your application that should process this workflow. If not set, only an executor on the owning application's latest registered version dequeues it. -- **`withTimeout(Duration timeout)`** / **`withTimeout(Timeout timeout)`** / **`withNoTimeout()`**: Set a timeout for the enqueued workflow. Does not begin until the workflow is dequeued and starts execution. Inside a workflow, an unset timeout inherits the caller's; `withNoTimeout()` declines it. -- **`withDeadline(Instant deadline)`**: Set an absolute deadline for the enqueued workflow. +- **`withTimeout(Duration timeout)`** / **`withTimeout(Timeout timeout)`** / **`withNoTimeout()`**: Set a timeout for the enqueued workflow. Does not begin until the workflow is dequeued and starts execution. Inside a workflow, an unset timeout inherits the caller's deadline; `withNoTimeout()` declines it. +- Do NOT use `withDeadline`; it is deprecated since 1.2. Use `withTimeout`. - **`withDelay(Duration delay)`**: Delay the start of the workflow by the specified duration after it is dequeued. - **`withDeduplicationId(String deduplicationId)`**: At any given time, only one workflow with a specific deduplication ID can be enqueued in the specified queue. - **`withPriority(Integer priority)`**: Priority values range from `0` to `2,147,483,647`; lower numbers run first, and a negative priority throws. @@ -1443,7 +1447,9 @@ public record WorkflowStatus( String serialization, // Serialization format used for inputs/outputs Map attributes, // Custom key-value attributes attached at creation, if any String scheduleName, // The schedule that started this workflow, if any - String applicationName // The application that owns this workflow, or null if unclaimed + String applicationName, // The application that owns this workflow, or null if unclaimed + Boolean isDebounced, // Whether this is a debounced workflow + Instant debounceDeadline // The latest a debounced workflow's start may be pushed back to, if capped ) ``` @@ -1519,6 +1525,7 @@ Retrieve a list of WorkflowStatus of all workflows matching specified criteria. - `withForkedFrom(String id)` / `withForkedFrom(List)` — filter to workflows forked from these IDs - `withParentWorkflowId(String id)` / `withParentWorkflowId(List)` — filter to child workflows of these parents - `withWasForkedFrom(Boolean value)` — filter to workflows that have been forked from +- `withIsFork(Boolean value)` — filter to workflows that are (or are not) forks - `withHasParent(Boolean value)` — filter to child workflows diff --git a/docs/java/reference/client.md b/docs/java/reference/client.md index 37bdabffa..14be38271 100644 --- a/docs/java/reference/client.md +++ b/docs/java/reference/client.md @@ -113,8 +113,8 @@ The workflow name and queue must not be null or empty. - **`withAppVersion(String appVersion)`**: The version of your application that should process this workflow. If left undefined, the workflow is enqueued without a version and is only dequeued by an executor running the owning application's latest registered version, which sets the version when it first dequeues it. - **`withTimeout(Duration timeout)`**, **`withTimeout(long value, TimeUnit unit)`**: Set an explicit timeout for the enqueued workflow. When the timeout expires, the workflow and all its children are cancelled. The timeout does not begin until the workflow is dequeued and starts execution. -- **`withTimeout(Timeout timeout)`**, **`withNoTimeout()`**: Set the timeout as a [`Timeout`](./methods.md#timeout): explicit, none, or inherit. Inside a workflow, [`dbos.enqueueWorkflow`](./methods.md#enqueueworkflow) resolves it as `startWorkflow` does, so an unset timeout inherits the enqueuing workflow's and `withNoTimeout()` declines it. From a client there is nothing to inherit, so an unset or inherited timeout means no timeout. -- **`withDeadline(Instant deadline)`**: Set a deadline for the enqueued workflow. If the workflow is executing when the deadline arrives, the workflow and all its children are cancelled. +- **`withTimeout(Timeout timeout)`**, **`withNoTimeout()`**: Set the timeout as a [`Timeout`](./methods.md#timeout): explicit, none, or inherit. Inside a workflow, [`dbos.enqueueWorkflow`](./methods.md#enqueueworkflow) resolves it as `startWorkflow` does, so an unset timeout inherits the enqueuing workflow's deadline and `withNoTimeout()` declines it. From a client there is nothing to inherit, so an unset or inherited timeout means no timeout. +- **`withDeadline(Instant deadline)`** *(deprecated since 1.2)*: Use `withTimeout` instead; no other DBOS SDK lets a caller set a deadline. Set a deadline for the enqueued workflow. If the workflow is executing when the deadline arrives, the workflow and all its children are cancelled. :::info An explicit timeout and a deadline cannot both be set. @@ -567,13 +567,13 @@ Create a `DebouncerClient` for the named workflow. Similar to [`dbos.debouncer() - **`withClassName(String className)`**: The fully-qualified Java class name of the workflow implementation. **Required** — must be set before calling `debounce`. - **`withInstanceName(String instanceName)`**: The DBOS instance name of the target workflow implementation. - **`withDebounceTimeout(Duration debounceTimeout)`**: Set an absolute cap on how long the debouncer may keep absorbing calls for a single key. -- **`withQueue(QueueName queue)`** / **`withQueue(String queueName)`**: Enqueue the user workflow on the specified queue when the debounce period elapses. `withQueue(Queue queue)` is *(deprecated since 1.1)*. -- **`withTimeout(Duration timeout)`**: Set a timeout for the user workflow. +- **`withQueue(QueueName queue)`** / **`withQueue(String queueName)`**: The queue the user workflow waits and runs on. Without one, it uses the DBOS internal queue. `withQueue(Queue queue)` is *(deprecated since 1.1)*. +- **`withTimeout(Duration timeout)`**: Set a timeout for the user workflow, timed from when it is dequeued. A zero or negative timeout throws `IllegalArgumentException` when set. - **`withAppVersion(String appVersion)`**: Target a specific application version. -- **`withPriority(Integer priority)`**: Set the priority for the user workflow. A priority requires a queue: if a priority is set without `withQueue`, `debounce` throws `IllegalArgumentException`. +- **`withPriority(Integer priority)`**: Set the priority for the user workflow. A negative priority throws `IllegalArgumentException` when set. A priority requires a queue: if a priority is set without `withQueue`, `debounce` throws `IllegalArgumentException`. - **`withAttributes(Map attributes)`**: Attach custom JSON-serializable key-value metadata to the user workflow. - **`withSerialization(SerializationStrategy serialization)`**: The [serialization strategy](./methods.md#serialization-strategy) for the user workflow's arguments. It should match the strategy the workflow is registered with. -- **`withDeduplicationId(String deduplicationId)`** *(deprecated since 1.1)*: Set a deduplication ID forwarded to the user workflow. This will be ignored from the next release, where the debouncer sets the deduplication ID itself, and removed in 2.0. +- **`withDeduplicationId(String deduplicationId)`** *(deprecated since 1.1)*: Ignored since 1.2. The debounced workflow holds its debounce key as its deduplication ID. ### DebouncerClient.debounce diff --git a/docs/java/reference/methods.md b/docs/java/reference/methods.md index f2818c470..9550b9cc4 100644 --- a/docs/java/reference/methods.md +++ b/docs/java/reference/methods.md @@ -317,7 +317,11 @@ public record WorkflowStatus( // The name of the schedule that started this workflow, if any String scheduleName, // The application that owns this workflow, or null if no application owns it - String applicationName + String applicationName, + // Whether this is a debounced workflow, whose deduplication ID is its debounce key + Boolean isDebounced, + // The latest a debounced workflow's start may be pushed back to, or null for no cap + Instant debounceDeadline ) ``` @@ -517,6 +521,14 @@ ListWorkflowsInput withWasForkedFrom(Boolean wasForkedFrom) Filter to workflows from which another workflow was forked. +#### withIsFork + +```java +ListWorkflowsInput withIsFork(Boolean isFork) +``` + +Filter to workflows that are forks of another workflow (`true`), or that are not (`false`). + #### withHasParent ```java @@ -929,10 +941,13 @@ Obtain a `Debouncer` via `dbos.debouncer()`: `Debouncer` is an immutable builder. Configure it with the following methods before calling `debounce`: - **`withDebounceTimeout(Duration debounceTimeout)`**: Set an absolute cap on how long the debouncer may keep absorbing calls for a single key. After this duration elapses from the first call, the user workflow starts regardless of further incoming calls. -- **`withQueue(QueueName queue)`** / **`withQueue(String queueName)`**: Enqueue the user workflow on the specified queue when the debounce period elapses instead of starting it directly. `withQueue(Queue queue)` is *(deprecated since 1.1)*. +- **`withQueue(QueueName queue)`** / **`withQueue(String queueName)`**: The queue the user workflow waits and runs on. Without one, it uses the DBOS internal queue. `withQueue(Queue queue)` is *(deprecated since 1.1)*. - **`withAppVersion(String appVersion)`**: Target a specific application version for the user workflow. -- **`withPriority(Integer priority)`**: Set the priority for the user workflow. A priority requires a queue: if a priority is set without `withQueue`, `debounce` throws `IllegalArgumentException`. -- **`withDeduplicationId(String deduplicationId)`** *(deprecated since 1.1)*: Set a deduplication ID forwarded to the user workflow. This will be ignored from the next release, where the debouncer sets the deduplication ID itself, and removed in 2.0. +- **`withPriority(Integer priority)`**: Set the priority for the user workflow. A negative priority throws `IllegalArgumentException` when set. A priority requires a queue: if a priority is set without `withQueue`, `debounce` throws `IllegalArgumentException`. +- **`withTimeout(Duration timeout)`**: Set a timeout for every user workflow this debouncer starts, timed from when that workflow is dequeued. It takes precedence over a timeout set with [`WorkflowOptions`](./workflows-steps.md#workflowoptions) around the `debounce` call, which applies when this isn't set. A zero or negative timeout throws `IllegalArgumentException` when set. +- **`withDeduplicationId(String deduplicationId)`** *(deprecated since 1.1)*: Ignored since 1.2. The debounced workflow holds its debounce key as its deduplication ID. + +The user workflow never inherits the calling workflow's timeout or deadline, and a deadline set with `WorkflowOptions` around the call is ignored, because the workflow may start long after the call. ### debouncer.debounce @@ -949,6 +964,10 @@ The workflow may be debounced again, which further delays its execution (up to ` When the workflow eventually executes, it uses the **last** set of inputs passed into `debounce`. After the workflow begins execution, the next call to `debounce` starts the debouncing process again for a new workflow execution. +The first call on a key enqueues the user workflow itself in the `DELAYED` state on its queue, with `workflowName-debounceKey` as its [deduplication ID](../tutorials/queue-tutorial.md#deduplication). +Each later call on the key pushes back its start and replaces its arguments. +When the delay expires, the workflow becomes `ENQUEUED`, releases the key, and runs like any other queued workflow. + **Parameters:** - **debounceKey**: A key used to group workflow executions that will be debounced together. For example, if the debounce key is set to customer ID, each customer's workflows are debounced separately. - **debouncePeriod**: Inactivity window before the user workflow runs; each call resets it. Must be positive. @@ -1043,13 +1062,15 @@ import dev.dbos.transact.workflow.Timeout; - **`Timeout.of(Duration duration)`** — Set an explicit timeout of the given duration. - **`Timeout.of(long value, TimeUnit unit)`** — Set an explicit timeout. -- **`Timeout.none()`** — Opt out of any inherited timeout. The workflow will run without a timeout regardless of what the calling context specifies. -- **`Timeout.inherit()`** — Explicitly inherit the timeout from the calling context (the default behavior when no timeout is set). +- **`Timeout.none()`** — Run without a timeout, and don't inherit the calling workflow's deadline. A deadline set on the same options still applies. +- **`Timeout.inherit()`** — Bound the workflow by the calling workflow's deadline, if any (the default behavior when no timeout is set). Outside a workflow, this means no timeout. + +A child workflow inherits its parent's deadline, not its timeout: a queued child doesn't start a fresh copy of the parent's timeout when it is dequeued. **Example:** ```java -// Detach a child workflow from the parent's timeout +// Detach a child workflow from the parent's deadline dbos.startWorkflow(() -> proxy.longRunningChild(), new StartWorkflowOptions().withTimeout(Timeout.none())); ``` diff --git a/docs/java/reference/plugins.md b/docs/java/reference/plugins.md index 82cf331a2..134d71b76 100644 --- a/docs/java/reference/plugins.md +++ b/docs/java/reference/plugins.md @@ -180,8 +180,8 @@ ExternalState upsertExternalState(ExternalState state) ``` :::warning Deprecated -`upsertExternalState`, `getExternalState`, and `ExternalState` are *(deprecated since 1.1)* and will be removed in DBOS Java 2.0. -The system database table behind them, `event_dispatch_kv`, is being retired: it holds dispatch bookkeeping for in-memory event receivers that the other DBOS SDKs have removed or never implemented, and a shared system database migration will drop it sometime after Java 2.0. +`upsertExternalState`, `getExternalState`, and `ExternalState` are *(deprecated since 1.1)* and will be removed in a future release. +The system database table behind them, `event_dispatch_kv`, is being retired: it holds dispatch bookkeeping for in-memory event receivers that the other DBOS SDKs have removed or never implemented, and a shared system database migration will drop it sometime after the Java API is removed. Store integration state in your own table instead. ::: diff --git a/docs/java/reference/queues.md b/docs/java/reference/queues.md index c5473927a..508d87df1 100644 --- a/docs/java/reference/queues.md +++ b/docs/java/reference/queues.md @@ -31,8 +31,9 @@ If the queue already exists in the database, the `onConflict` parameter controls ```java dbos.registerQueue("email", - QueueOptions.setConcurrency(10) - .andRateLimit(100, Duration.ofSeconds(60))); + new QueueOptions() + .withConcurrency(10) + .withRateLimit(100, Duration.ofSeconds(60))); ``` ### dbos.updateQueue @@ -47,7 +48,13 @@ Update the configuration of an existing queue. Only fields set on `options` are ```java // Change only the concurrency — rate limit and other fields are untouched -dbos.updateQueue("email", QueueOptions.setConcurrency(20)); +dbos.updateQueue("email", new QueueOptions().withConcurrency(20)); + +// Change only the rate limit's max — the stored period is kept +dbos.updateQueue("email", new QueueOptions().withRateLimitMax(200)); + +// Remove the concurrency limit +dbos.updateQueue("email", new QueueOptions().withConcurrency((Integer) null)); ``` The updated configuration is validated as a whole (see [`QueueOptions`](#queueoptions)), and an update that would produce an invalid queue throws `IllegalArgumentException`. @@ -93,58 +100,59 @@ Workflows already stuck on a deleted queue can be moved to a registered queue wi Each field uses [`Field`](#fieldt) tri-state semantics: absent fields are ignored (leave the current database value unchanged), a present field with a value sets it, and a present field with `null` clears it. ```java -// Empty options (all fields absent) -QueueOptions.empty() - -// Static factories — each creates options with a single field set -QueueOptions.setConcurrency(Integer value) -QueueOptions.setWorkerConcurrency(Integer value) -QueueOptions.setRateLimit(Integer max, Duration period) -QueueOptions.setRateLimit(int max, long period, TimeUnit unit) -QueueOptions.setPartitionConcurrency(Integer value) -QueueOptions.setPartitionWorkerConcurrency(Integer value) -QueueOptions.setPartitionRateLimit(Integer max, Duration period) -QueueOptions.setPartitionRateLimit(int max, long period, TimeUnit unit) -QueueOptions.setPollingInterval(Duration value) - -// Chainable setters — start from any factory and chain additional fields -QueueOptions andConcurrency(Integer value) -QueueOptions andWorkerConcurrency(Integer value) -QueueOptions andRateLimit(Integer max, Duration period) -QueueOptions andRateLimit(int max, long period, TimeUnit unit) -QueueOptions andPartitionConcurrency(Integer value) -QueueOptions andPartitionWorkerConcurrency(Integer value) -QueueOptions andPartitionRateLimit(Integer max, Duration period) -QueueOptions andPartitionRateLimit(int max, long period, TimeUnit unit) -QueueOptions andPollingInterval(Duration value) - -// Deprecated since 1.1: set a per-partition limit instead -QueueOptions.setPartitionQueue(boolean value) -QueueOptions andPartitionQueue(boolean value) - -// Deprecated since 1.1: every queue is a priority queue -QueueOptions.setPriorityEnabled(boolean value) -QueueOptions andPriorityEnabled(boolean value) +// Options with every field absent +new QueueOptions() + +// Builders — each returns a copy with the given field set +QueueOptions withConcurrency(Integer value) +QueueOptions withWorkerConcurrency(Integer value) +QueueOptions withRateLimit(Integer max, Duration period) +QueueOptions withRateLimit(int max, long period, TimeUnit unit) +QueueOptions withRateLimitMax(Integer max) +QueueOptions withRateLimitPeriod(Duration period) +QueueOptions withPartitionConcurrency(Integer value) +QueueOptions withPartitionWorkerConcurrency(Integer value) +QueueOptions withPartitionRateLimit(Integer max, Duration period) +QueueOptions withPartitionRateLimit(int max, long period, TimeUnit unit) +QueueOptions withPartitionRateLimitMax(Integer max) +QueueOptions withPartitionRateLimitPeriod(Duration period) +QueueOptions withPollingInterval(Duration value) +``` + +**Example syntax:** + +```java +new QueueOptions() + .withConcurrency(100) + .withWorkerConcurrency(10) + .withRateLimit(1000, Duration.ofSeconds(60)) ``` **Parameters:** - **concurrency**: The maximum number of workflows from this queue that may run concurrently across all DBOS processes. Pass `null` to remove the limit. - **workerConcurrency**: The maximum number of workflows from this queue that may run concurrently within a single DBOS process. Pass `null` to remove the limit. -- **rateLimit**: A limit on the maximum number of workflows (`max`) that may be started in a given `period`. Pass `null` for both to remove the limit. +- **rateLimit**: A limit on the maximum number of workflows (`max`) that may be started in a given `period`. Pass `null` for both to remove the limit. On [`updateQueue`](#dbosupdatequeue), `withRateLimitMax` or `withRateLimitPeriod` changes one half of the stored limit and keeps the other. - **partitionConcurrency**: The maximum number of workflows from any one partition of this queue that may run concurrently across all DBOS processes. Pass `null` to remove the limit. - **partitionWorkerConcurrency**: The maximum number of workflows from any one partition of this queue that may run concurrently within a single DBOS process. Pass `null` to remove the limit. -- **partitionRateLimit**: A limit on the maximum number of workflows (`max`) that may be started from any one partition in a given `period`. Pass `null` for both to remove the limit. +- **partitionRateLimit**: A limit on the maximum number of workflows (`max`) that may be started from any one partition in a given `period`. Pass `null` for both to remove the limit. On [`updateQueue`](#dbosupdatequeue), `withPartitionRateLimitMax` or `withPartitionRateLimitPeriod` changes one half of the stored limit and keeps the other. - **priorityEnabled** *(deprecated since 1.1)*: Ignored. Every queue dequeues workflows in priority order, so priority needs no queue configuration, and the queue is always stored as a priority queue. - **pollingInterval**: How often DBOS polls the database for new workflows to dequeue. Defaults to 1 second. - **partitionQueue** *(deprecated since 1.1)*: Enable [partitioning](../tutorials/queue-tutorial.md#partitioning-queues) with the queue-wide limits (`concurrency`, `workerConcurrency`, `rateLimit`) enforced per partition rather than across the queue. Set a per-partition limit instead. If a per-partition limit is also set at registration, this flag has no effect. +Passing `null` to a builder clears that limit on `updateQueue`. +While the deprecated `Field` overloads below exist, a bare `null` is ambiguous and doesn't compile, so write it with a cast: `withConcurrency((Integer) null)`. + +The following forms are deprecated: +- *Since 1.2:* `QueueOptions.empty()` (use `new QueueOptions()`); the static `setConcurrency`, `setWorkerConcurrency`, `setRateLimit`, `setPartitionConcurrency`, `setPartitionWorkerConcurrency`, `setPartitionRateLimit` and `setPollingInterval` factories and their `and...` counterparts (use the matching `with...` builder); and every `with...` overload that takes a [`Field`](#fieldt) or `Optional` (use the plain-value overload). +- *Since 1.1:* `setPartitionQueue`, `andPartitionQueue` and `withPartitionQueue` (set a per-partition limit instead); `setPriorityEnabled`, `andPriorityEnabled` and `withPriorityEnabled` (every queue is a priority queue, so remove the call). + Setting any per-partition limit [partitions](../tutorials/queue-tutorial.md#partitioning-queues) the queue: every workflow enqueued on it must supply a partition key, and the per-partition limits are enforced for each partition key alongside the queue-wide limits. Partitioning a queue that already has enqueued workflows strands them: they have no partition key, so they are never dequeued. Drain a queue before partitioning it. Workflows stranded this way can be moved to a queue that is not partitioned with [`dbos.resumeWorkflow(workflowId, queueName)`](./methods.md#resumeworkflow). The limits are validated when a queue is registered or updated, and an invalid combination throws `IllegalArgumentException`: - Every concurrency limit, rate-limit `max`, rate-limit `period`, and `pollingInterval` must be greater than zero. -- A rate limit's `max` and `period` go together: registering a queue with only one of them set, or an update that would leave only one set, throws. Pass `null` for both to register without a limit or to clear one. +- A rate limit's `max` and `period` go together: registering a queue with only one of them set, or an update that would leave only one set, throws. Pass `null` for both to register without a limit or to clear one. `withRateLimitMax` and `withRateLimitPeriod` (and their partition counterparts) are only useful on `updateQueue` for a queue that already has a stored limit. - A concurrency limit must be less than or equal to any wider limit that is also set; limits that are not set are not compared: - `workerConcurrency` and `partitionConcurrency` must each be less than or equal to `concurrency`. - `partitionWorkerConcurrency` must be less than or equal to `partitionConcurrency`, `workerConcurrency`, and `concurrency`. @@ -185,8 +193,7 @@ public sealed interface Field permits Field.Absent, Field.Present { - `Field.Present(value)` — the field was specified with a non-null value; the database value is set to `value`. - `Field.Present(null)` — the field was specified with `null`; the database value is cleared (removed). -Use `Field.absent()` and `Field.of(value)` to construct values directly. -The `QueueOptions` convenience methods (`set*` / `and*`) call these automatically, so you rarely need to construct `Field` values by hand. +The `QueueOptions` builders create these values for you, so you rarely need to construct `Field` values by hand. ## Queue @@ -279,7 +286,7 @@ Queue queue = new Queue("example-queue").withWorkerConcurrency(5); dbos.registerQueue(queue); // Replacement, after dbos.launch() -dbos.registerQueue("example-queue", QueueOptions.setWorkerConcurrency(5)); +dbos.registerQueue("example-queue", new QueueOptions().withWorkerConcurrency(5)); ``` ### dbos.registerQueue {#dbosregisterqueue-legacy} diff --git a/docs/java/reference/workflows-steps.md b/docs/java/reference/workflows-steps.md index 90655d8cd..c00052494 100644 --- a/docs/java/reference/workflows-steps.md +++ b/docs/java/reference/workflows-steps.md @@ -254,24 +254,29 @@ new StartWorkflowOptions(Queue queue) - **`withQueue(QueueName queue)`** / **`withQueue(String queueName)`** - Instead of starting the workflow directly, enqueue it on this queue. `withQueue(Queue queue)` is *(deprecated since 1.1)*; pass the queue's name instead. -- **`withTimeout(Timeout timeout)`** - Set a timeout using a [`Timeout`](./methods.md#timeout) object. Use this overload to pass `Timeout.none()` (opt out of any inherited timeout) or `Timeout.inherit()` (explicitly inherit from the calling context). +- **`withTimeout(Timeout timeout)`** - Set a timeout using a [`Timeout`](./methods.md#timeout) object. Use this overload to pass `Timeout.none()` (run without a timeout and don't inherit the parent's deadline) or `Timeout.inherit()` (bound the workflow by the calling workflow's deadline). - **`withTimeout(Duration timeout)`** / **`withTimeout(long value, TimeUnit unit)`** - Set an explicit timeout duration for this workflow. When the timeout expires, the workflow **and all its children** are cancelled. Cancelling a workflow sets its status to `CANCELLED` and preempts its execution at the beginning of its next step. Timeouts are **start-to-completion**: if a workflow is enqueued, the timeout does not begin until the workflow is dequeued and starts execution. Also, timeouts are **durable**: they are stored in the database and persist across restarts, so workflows can have very long timeouts. - Timeout deadlines are propagated to child workflows by default, so when a workflow's deadline expires all of its child workflows (and their children, and so on) are also cancelled. If you want to detach a child workflow from its parent's timeout, you can start it with its own explicit timeout (or `Timeout.none()`) to override the propagated timeout. + A child workflow inherits its parent's **deadline** (the moment the parent's timeout expires), not its timeout, so when a workflow's deadline expires all of its child workflows (and their children, and so on) are also cancelled, including children still waiting in a queue. A child's bound is the first of these that is set: + 1. the timeout, `Timeout.none()`, `Timeout.inherit()`, or deadline given in this `StartWorkflowOptions`; + 2. the timeout or deadline set by an enclosing [`WorkflowOptions`](#workflowoptions) block; + 3. the parent's deadline. + + To detach a child workflow from its parent's deadline, start it with its own explicit timeout, or with `Timeout.none()`. - **`withNoTimeout()`** - Explicitly remove any inherited timeout or deadline from this workflow. -- **`withDeadline(Instant deadline)`** - Set a deadline for this workflow. If the workflow is executing at the time of the deadline, the workflow **and all its children** are cancelled. Cancelling a workflow sets its status to `CANCELLED` and preempts its execution at the beginning of its next step. +- **`withDeadline(Instant deadline)`** *(deprecated since 1.2)* - Use `withTimeout` instead; no other DBOS SDK lets a caller set a deadline. Set a deadline for this workflow. If the workflow is executing at the time of the deadline, the workflow **and all its children** are cancelled. Cancelling a workflow sets its status to `CANCELLED` and preempts its execution at the beginning of its next step. Deadlines are **durable**: they are stored in the database and persist across restarts. - Deadlines are propagated to child workflows by default, so when a workflow's deadline expires all of its child workflows (and their children, and so on) are also cancelled. If you want to detach a child workflow from its parent's deadline, you can start it with a different explicit deadline. + Deadlines are propagated to child workflows by default, so when a workflow's deadline expires all of its child workflows (and their children, and so on) are also cancelled. :::info -An explicit timeout and deadline cannot both be set. +An explicit timeout and deadline cannot both be set: building a `StartWorkflowOptions` with both throws `IllegalArgumentException`. ::: - **`withPriority(int priority)`** - May only be used when enqueuing. The priority of the enqueued workflow in the specified queue. Workflows with the same priority are dequeued in FIFO (first in, first out) order. Priority values can range from `0` to `2,147,483,647`, where a low number indicates a higher priority. A negative priority throws `IllegalArgumentException`. Workflows without assigned priorities have priority `0`, the highest priority. Priority works on every queue; no queue configuration is needed. @@ -430,17 +435,19 @@ Shortcut for `new WorkflowOptions().withWorkflowId(workflowId)`. **Fields:** - **workflowId**: The ID to be assigned to a workflow called within the `try` block - **timeout**: The timeout to be assigned to all workflows called within the `try` block -- **deadline**: The deadline to be assigned to all workflows called within the `try` block +- **deadline** *(deprecated since 1.2)*: The deadline to be assigned to all workflows called within the `try` block **Methods:** - **`withWorkflowId(String workflowId)`** - Set the [workflow ID](../tutorials/workflow-tutorial.md#workflow-ids-and-idempotency) of the next workflow run. -- **`withTimeout(Timeout timeout)`** / **`withTimeout(Duration timeout)`** / **`withTimeout(long value, TimeUnit unit)`** - Set a timeout for all enclosed workflow invocations. When the timeout expires, the workflow **and all its children** are cancelled. Timeouts are **start-to-completion**: the timeout does not begin until the workflow starts execution. Timeouts are also **durable**: they persist across restarts. Timeout deadlines are propagated to child workflows by default. +- **`withTimeout(Timeout timeout)`** / **`withTimeout(Duration timeout)`** / **`withTimeout(long value, TimeUnit unit)`** - Set a timeout for all enclosed workflow invocations. When the timeout expires, the workflow **and all its children** are cancelled. Timeouts are **start-to-completion**: the timeout does not begin until the workflow starts execution. Timeouts are also **durable**: they persist across restarts. Child workflows inherit the workflow's deadline, not its timeout. + + A timeout or deadline set here replaces both the timeout and the deadline set by an enclosing `WorkflowOptions` block, which return when this block closes. A timeout, `Timeout.none()`, `Timeout.inherit()`, or deadline given for a call in [`StartWorkflowOptions`](#startworkflowoptions) or `EnqueueOptions` replaces both of those set here. - **`withNoTimeout()`** - Explicitly opt out of any inherited timeout. The workflow called within the `try` block will run without a timeout regardless of any timeout in the surrounding context. -- **`withDeadline(Instant deadline)`** - Set an absolute deadline for all enclosed workflow invocations. At the deadline time, the workflow **and all its children** are cancelled. Deadlines are propagated to child workflows by default. +- **`withDeadline(Instant deadline)`** *(deprecated since 1.2)* - Use `withTimeout` instead. Set an absolute deadline for all enclosed workflow invocations. At the deadline time, the workflow **and all its children** are cancelled. Deadlines are propagated to child workflows by default. :::info An explicit timeout and deadline cannot both be set. diff --git a/docs/java/tutorials/kotlin.md b/docs/java/tutorials/kotlin.md index 5ea074109..f884757ee 100644 --- a/docs/java/tutorials/kotlin.md +++ b/docs/java/tutorials/kotlin.md @@ -15,7 +15,7 @@ The Kotlin extensions are included in the main `transact` artifact alongside the ```kotlin dependencies { - implementation("dev.dbos:transact:1.1.0") + implementation("dev.dbos:transact:1.2.0") } ``` @@ -25,7 +25,7 @@ dependencies { dev.dbos transact - 1.1.0 + 1.2.0 ``` diff --git a/docs/java/tutorials/queue-tutorial.md b/docs/java/tutorials/queue-tutorial.md index 58c9cc155..6cc8703f1 100644 --- a/docs/java/tutorials/queue-tutorial.md +++ b/docs/java/tutorials/queue-tutorial.md @@ -13,7 +13,7 @@ Queue configuration is persisted to the system database, so queues are visible t ```java dbos.launch(); -dbos.registerQueue("example-queue", QueueOptions.empty()); +dbos.registerQueue("example-queue", new QueueOptions()); ``` You can then enqueue any workflow using [`withQueue`](../reference/workflows-steps.md#startworkflow) when calling `startWorkflow`. @@ -120,7 +120,7 @@ public class App { dbos.launch(); // Register the queue - dbos.registerQueue("example-queue", QueueOptions.empty()); + dbos.registerQueue("example-queue", new QueueOptions()); impl.setQueueName("example-queue"); // Run the queue workflow @@ -279,7 +279,7 @@ This is particularly useful for resource-intensive workflows to avoid exhausting For example, this queue has a worker concurrency of 5, so each process will run at most 5 workflows from this queue simultaneously: ```java -dbos.registerQueue("example-queue", QueueOptions.setWorkerConcurrency(5)); +dbos.registerQueue("example-queue", new QueueOptions().withWorkerConcurrency(5)); ``` #### Global Concurrency @@ -293,7 +293,7 @@ Take care when using a global concurrency limit as any `PENDING` workflow on the ::: ```java -dbos.registerQueue("example-queue", QueueOptions.setConcurrency(10)); +dbos.registerQueue("example-queue", new QueueOptions().withConcurrency(10)); ``` ### Rate Limiting @@ -303,7 +303,7 @@ Rate limits are global across all DBOS processes using this queue. For example, this queue has a limit of 100 workflows with a period of 60 seconds, so it may not start more than 100 workflows in 60 seconds: ```java -dbos.registerQueue("example-queue", QueueOptions.setRateLimit(100, 60, TimeUnit.SECONDS)); +dbos.registerQueue("example-queue", new QueueOptions().withRateLimit(100, 60, TimeUnit.SECONDS)); ``` Rate limits are especially useful when working with a rate-limited API. @@ -315,10 +315,10 @@ Use `dbos.updateQueue` to modify a queue's configuration. Workers pick up the ne ```java // Change the queue's concurrency -dbos.updateQueue("example-queue", QueueOptions.setConcurrency(20)); +dbos.updateQueue("example-queue", new QueueOptions().withConcurrency(20)); // Change its rate limit -dbos.updateQueue("example-queue", QueueOptions.setRateLimit(25, 30, TimeUnit.SECONDS)); +dbos.updateQueue("example-queue", new QueueOptions().withRateLimit(25, 30, TimeUnit.SECONDS)); ``` :::warning @@ -345,13 +345,13 @@ You can do all of this from a [`DBOSClient`](../reference/client.md#queue-manage var client = new DBOSClient(dbUrl, dbUser, dbPassword); // Register or update a queue -client.registerQueue("example-queue", QueueOptions.setConcurrency(10)); +client.registerQueue("example-queue", new QueueOptions().withConcurrency(10)); // Register only if it doesn't already exist -client.registerQueue("example-queue", QueueOptions.setConcurrency(10), QueueConflictResolution.NEVER_UPDATE); +client.registerQueue("example-queue", new QueueOptions().withConcurrency(10), QueueConflictResolution.NEVER_UPDATE); // Update only the concurrency of an existing queue -client.updateQueue("example-queue", QueueOptions.setConcurrency(20)); +client.updateQueue("example-queue", new QueueOptions().withConcurrency(20)); // Find a queue by name Optional queue = client.findQueue("example-queue"); @@ -372,6 +372,9 @@ Cancelling a workflow sets its status to `CANCELLED` and preempts its execution Timeouts are **start-to-completion**: a workflow's timeout does not begin until the workflow is dequeued and starts execution. Also, timeouts are **durable**: they are stored in the database and persist across restarts, so workflows can have very long timeouts. +A workflow enqueued from inside another workflow without a timeout of its own inherits its parent's **deadline** instead. +If it is still waiting in the queue when that deadline passes, it is cancelled when it is dequeued. + Example syntax: ```java @@ -382,6 +385,11 @@ var handle = dbos.startWorkflow(() -> proxy.workflow(), options); ### Setting Deadlines +:::warning Deprecated +`withDeadline` is deprecated since 1.2 and will be removed in a future release, because the other DBOS SDKs bound workflows with timeouts only. +Use [`withTimeout`](#setting-timeouts) instead. +::: + You can set a deadline for an enqueued workflow via the `withDeadline` function on `StartWorkflowOptions`. A deadline is an **absolute point in time** by which the workflow must complete; if the deadline passes before the workflow finishes, the workflow **and all its children** are cancelled. Cancelling a workflow sets its status to `CANCELLED` and preempts its execution at the beginning of its next step. @@ -435,7 +443,7 @@ You can do this with a queue whose `partitionConcurrency` is 1, where the partit **Example Syntax** ```java -dbos.registerQueue("example-queue", QueueOptions.setPartitionConcurrency(1)); +dbos.registerQueue("example-queue", new QueueOptions().withPartitionConcurrency(1)); void onUserTaskSubmission(String userID, Task task) { // Partition the task queue by user ID. As the queue has a @@ -463,7 +471,7 @@ For example, this "fair queue" runs at most one task per user, but no more than ```java dbos.registerQueue("fair-queue", - QueueOptions.setPartitionConcurrency(1).andWorkerConcurrency(10)); + new QueueOptions().withPartitionConcurrency(1).withWorkerConcurrency(10)); ``` Each queue-wide limit has a per-partition counterpart, so you can mix and match them freely: @@ -473,12 +481,13 @@ Each queue-wide limit has a per-partition counterpart, so you can mix and match // at most 10 tasks running per process and 2 per tenant per process, // and at most 1000 tasks started per minute globally and 50 per tenant. dbos.registerQueue("tenant-queue", - QueueOptions.setConcurrency(100) - .andWorkerConcurrency(10) - .andRateLimit(1000, Duration.ofSeconds(60)) - .andPartitionConcurrency(25) - .andPartitionWorkerConcurrency(2) - .andPartitionRateLimit(50, Duration.ofSeconds(60))); + new QueueOptions() + .withConcurrency(100) + .withWorkerConcurrency(10) + .withRateLimit(1000, Duration.ofSeconds(60)) + .withPartitionConcurrency(25) + .withPartitionWorkerConcurrency(2) + .withPartitionRateLimit(50, Duration.ofSeconds(60))); ``` When both are set, each per-partition concurrency limit must be less than or equal to its queue-wide counterpart, and `partitionWorkerConcurrency` must be less than or equal to `partitionConcurrency`; limits that are not set are not compared. @@ -577,8 +586,8 @@ DBOS dbos = new DBOS(config); // register workflows... dbos.launch(); -dbos.registerQueue("cpuQueue", QueueOptions.empty()); -dbos.registerQueue("gpuQueue", QueueOptions.empty()); +dbos.registerQueue("cpuQueue", new QueueOptions()); +dbos.registerQueue("gpuQueue", new QueueOptions()); ``` Note that `withListenQueues` only controls what workflows are dequeued, not what workflows can be enqueued, so you can freely enqueue tasks onto the GPU queue from a CPU worker for execution on a GPU worker, and vice versa. diff --git a/docs/java/tutorials/spring-boot-integration.md b/docs/java/tutorials/spring-boot-integration.md index 2be6ac169..7394de394 100644 --- a/docs/java/tutorials/spring-boot-integration.md +++ b/docs/java/tutorials/spring-boot-integration.md @@ -17,7 +17,7 @@ that illustrates the features described in this documentation. ```kotlin dependencies { - implementation("dev.dbos:transact-spring-boot-starter:1.1.0") + implementation("dev.dbos:transact-spring-boot-starter:1.2.0") } ``` @@ -27,7 +27,7 @@ dependencies { dev.dbos transact-spring-boot-starter - 1.1.0 + 1.2.0 ``` @@ -148,7 +148,7 @@ public class QueueSetup implements ApplicationListener { @Override public void onApplicationEvent(ContextRefreshedEvent event) { - dbos.registerQueue("orders", QueueOptions.setWorkerConcurrency(5)); + dbos.registerQueue("orders", new QueueOptions().withWorkerConcurrency(5)); } } ``` diff --git a/docs/java/tutorials/workflow-tutorial.md b/docs/java/tutorials/workflow-tutorial.md index 41db73609..e232ac735 100644 --- a/docs/java/tutorials/workflow-tutorial.md +++ b/docs/java/tutorials/workflow-tutorial.md @@ -174,7 +174,8 @@ You can set a timeout for a workflow using [`withTimeout`](../reference/workflow When the timeout expires, the workflow and all its children (by default) are cancelled. Cancelling a workflow sets its status to CANCELLED and preempts its execution at the beginning of its next step. -You can detach a child workflow from its parent's timeout by starting it with a custom timeout using `withTimeout`. +A child workflow started without a timeout of its own inherits its parent's **deadline**, the moment the parent's timeout expires, so it can't outlive its parent, even if it is enqueued and waits in the queue. +You can detach a child workflow from its parent's deadline by starting it with its own timeout using `withTimeout`, or with no timeout using `Timeout.none()`. Timeouts are **start-to-completion**: if a workflow is [enqueued](./queue-tutorial.md), the timeout does not begin until the workflow is dequeued and starts execution. Also, timeouts are durable: they are stored in the database and persist across restarts, so workflows can have very long timeouts. diff --git a/docs/java/upgrading.md b/docs/java/upgrading.md index c76dbdfdf..71fdedc00 100644 --- a/docs/java/upgrading.md +++ b/docs/java/upgrading.md @@ -11,10 +11,107 @@ If you run with `withMigrate(false)`, run [`dbosctl sysdb migrate`](../conductor [`DBOSClient`](./reference/client.md) never migrates, so upgrade clients only after an upgraded application has launched or you have run `dbosctl sysdb migrate`. An application or client that needs a newer schema than the system database has throws `IllegalStateException` at launch or construction. +## Upgrading to v1.2 + +Most code written against DBOS Transact Java 1.1 compiles and runs unchanged on 1.2; the exceptions are listed below. +1.2 deprecates the old `QueueOptions` builders and absolute workflow deadlines, which will be removed in a future release. +This section covers what might require a change to your code, configuration, or operations, and what to replace deprecated APIs with. +For new features, see the [release notes](https://github.com/dbos-inc/dbos-transact-java/releases). + +### Upgrade to 1.1 First + +Every server must run 1.1 before any server runs 1.2. Don't run 1.0 and 1.2 servers against the same system database, and don't roll a 1.2 deployment back to 1.0. +1.1 and 1.2 servers can share a system database, and you can roll 1.2 back to 1.1. +See [1.1 Is Required Before 1.2](#11-is-required-before-12) for why. + +1.2 migrates the system database to version 114. Migration 113 changes the `enqueue_workflow` SQL function to write inputs to `workflow_input`, and migration 114 drops a duplicate index on `notifications`. +The minimum schema version is still 111, so 1.1 servers and clients keep working against a migrated database. + +### Changes That May Require Action + +#### Workflow Inputs and Outputs Have Moved + +1.2 writes workflow inputs to [`dbos.workflow_input`](../explanations/system-tables.md#dbosworkflow_input), and outputs and errors to [`dbos.workflow_output`](../explanations/system-tables.md#dbosworkflow_output). +For workflows started by 1.2, the `inputs`, `output`, and `error` columns of `dbos.workflow_status` are `NULL`. +DBOS still reads those columns for workflows written by earlier releases, but if you query them directly, for example from a dashboard or a script, read the new tables instead. + +#### Debouncing + +The debouncer no longer starts a separate debouncer workflow. +The first `debounce` call on a key enqueues your workflow itself in the `DELAYED` state on its queue, with `workflowName-debounceKey` as its deduplication ID, and later calls push back its start and replace its arguments. +Without `withQueue`, it goes on the DBOS internal queue instead of being started directly. +While it waits, the workflow appears in `listWorkflows` as `DELAYED`, with `isDebounced()` set to `true`. + +- `withDeduplicationId` on `Debouncer` and `DebouncerClient` is now ignored. +- A negative `withPriority` or a zero or negative `withTimeout` now throws `IllegalArgumentException` when set, not at `debounce`. +- Outside a workflow, a timeout set with `WorkflowOptions` around `debounce` now limits your workflow, not an internal debouncer workflow. The new [`Debouncer.withTimeout`](./reference/methods.md#debouncer) sets it on the debouncer instead. +- A 1.1 debouncer workflow that still holds a key is taken over by the next 1.2 `debounce` call on that key, after about 5 seconds: 1.2 cancels it and creates the workflow it promised. If your application version changes with the upgrade, 1.2 servers don't run 1.1 debouncer workflows, so cancel any leftover `debouncerWorkflow` workflows whose keys will never be debounced again. + +#### Child Workflow Timeouts + +A child workflow started without a timeout of its own now inherits its parent's **deadline**, not its timeout. +In 1.1, a queued child copied its parent's timeout and started a fresh copy of it when dequeued, so it could outlive its parent. +Now a child's bound is the first of these that is set: + +1. the timeout, `Timeout.none()`, `Timeout.inherit()`, or deadline given in the call's `StartWorkflowOptions` or `EnqueueOptions`; +2. the timeout or deadline set by an enclosing `WorkflowOptions` block; +3. the parent's deadline. + +A queued child that is dequeued after its inherited deadline is cancelled. Other effects: + +- Options given for a call replace the whole bound set by `WorkflowOptions`. In 1.1, the timeout and deadline merged field by field, so a deadline from `WorkflowOptions` could override a timeout given for the call. +- An inner `WorkflowOptions` block that sets a timeout, deadline, or `Timeout.none()` replaces the outer block's timeout and deadline together. +- A child that inherited its bound has no `workflow_timeout_ms`, so `WorkflowStatus.timeout()` is `null` for it, and `DBOSContext.getTimeout()` returns `null` inside it. +- Resuming a child that inherited only a deadline leaves it without a bound, because resume clears the deadline and keeps the timeout. +- Building a `StartWorkflowOptions` with both an explicit timeout and a deadline now throws `IllegalArgumentException` immediately, as `EnqueueOptions` already did. + +#### Exporting and Importing Workflows + +A workflow exported from 1.2 carries its stored payloads unchanged, so its inputs, output, error, and step outputs keep their Java types when imported. +Import a 1.2 export only into 1.1.1 or later. +Imported into 1.1.0 or 1.0, it is accepted without any error, but its inputs, output, error, and step outputs import as `NULL`. +Exports from 1.1.0 and earlier still import correctly into 1.2. + +#### New Record Components + +These public records gained components. Code that calls their canonical constructors directly must pass the new values, and code compiled against 1.1 that calls them fails with `NoSuchMethodError`: + +- [`WorkflowStatus`](./reference/methods.md#workflowstatus) gains `isDebounced` and `debounceDeadline`, after `applicationName`. Its `equals` and `hashCode` now also compare `applicationName`. +- `ListWorkflowsInput` gains `isFork`, after `wasForkedFrom`. Code that starts from `new ListWorkflowsInput()` and uses the `with...` methods is unaffected. +- `ExportedWorkflow` gains `payloads`. Pass `null` when there are no stored payloads. + +#### Other Behavior Changes + +- Workflow and queue timestamps in the system database (`created_at`, `updated_at`, `completed_at`, `started_at_epoch_ms`, and rate-limit windows) now come from the database's clock instead of each server's clock, as in the other DBOS SDKs. Delays, deadlines, and durable sleeps still use the server's clock. +- A workflow enqueued without a class name, such as one meant for a Python application, now fails on a Java server with `DBOSWorkflowFunctionNotFoundException` and stays `PENDING`, instead of throwing `NullPointerException`. To target a Java workflow, always pass its class name to `EnqueueOptions`. + +### Deprecations + +The following APIs are deprecated in 1.2 and will be removed in a future release. + +| Deprecated | Replacement | +|---|---| +| `QueueOptions.empty()` | `new QueueOptions()` | +| The static `QueueOptions.setConcurrency`, `setWorkerConcurrency`, `setRateLimit`, `setPartitionConcurrency`, `setPartitionWorkerConcurrency`, `setPartitionRateLimit`, and `setPollingInterval` factories, and their `and...` counterparts | `new QueueOptions()` and the matching [`with...` builder](./reference/queues.md#queueoptions), such as `new QueueOptions().withConcurrency(10)` | +| The `QueueOptions.with...` overloads that take a `Field` or an `Optional` | The plain-value overloads. To clear a limit on `updateQueue`, pass a cast `null`, such as `withConcurrency((Integer) null)`. | +| `withDeadline` and `deadline()` on `StartWorkflowOptions`, `EnqueueOptions`, and `WorkflowOptions` | `withTimeout`. For a workflow that starts right away, a timeout of `Duration.between(Instant.now(), deadline)` is the same bound. | + +To move to the new `QueueOptions` builders: + +```java +// Before +dbos.registerQueue("email", QueueOptions.setConcurrency(10).andRateLimit(100, Duration.ofSeconds(60))); + +// After +dbos.registerQueue("email", new QueueOptions().withConcurrency(10).withRateLimit(100, Duration.ofSeconds(60))); +``` + +--- + ## Upgrading to v1.1 Most code written against DBOS Transact Java 1.0 compiles and runs unchanged on 1.1; the exceptions are listed below. -1.1 deprecates in-memory queues and a few other APIs, which will be removed in 2.0, and validates some inputs that 1.0 accepted. +1.1 deprecates in-memory queues and a few other APIs, which will be removed in a future release, and validates some inputs that 1.0 accepted. This section covers what might require a change to your code or configuration, and what to replace deprecated APIs with. For new features, see the [release notes](https://github.com/dbos-inc/dbos-transact-java/releases). @@ -161,7 +258,7 @@ Workflows already stranded this way can be moved to a queue that is not partitio ### Deprecations -The following APIs are deprecated in 1.1 and will be removed in 2.0. +The following APIs are deprecated in 1.1 and will be removed in a future release. | Deprecated | Replacement | |---|---| @@ -171,11 +268,11 @@ The following APIs are deprecated in 1.1 and will be removed in 2.0. | `dbos.getQueue(String)` | `dbos.findQueue(String)` | | `Queue`-typed overloads: `new StartWorkflowOptions(Queue)`, `StartWorkflowOptions.withQueue(Queue)`, `ForkOptions.withQueue(Queue)`, `Debouncer.withQueue(Queue)`, `DebouncerClient.withQueue(Queue)`, `DBOSConfig.withListenQueue(Queue)`, `DBOSConfig.withListenQueues(Queue...)` | The `QueueName` or `String` overloads | | `QueueOptions.setPriorityEnabled`, `withPriorityEnabled`, `andPriorityEnabled`, the `QueueOptions.priorityEnabled()` accessor, and `Queue.priorityEnabled()` | None. Every queue dequeues in priority order; set a priority on the workflow instead. | -| `QueueOptions.setPartitionQueue`, `withPartitionQueue`, `andPartitionQueue`, and the `partitionQueue()` accessor | `setPartitionConcurrency`, `setPartitionWorkerConcurrency`, `setPartitionRateLimit` (and their `and`/`with` forms) | -| The seven-argument `QueueOptions` constructor (without per-partition limits) | The static `QueueOptions.set...` factories | +| `QueueOptions.setPartitionQueue`, `withPartitionQueue`, `andPartitionQueue`, and the `partitionQueue()` accessor | `setPartitionConcurrency`, `setPartitionWorkerConcurrency`, `setPartitionRateLimit` (and their `and`/`with` forms); from 1.2, `new QueueOptions().withPartitionConcurrency(...)` and the other `with...` builders | +| The seven-argument `QueueOptions` constructor (without per-partition limits) | The static `QueueOptions.set...` factories; from 1.2, `new QueueOptions()` and the `with...` builders | | `DBOSClient.EnqueueOptions`, and the `DBOSClient.enqueueWorkflow` / `enqueuePortableWorkflow` overloads that take it | The top-level [`dev.dbos.transact.EnqueueOptions`](./reference/client.md#enqueueoptions), whose constructors take the workflow name, optional class and instance names, and a `QueueName`. For portable enqueue, set `withSerialization(SerializationStrategy.PORTABLE)` and call `enqueueWorkflow(options, positionalArgs, namedArgs)`. | -| `Debouncer.withDeduplicationId`, `DebouncerClient.withDeduplicationId` | None. From the next release the debouncer sets the deduplication ID itself and ignores this setting. | -| `ExternalState`, `DBOSIntegration.getExternalState`, `DBOSIntegration.upsertExternalState` (the `event_dispatch_kv` API) | Store integration state in your own table. A shared system database migration will drop the `event_dispatch_kv` table sometime after Java 2.0. | +| `Debouncer.withDeduplicationId`, `DebouncerClient.withDeduplicationId` | None. From 1.2 the debouncer sets the deduplication ID itself and ignores this setting. | +| `ExternalState`, `DBOSIntegration.getExternalState`, `DBOSIntegration.upsertExternalState` (the `event_dispatch_kv` API) | Store integration state in your own table. A shared system database migration will drop the `event_dispatch_kv` table sometime after this API is removed. | | `DBOSSystemDatabaseException.databaseException()` | `getCause()` | --- From b2c1940e32234151db1781447ec7592ebc3b2b6a Mon Sep 17 00:00:00 2001 From: Harry Pierson Date: Thu, 1 Oct 2026 11:17:23 -0700 Subject: [PATCH 2/2] Fix Java 1.2 docs review findings - Debouncer: the key is released when the workflow leaves DELAYED, not when it starts; later calls reset the start to one period after the call; a debouncer can't use a partitioned queue. - Deadline migration: Duration.between throws for a deadline that has passed; drop the DBOSContext.getTimeout() mention. - dbos.enqueueWorkflow: an enclosing WorkflowOptions bound applies before the caller's deadline. --- docs/java/reference/client.md | 2 +- docs/java/reference/methods.md | 6 +++--- docs/java/upgrading.md | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/java/reference/client.md b/docs/java/reference/client.md index 14be38271..5e63f2121 100644 --- a/docs/java/reference/client.md +++ b/docs/java/reference/client.md @@ -113,7 +113,7 @@ The workflow name and queue must not be null or empty. - **`withAppVersion(String appVersion)`**: The version of your application that should process this workflow. If left undefined, the workflow is enqueued without a version and is only dequeued by an executor running the owning application's latest registered version, which sets the version when it first dequeues it. - **`withTimeout(Duration timeout)`**, **`withTimeout(long value, TimeUnit unit)`**: Set an explicit timeout for the enqueued workflow. When the timeout expires, the workflow and all its children are cancelled. The timeout does not begin until the workflow is dequeued and starts execution. -- **`withTimeout(Timeout timeout)`**, **`withNoTimeout()`**: Set the timeout as a [`Timeout`](./methods.md#timeout): explicit, none, or inherit. Inside a workflow, [`dbos.enqueueWorkflow`](./methods.md#enqueueworkflow) resolves it as `startWorkflow` does, so an unset timeout inherits the enqueuing workflow's deadline and `withNoTimeout()` declines it. From a client there is nothing to inherit, so an unset or inherited timeout means no timeout. +- **`withTimeout(Timeout timeout)`**, **`withNoTimeout()`**: Set the timeout as a [`Timeout`](./methods.md#timeout): explicit, none, or inherit. [`dbos.enqueueWorkflow`](./methods.md#enqueueworkflow) resolves it as `startWorkflow` does: an unset timeout takes the bound of an enclosing [`WorkflowOptions`](./workflows-steps.md#workflowoptions) block if there is one, and otherwise, inside a workflow, inherits the enqueuing workflow's deadline. `withNoTimeout()` declines both. From a client there is nothing to inherit, so an unset or inherited timeout means no timeout. - **`withDeadline(Instant deadline)`** *(deprecated since 1.2)*: Use `withTimeout` instead; no other DBOS SDK lets a caller set a deadline. Set a deadline for the enqueued workflow. If the workflow is executing when the deadline arrives, the workflow and all its children are cancelled. :::info diff --git a/docs/java/reference/methods.md b/docs/java/reference/methods.md index 9550b9cc4..31cee953f 100644 --- a/docs/java/reference/methods.md +++ b/docs/java/reference/methods.md @@ -941,7 +941,7 @@ Obtain a `Debouncer` via `dbos.debouncer()`: `Debouncer` is an immutable builder. Configure it with the following methods before calling `debounce`: - **`withDebounceTimeout(Duration debounceTimeout)`**: Set an absolute cap on how long the debouncer may keep absorbing calls for a single key. After this duration elapses from the first call, the user workflow starts regardless of further incoming calls. -- **`withQueue(QueueName queue)`** / **`withQueue(String queueName)`**: The queue the user workflow waits and runs on. Without one, it uses the DBOS internal queue. `withQueue(Queue queue)` is *(deprecated since 1.1)*. +- **`withQueue(QueueName queue)`** / **`withQueue(String queueName)`**: The queue the user workflow waits and runs on. Without one, it uses the DBOS internal queue. The queue can't be [partitioned](../tutorials/queue-tutorial.md#partitioning-queues): a debounced workflow has no partition key, and partitioned queues don't support deduplication IDs. `withQueue(Queue queue)` is *(deprecated since 1.1)*. - **`withAppVersion(String appVersion)`**: Target a specific application version for the user workflow. - **`withPriority(Integer priority)`**: Set the priority for the user workflow. A negative priority throws `IllegalArgumentException` when set. A priority requires a queue: if a priority is set without `withQueue`, `debounce` throws `IllegalArgumentException`. - **`withTimeout(Duration timeout)`**: Set a timeout for every user workflow this debouncer starts, timed from when that workflow is dequeued. It takes precedence over a timeout set with [`WorkflowOptions`](./workflows-steps.md#workflowoptions) around the `debounce` call, which applies when this isn't set. A zero or negative timeout throws `IllegalArgumentException` when set. @@ -962,10 +962,10 @@ The user workflow never inherits the calling workflow's timeout or deadline, and Submit a workflow for execution but delay it by `debouncePeriod`. Returns a handle to the workflow. The workflow may be debounced again, which further delays its execution (up to `debounceTimeout`). When the workflow eventually executes, it uses the **last** set of inputs passed into `debounce`. -After the workflow begins execution, the next call to `debounce` starts the debouncing process again for a new workflow execution. +Once the delay expires and the workflow becomes `ENQUEUED`, the next call to `debounce` starts the debouncing process again for a new workflow execution, even if the first workflow is still waiting on a busy queue. The first call on a key enqueues the user workflow itself in the `DELAYED` state on its queue, with `workflowName-debounceKey` as its [deduplication ID](../tutorials/queue-tutorial.md#deduplication). -Each later call on the key pushes back its start and replaces its arguments. +Each later call on the key resets its start to `debouncePeriod` after that call, never past the debounce timeout, and replaces its arguments. When the delay expires, the workflow becomes `ENQUEUED`, releases the key, and runs like any other queued workflow. **Parameters:** diff --git a/docs/java/upgrading.md b/docs/java/upgrading.md index 71fdedc00..8b9e1c3ad 100644 --- a/docs/java/upgrading.md +++ b/docs/java/upgrading.md @@ -38,7 +38,7 @@ DBOS still reads those columns for workflows written by earlier releases, but if #### Debouncing The debouncer no longer starts a separate debouncer workflow. -The first `debounce` call on a key enqueues your workflow itself in the `DELAYED` state on its queue, with `workflowName-debounceKey` as its deduplication ID, and later calls push back its start and replace its arguments. +The first `debounce` call on a key enqueues your workflow itself in the `DELAYED` state on its queue, with `workflowName-debounceKey` as its deduplication ID, and each later call resets its start to one debounce period after that call and replaces its arguments. Without `withQueue`, it goes on the DBOS internal queue instead of being started directly. While it waits, the workflow appears in `listWorkflows` as `DELAYED`, with `isDebounced()` set to `true`. @@ -61,7 +61,7 @@ A queued child that is dequeued after its inherited deadline is cancelled. Other - Options given for a call replace the whole bound set by `WorkflowOptions`. In 1.1, the timeout and deadline merged field by field, so a deadline from `WorkflowOptions` could override a timeout given for the call. - An inner `WorkflowOptions` block that sets a timeout, deadline, or `Timeout.none()` replaces the outer block's timeout and deadline together. -- A child that inherited its bound has no `workflow_timeout_ms`, so `WorkflowStatus.timeout()` is `null` for it, and `DBOSContext.getTimeout()` returns `null` inside it. +- A child that inherited its bound has no `workflow_timeout_ms`, so `WorkflowStatus.timeout()` is `null` for it. - Resuming a child that inherited only a deadline leaves it without a bound, because resume clears the deadline and keeps the timeout. - Building a `StartWorkflowOptions` with both an explicit timeout and a deadline now throws `IllegalArgumentException` immediately, as `EnqueueOptions` already did. @@ -94,7 +94,7 @@ The following APIs are deprecated in 1.2 and will be removed in a future release | `QueueOptions.empty()` | `new QueueOptions()` | | The static `QueueOptions.setConcurrency`, `setWorkerConcurrency`, `setRateLimit`, `setPartitionConcurrency`, `setPartitionWorkerConcurrency`, `setPartitionRateLimit`, and `setPollingInterval` factories, and their `and...` counterparts | `new QueueOptions()` and the matching [`with...` builder](./reference/queues.md#queueoptions), such as `new QueueOptions().withConcurrency(10)` | | The `QueueOptions.with...` overloads that take a `Field` or an `Optional` | The plain-value overloads. To clear a limit on `updateQueue`, pass a cast `null`, such as `withConcurrency((Integer) null)`. | -| `withDeadline` and `deadline()` on `StartWorkflowOptions`, `EnqueueOptions`, and `WorkflowOptions` | `withTimeout`. For a workflow that starts right away, a timeout of `Duration.between(Instant.now(), deadline)` is the same bound. | +| `withDeadline` and `deadline()` on `StartWorkflowOptions`, `EnqueueOptions`, and `WorkflowOptions` | `withTimeout`. For a workflow that starts right away, a timeout of `Duration.between(Instant.now(), deadline)` is the same bound while the deadline is in the future. A deadline that is now or past gives a zero or negative timeout, which throws `IllegalArgumentException`. | To move to the new `QueueOptions` builders: