diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java b/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java index b08ffcc53c..b5fc7af41c 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java +++ b/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskClient.java @@ -13,9 +13,13 @@ package io.dapr.durabletask; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent; +import io.dapr.durabletask.implementation.protobuf.OrchestratorService; + import javax.annotation.Nullable; import java.time.Duration; +import java.util.List; import java.util.concurrent.TimeoutException; /** @@ -318,4 +322,35 @@ public void resumeInstance(String instanceId) { * @param reason the reason for resuming the orchestration instance */ public abstract void resumeInstance(String instanceId, @Nullable String reason); + + /** + * Lists workflow instance IDs with optional pagination. + * + * @param continuationToken the continuation token from a previous call, or null for the first page + * @param pageSize the maximum number of instance IDs to return, or null for no limit + * @return the raw list-instance-IDs response from the sidecar + */ + public abstract OrchestratorService.ListInstanceIDsResponse listInstanceIds( + @Nullable String continuationToken, @Nullable Integer pageSize); + + /** + * Gets the full execution history of a workflow instance. + * + * @param instanceId the ID of the workflow instance to get history for + * @return the list of history events for the workflow instance + */ + public abstract List getInstanceHistory(String instanceId); + + /** + * Reruns a workflow from a specific history event, creating a new workflow instance. + * + * @param sourceInstanceId the ID of the source workflow instance to rerun from + * @param eventId the history event ID to rerun from + * @param newInstanceId the instance ID to use for the new instance, or null for a random ID + * @param input the input applied at the next activity event, used only when overwriteInput is true + * @param overwriteInput true to overwrite the input at the rerun point with input + * @return the instance ID of the new workflow instance + */ + public abstract String rerunWorkflowFromEvent(String sourceInstanceId, int eventId, + @Nullable String newInstanceId, @Nullable Object input, boolean overwriteInput); } diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java b/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java index 4f27bdef94..b954e09f9f 100644 --- a/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java +++ b/durabletask-client/src/main/java/io/dapr/durabletask/DurableTaskGrpcClient.java @@ -15,6 +15,7 @@ import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent; import io.dapr.durabletask.implementation.protobuf.Orchestration; import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; @@ -45,6 +46,7 @@ import java.time.Duration; import java.time.Instant; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -448,6 +450,57 @@ public String restartInstance(String instanceId, boolean restartWithNewInstanceI } } + @Override + public OrchestratorService.ListInstanceIDsResponse listInstanceIds( + @Nullable String continuationToken, @Nullable Integer pageSize) { + OrchestratorService.ListInstanceIDsRequest.Builder builder = + OrchestratorService.ListInstanceIDsRequest.newBuilder(); + if (continuationToken != null) { + builder.setContinuationToken(continuationToken); + } + if (pageSize != null) { + if (pageSize <= 0) { + throw new IllegalArgumentException("pageSize must be greater than zero."); + } + builder.setPageSize(pageSize); + } + return this.sidecarClient.listInstanceIDs(builder.build()); + } + + @Override + public List getInstanceHistory(String instanceId) { + Helpers.throwIfArgumentNull(instanceId, "instanceId"); + OrchestratorService.GetInstanceHistoryRequest request = + OrchestratorService.GetInstanceHistoryRequest.newBuilder() + .setInstanceId(instanceId) + .build(); + OrchestratorService.GetInstanceHistoryResponse response = this.sidecarClient.getInstanceHistory(request); + return response.getEventsList(); + } + + @Override + public String rerunWorkflowFromEvent(String sourceInstanceId, int eventId, + @Nullable String newInstanceId, @Nullable Object input, boolean overwriteInput) { + Helpers.throwIfArgumentNull(sourceInstanceId, "sourceInstanceId"); + OrchestratorService.RerunWorkflowFromEventRequest.Builder builder = + OrchestratorService.RerunWorkflowFromEventRequest.newBuilder() + .setSourceInstanceID(sourceInstanceId) + .setEventID(eventId) + .setOverwriteInput(overwriteInput); + if (newInstanceId != null) { + builder.setNewInstanceID(newInstanceId); + } + if (overwriteInput) { + String serializedInput = this.dataConverter.serialize(input); + if (serializedInput != null) { + builder.setInput(StringValue.of(serializedInput)); + } + } + OrchestratorService.RerunWorkflowFromEventResponse response = + this.sidecarClient.rerunWorkflowFromEvent(builder.build()); + return response.getNewInstanceID(); + } + private PurgeResult toPurgeResult(OrchestratorService.PurgeInstancesResponse response) { return new PurgeResult(response.getDeletedInstanceCount()); } diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java b/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java index 6c870b8452..4a7c8a1ec3 100644 --- a/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java +++ b/durabletask-client/src/test/java/io/dapr/durabletask/DurableTaskClientIT.java @@ -40,6 +40,9 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents; +import io.dapr.durabletask.implementation.protobuf.OrchestratorService; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -1746,6 +1749,88 @@ public void taskExecutionIdTest() { } + @Test + void getInstanceHistoryReturnsEvents() throws TimeoutException { + final String orchestratorName = "historyTest"; + DurableTaskGrpcWorker worker = this.createWorkerBuilder() + .addOrchestrator(orchestratorName, ctx -> ctx.complete(ctx.getInput(String.class))) + .buildAndStart(); + + DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); + try (worker; client) { + String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, "hello"); + client.waitForInstanceCompletion(instanceId, defaultTimeout, true); + + List history = client.getInstanceHistory(instanceId); + + assertFalse(history.isEmpty()); + } + } + + @Test + void listInstanceIdsReturnsScheduledInstance() throws TimeoutException { + final String orchestratorName = "listTest"; + DurableTaskGrpcWorker worker = this.createWorkerBuilder() + .addOrchestrator(orchestratorName, ctx -> ctx.complete(ctx.getInput(String.class))) + .buildAndStart(); + + DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); + try (worker; client) { + String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, "hello"); + client.waitForInstanceCompletion(instanceId, defaultTimeout, true); + + List ids = new ArrayList<>(); + String token = null; + do { + OrchestratorService.ListInstanceIDsResponse page = client.listInstanceIds(token, 100); + ids.addAll(page.getInstanceIdsList()); + token = page.hasContinuationToken() ? page.getContinuationToken() : null; + } while (token != null); + + assertTrue(ids.contains(instanceId)); + } + } + + @Test + void rerunWorkflowFromEventCreatesNewInstance() throws TimeoutException { + final String orchestratorName = "rerunTest"; + DurableTaskGrpcWorker worker = this.createWorkerBuilder() + .addOrchestrator(orchestratorName, ctx -> ctx.complete(ctx.getInput(String.class))) + .buildAndStart(); + + DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); + try (worker; client) { + String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, "hello"); + client.waitForInstanceCompletion(instanceId, defaultTimeout, true); + + String newInstanceId = client.rerunWorkflowFromEvent(instanceId, 0, null, null, false); + + assertNotEquals(instanceId, newInstanceId); + OrchestrationMetadata instance = client.waitForInstanceCompletion(newInstanceId, defaultTimeout, true); + assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + } + } + + @Test + void rerunWorkflowFromEventWithOverwriteNullInputDoesNotThrow() throws TimeoutException { + final String orchestratorName = "rerunNullInputTest"; + DurableTaskGrpcWorker worker = this.createWorkerBuilder() + .addOrchestrator(orchestratorName, ctx -> ctx.complete(ctx.getInput(String.class))) + .buildAndStart(); + + DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); + try (worker; client) { + String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, "hello"); + client.waitForInstanceCompletion(instanceId, defaultTimeout, true); + + String newInstanceId = client.rerunWorkflowFromEvent(instanceId, 0, null, null, true); + + assertNotEquals(instanceId, newInstanceId); + OrchestrationMetadata instance = client.waitForInstanceCompletion(newInstanceId, defaultTimeout, true); + assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); + } + } + } diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index 167a423d5d..267f3f2e1e 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -984,4 +984,53 @@ The client log: ```text Started a new external-event model workflow with instance ID: 23410d96-1afe-4698-9fcd-c01c1e0db255 workflow instance with ID: 23410d96-1afe-4698-9fcd-c01c1e0db255 completed. -``` \ No newline at end of file +``` + +### Workflow Management (List, History, Rerun) Pattern + +The `DaprWorkflowClient` can list workflow instance IDs, read a workflow instance's full +execution history, and rerun a workflow from a specific history event. This example shows +all three operations. + + + +Run the worker: + +```sh +dapr run --app-id demoworkflowworker --resources-path ./components/workflows --dapr-grpc-port 50005 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.management.DemoWorkflowManagementWorker 50005 +``` + + + + + +In a separate terminal, run the client. It connects to the worker's sidecar on +gRPC port 50005, so the workflow it schedules runs on the worker: + +```sh +java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.management.DemoWorkflowManagementClient 50005 +dapr stop --app-id demoworkflowworker +``` + + + +The client output shows the started instance ID, the completed result, the list of history +events (each with its event ID, type, and timestamp), the new instance ID from the rerun, +and the count of listed instance IDs. \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementActivity.java b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementActivity.java new file mode 100644 index 0000000000..c5a5253c9d --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementActivity.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.management; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DemoWorkflowManagementActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(DemoWorkflowManagementActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + String message = ctx.getInput(String.class); + String newMessage = message.toUpperCase(); + logger.info("Message Received from input: " + message); + return newMessage; + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementClient.java b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementClient.java new file mode 100644 index 0000000000..4e98af6db8 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementClient.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.management; + +import io.dapr.examples.workflows.utils.PropertyUtils; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.RerunWorkflowFromEventOptions; +import io.dapr.workflows.client.WorkflowHistoryEvent; +import io.dapr.workflows.client.WorkflowInstancePage; +import io.dapr.workflows.client.WorkflowState; + +import java.util.List; +import java.util.concurrent.TimeoutException; + +public class DemoWorkflowManagementClient { + /** + * The main method to start the client. + * + * @param args Input arguments (unused). + */ + public static void main(String[] args) { + try (DaprWorkflowClient client = new DaprWorkflowClient(PropertyUtils.getProperties(args))) { + String instanceId = client.scheduleNewWorkflow(DemoWorkflowManagementWorkflow.class); + System.out.printf("Started a new workflow with instance ID: %s%n", instanceId); + + WorkflowState state = client.waitForWorkflowCompletion(instanceId, null, true); + System.out.printf("Workflow completed with result: %s%n", state.readOutputAs(String.class)); + + // Read the full execution history. + List history = client.getInstanceHistory(instanceId); + System.out.printf("History for %s has %d events:%n", instanceId, history.size()); + for (WorkflowHistoryEvent event : history) { + System.out.printf(" eventId=%d type=%s at=%s%n", + event.getEventId(), event.getEventType(), event.getTimestamp()); + } + + // Rerun the workflow from the first history event. + int firstEventId = history.get(0).getEventId(); + String rerunId = client.rerunWorkflowFromEvent(instanceId, firstEventId, + new RerunWorkflowFromEventOptions().setInput("Osaka").setOverwriteInput(true)); + System.out.printf("Reran workflow from event %d as new instance: %s%n", firstEventId, rerunId); + client.waitForWorkflowCompletion(rerunId, null, true); + + // List workflow instance IDs (first page). + WorkflowInstancePage page = client.listInstanceIds(null, 100); + System.out.printf("Listed %d instance ID(s); continuationToken=%s%n", + page.getInstanceIds().size(), page.getContinuationToken()); + } catch (TimeoutException | InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorker.java b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorker.java new file mode 100644 index 0000000000..823e480258 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorker.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.management; + +import io.dapr.examples.workflows.utils.PropertyUtils; +import io.dapr.workflows.runtime.WorkflowRuntime; +import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; + +public class DemoWorkflowManagementWorker { + /** + * The main method of this app. + * + * @param args The port the app will listen on. + * @throws Exception An Exception. + */ + public static void main(String[] args) throws Exception { + WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder(PropertyUtils.getProperties(args)) + .registerWorkflow(DemoWorkflowManagementWorkflow.class); + builder.registerActivity(DemoWorkflowManagementActivity.class); + + WorkflowRuntime runtime = builder.build(); + System.out.println("Start workflow runtime"); + runtime.start(); + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorkflow.java new file mode 100644 index 0000000000..edc721460d --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/management/DemoWorkflowManagementWorkflow.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.management; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; + +public class DemoWorkflowManagementWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + String result = ctx.callActivity( + DemoWorkflowManagementActivity.class.getName(), "Tokyo", String.class).await(); + ctx.getLogger().info("Workflow finished with result: " + result); + ctx.complete(result); + }; + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java index d97b1e288b..2b8c177125 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java @@ -19,6 +19,8 @@ import io.dapr.durabletask.NewOrchestrationInstanceOptions; import io.dapr.durabletask.OrchestrationMetadata; import io.dapr.durabletask.PurgeResult; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent; +import io.dapr.durabletask.implementation.protobuf.OrchestratorService; import io.dapr.utils.NetworkUtils; import io.dapr.workflows.Workflow; import io.dapr.workflows.internal.ApiTokenClientInterceptor; @@ -33,6 +35,7 @@ import javax.annotation.Nullable; import java.time.Duration; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; @@ -437,6 +440,81 @@ public boolean purgeWorkflow(String workflowInstanceId) { return false; } + /** + * Lists workflow instance IDs. Returns the first page with no size limit. + * + * @return a page of workflow instance IDs + */ + public WorkflowInstancePage listInstanceIds() { + return this.listInstanceIds(null, null); + } + + /** + * Lists workflow instance IDs with pagination. + * + * @param continuationToken the continuation token from a previous call, or null for the first page + * @param pageSize the maximum number of instance IDs to return, or null for no limit; must be + * greater than zero when set + * @return a page of workflow instance IDs and an optional continuation token for the next page + */ + public WorkflowInstancePage listInstanceIds(@Nullable String continuationToken, @Nullable Integer pageSize) { + if (pageSize != null && pageSize <= 0) { + throw new IllegalArgumentException("pageSize must be greater than zero."); + } + OrchestratorService.ListInstanceIDsResponse response = + this.innerClient.listInstanceIds(continuationToken, pageSize); + return WorkflowClientConverter.toWorkflowInstancePage(response); + } + + /** + * Gets the full execution history of a workflow instance. + * + * @param instanceId the unique ID of the workflow instance to get history for + * @return the list of history events for the workflow instance + */ + public List getInstanceHistory(String instanceId) { + if (instanceId == null || instanceId.isEmpty()) { + throw new IllegalArgumentException("instanceId must not be null or empty."); + } + List events = this.innerClient.getInstanceHistory(instanceId); + return WorkflowClientConverter.toWorkflowHistory(events); + } + + /** + * Reruns a workflow from a history event, creating a new workflow instance. + * + * @param sourceInstanceId the ID of the source workflow instance to rerun from + * @param eventId the history event ID to rerun from + * @return the instance ID of the new workflow instance + */ + public String rerunWorkflowFromEvent(String sourceInstanceId, int eventId) { + return this.rerunWorkflowFromEvent(sourceInstanceId, eventId, null); + } + + /** + * Reruns a workflow from a history event with options, creating a new workflow instance. + * + * @param sourceInstanceId the ID of the source workflow instance to rerun from + * @param eventId the history event ID to rerun from + * @param options optional rerun configuration; may be null + * @return the instance ID of the new workflow instance + * @throws IllegalArgumentException if input is set on options without overwriteInput being true + */ + public String rerunWorkflowFromEvent(String sourceInstanceId, int eventId, + @Nullable RerunWorkflowFromEventOptions options) { + if (sourceInstanceId == null || sourceInstanceId.isEmpty()) { + throw new IllegalArgumentException("sourceInstanceId must not be null or empty."); + } + if (options == null) { + return this.innerClient.rerunWorkflowFromEvent(sourceInstanceId, eventId, null, null, false); + } + if (options.getInput() != null && !options.isOverwriteInput()) { + throw new IllegalArgumentException("overwriteInput must be true when input is set."); + } + return this.innerClient.rerunWorkflowFromEvent(sourceInstanceId, eventId, + options.getNewInstanceId(), options.getInput(), options.isOverwriteInput()); + } + /** * Closes the inner DurableTask client and shutdown the GRPC channel. */ diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/RerunWorkflowFromEventOptions.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/RerunWorkflowFromEventOptions.java new file mode 100644 index 0000000000..d5d12f826c --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/RerunWorkflowFromEventOptions.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +import javax.annotation.Nullable; + +/** + * Options for the {@link DaprWorkflowClient#rerunWorkflowFromEvent(String, int, RerunWorkflowFromEventOptions)} + * operation. + */ +public final class RerunWorkflowFromEventOptions { + + @Nullable + private String newInstanceId; + @Nullable + private Object input; + private boolean overwriteInput; + + /** + * Sets the instance ID to use for the new workflow instance. When not set, a random ID is generated. + * + * @param newInstanceId the new instance ID + * @return this {@link RerunWorkflowFromEventOptions} object + */ + public RerunWorkflowFromEventOptions setNewInstanceId(String newInstanceId) { + this.newInstanceId = newInstanceId; + return this; + } + + /** + * Sets the input applied at the next activity event of the rerun instance. When set, + * {@link #setOverwriteInput(boolean)} must also be set to true. + * + * @param input the input to apply + * @return this {@link RerunWorkflowFromEventOptions} object + */ + public RerunWorkflowFromEventOptions setInput(Object input) { + this.input = input; + return this; + } + + /** + * Sets whether the input at the rerun point is overwritten with {@link #setInput(Object)}. + * + * @param overwriteInput true to overwrite the input + * @return this {@link RerunWorkflowFromEventOptions} object + */ + public RerunWorkflowFromEventOptions setOverwriteInput(boolean overwriteInput) { + this.overwriteInput = overwriteInput; + return this; + } + + /** + * Gets the new instance ID. + * + * @return the new instance ID, or null if not set + */ + @Nullable + public String getNewInstanceId() { + return this.newInstanceId; + } + + /** + * Gets the input to apply at the rerun point. + * + * @return the input, or null if not set + */ + @Nullable + public Object getInput() { + return this.input; + } + + /** + * Gets whether the input at the rerun point is overwritten. + * + * @return true if the input is overwritten + */ + public boolean isOverwriteInput() { + return this.overwriteInput; + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowClientConverter.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowClientConverter.java new file mode 100644 index 0000000000..64e901fcd0 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowClientConverter.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +import com.google.protobuf.Timestamp; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent; +import io.dapr.durabletask.implementation.protobuf.OrchestratorService.ListInstanceIDsResponse; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Converts durabletask proto messages to public workflow client model types. + */ +final class WorkflowClientConverter { + + private WorkflowClientConverter() { + } + + static WorkflowInstancePage toWorkflowInstancePage(ListInstanceIDsResponse response) { + return new WorkflowInstancePage( + new ArrayList<>(response.getInstanceIdsList()), + response.hasContinuationToken() ? response.getContinuationToken() : null); + } + + static List toWorkflowHistory(List events) { + List result = new ArrayList<>(events.size()); + for (HistoryEvent event : events) { + result.add(toWorkflowHistoryEvent(event)); + } + return Collections.unmodifiableList(result); + } + + static WorkflowHistoryEvent toWorkflowHistoryEvent(HistoryEvent event) { + Instant timestamp = event.hasTimestamp() ? toInstant(event.getTimestamp()) : Instant.EPOCH; + return new WorkflowHistoryEvent(event.getEventId(), toEventType(event.getEventTypeCase()), timestamp); + } + + static WorkflowHistoryEventType toEventType(HistoryEvent.EventTypeCase eventType) { + switch (eventType) { + case EXECUTIONSTARTED: + return WorkflowHistoryEventType.EXECUTION_STARTED; + case EXECUTIONCOMPLETED: + return WorkflowHistoryEventType.EXECUTION_COMPLETED; + case EXECUTIONTERMINATED: + return WorkflowHistoryEventType.EXECUTION_TERMINATED; + case TASKSCHEDULED: + return WorkflowHistoryEventType.TASK_SCHEDULED; + case TASKCOMPLETED: + return WorkflowHistoryEventType.TASK_COMPLETED; + case TASKFAILED: + return WorkflowHistoryEventType.TASK_FAILED; + case CHILDWORKFLOWINSTANCECREATED: + case DETACHEDWORKFLOWINSTANCECREATED: + return WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_CREATED; + case CHILDWORKFLOWINSTANCECOMPLETED: + return WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_COMPLETED; + case CHILDWORKFLOWINSTANCEFAILED: + return WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_FAILED; + case TIMERCREATED: + return WorkflowHistoryEventType.TIMER_CREATED; + case TIMERFIRED: + return WorkflowHistoryEventType.TIMER_FIRED; + case WORKFLOWSTARTED: + return WorkflowHistoryEventType.WORKFLOW_STARTED; + case WORKFLOWCOMPLETED: + return WorkflowHistoryEventType.WORKFLOW_COMPLETED; + case EVENTSENT: + return WorkflowHistoryEventType.EVENT_SENT; + case EVENTRAISED: + return WorkflowHistoryEventType.EVENT_RAISED; + case CONTINUEASNEW: + return WorkflowHistoryEventType.CONTINUE_AS_NEW; + case EXECUTIONSUSPENDED: + return WorkflowHistoryEventType.EXECUTION_SUSPENDED; + case EXECUTIONRESUMED: + return WorkflowHistoryEventType.EXECUTION_RESUMED; + case EXECUTIONSTALLED: + return WorkflowHistoryEventType.EXECUTION_STALLED; + default: + return WorkflowHistoryEventType.UNKNOWN; + } + } + + private static Instant toInstant(Timestamp timestamp) { + return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEvent.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEvent.java new file mode 100644 index 0000000000..99ec3077a3 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEvent.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +import java.time.Instant; + +/** + * Represents a single event in a workflow instance's execution history. + */ +public final class WorkflowHistoryEvent { + + private final int eventId; + private final WorkflowHistoryEventType eventType; + private final Instant timestamp; + + /** + * Constructs a workflow history event. + * + * @param eventId the event ID within the workflow instance history + * @param eventType the type of history event + * @param timestamp the time the event occurred + */ + public WorkflowHistoryEvent(int eventId, WorkflowHistoryEventType eventType, Instant timestamp) { + this.eventId = eventId; + this.eventType = eventType; + this.timestamp = timestamp; + } + + /** + * Gets the event ID within the workflow instance history. + * + * @return the event ID + */ + public int getEventId() { + return this.eventId; + } + + /** + * Gets the type of this history event. + * + * @return the event type + */ + public WorkflowHistoryEventType getEventType() { + return this.eventType; + } + + /** + * Gets the time this event occurred. + * + * @return the event timestamp + */ + public Instant getTimestamp() { + return this.timestamp; + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEventType.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEventType.java new file mode 100644 index 0000000000..662c4925e7 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowHistoryEventType.java @@ -0,0 +1,119 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +/** + * Represents the type of a workflow history event. + */ +public enum WorkflowHistoryEventType { + /** + * Unknown or unmapped event type. + */ + UNKNOWN, + + /** + * The workflow execution started. + */ + EXECUTION_STARTED, + + /** + * The workflow execution completed. + */ + EXECUTION_COMPLETED, + + /** + * The workflow execution was terminated. + */ + EXECUTION_TERMINATED, + + /** + * An activity task was scheduled. + */ + TASK_SCHEDULED, + + /** + * An activity task completed successfully. + */ + TASK_COMPLETED, + + /** + * An activity task failed. + */ + TASK_FAILED, + + /** + * A child workflow instance was created. + */ + CHILD_WORKFLOW_INSTANCE_CREATED, + + /** + * A child workflow instance completed. + */ + CHILD_WORKFLOW_INSTANCE_COMPLETED, + + /** + * A child workflow instance failed. + */ + CHILD_WORKFLOW_INSTANCE_FAILED, + + /** + * A timer was created. + */ + TIMER_CREATED, + + /** + * A timer fired. + */ + TIMER_FIRED, + + /** + * The workflow started processing a work item. + */ + WORKFLOW_STARTED, + + /** + * The workflow completed processing a work item. + */ + WORKFLOW_COMPLETED, + + /** + * An event was sent to another instance. + */ + EVENT_SENT, + + /** + * An external event was raised. + */ + EVENT_RAISED, + + /** + * The workflow continued as new. + */ + CONTINUE_AS_NEW, + + /** + * The workflow execution was suspended. + */ + EXECUTION_SUSPENDED, + + /** + * The workflow execution was resumed. + */ + EXECUTION_RESUMED, + + /** + * The workflow execution stalled. + */ + EXECUTION_STALLED +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstancePage.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstancePage.java new file mode 100644 index 0000000000..2d1d81d742 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/WorkflowInstancePage.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a page of workflow instance IDs returned by a list operation. + */ +public final class WorkflowInstancePage { + + private final List instanceIds; + @Nullable + private final String continuationToken; + + /** + * Constructs a page of workflow instance IDs. + * + * @param instanceIds the workflow instance IDs in this page; must not be null + * @param continuationToken the token used to retrieve the next page, or null if there are no more pages + */ + public WorkflowInstancePage(List instanceIds, @Nullable String continuationToken) { + this.instanceIds = Collections.unmodifiableList(new ArrayList<>(instanceIds)); + this.continuationToken = continuationToken; + } + + /** + * Gets the workflow instance IDs in this page. + * + * @return an unmodifiable list of instance IDs + */ + public List getInstanceIds() { + return this.instanceIds; + } + + /** + * Gets the continuation token for the next page. + * + * @return the continuation token, or null if there are no more pages + */ + @Nullable + public String getContinuationToken() { + return this.continuationToken; + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java index f88a7dbcc0..dc9678ecc0 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java @@ -13,12 +13,16 @@ package io.dapr.workflows.client; +import com.google.protobuf.Timestamp; import io.dapr.config.Properties; import io.dapr.durabletask.DurableTaskClient; import io.dapr.durabletask.DurableTaskGrpcClientBuilder; import io.dapr.durabletask.NewOrchestrationInstanceOptions; import io.dapr.durabletask.OrchestrationMetadata; import io.dapr.durabletask.OrchestrationRuntimeStatus; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent; +import io.dapr.durabletask.implementation.protobuf.OrchestratorService.ListInstanceIDsResponse; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowContext; import io.dapr.workflows.WorkflowStub; @@ -36,14 +40,18 @@ import java.time.Duration; import java.time.Instant; import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeoutException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; @@ -366,6 +374,91 @@ public void purgeInstance() { verify(mockInnerClient, times(1)).purgeInstance(expectedArgument); } + @Test + public void listInstanceIds() { + ListInstanceIDsResponse response = ListInstanceIDsResponse.newBuilder() + .addInstanceIds("id-1").addInstanceIds("id-2") + .setContinuationToken("next-token") + .build(); + when(mockInnerClient.listInstanceIds("tok", 50)).thenReturn(response); + + WorkflowInstancePage page = client.listInstanceIds("tok", 50); + + verify(mockInnerClient, times(1)).listInstanceIds("tok", 50); + assertEquals(Arrays.asList("id-1", "id-2"), page.getInstanceIds()); + assertEquals("next-token", page.getContinuationToken()); + } + + @Test + public void listInstanceIdsNoArgs() { + ListInstanceIDsResponse response = ListInstanceIDsResponse.newBuilder().addInstanceIds("id-1").build(); + when(mockInnerClient.listInstanceIds(null, null)).thenReturn(response); + + WorkflowInstancePage page = client.listInstanceIds(); + + verify(mockInnerClient, times(1)).listInstanceIds(null, null); + assertEquals(Arrays.asList("id-1"), page.getInstanceIds()); + assertNull(page.getContinuationToken()); + } + + @Test + public void listInstanceIdsRejectsNonPositivePageSize() { + assertThrows(IllegalArgumentException.class, () -> client.listInstanceIds(null, 0)); + verify(mockInnerClient, never()).listInstanceIds(any(), any()); + } + + @Test + public void getInstanceHistory() { + HistoryEvent event = HistoryEvent.newBuilder() + .setEventId(1) + .setTimestamp(Timestamp.newBuilder().setSeconds(10).build()) + .setExecutionStarted(HistoryEvents.ExecutionStartedEvent.getDefaultInstance()) + .build(); + when(mockInnerClient.getInstanceHistory("wf-1")).thenReturn(Arrays.asList(event)); + + List history = client.getInstanceHistory("wf-1"); + + verify(mockInnerClient, times(1)).getInstanceHistory("wf-1"); + assertEquals(1, history.size()); + assertEquals(1, history.get(0).getEventId()); + assertEquals(WorkflowHistoryEventType.EXECUTION_STARTED, history.get(0).getEventType()); + } + + @Test + public void getInstanceHistoryRejectsEmptyId() { + assertThrows(IllegalArgumentException.class, () -> client.getInstanceHistory("")); + verify(mockInnerClient, never()).getInstanceHistory(any()); + } + + @Test + public void rerunWorkflowFromEvent() { + when(mockInnerClient.rerunWorkflowFromEvent("src", 2, null, null, false)).thenReturn("new-id"); + + String newId = client.rerunWorkflowFromEvent("src", 2); + + verify(mockInnerClient, times(1)).rerunWorkflowFromEvent("src", 2, null, null, false); + assertEquals("new-id", newId); + } + + @Test + public void rerunWorkflowFromEventWithOptions() { + RerunWorkflowFromEventOptions options = new RerunWorkflowFromEventOptions() + .setNewInstanceId("target").setInput("payload").setOverwriteInput(true); + when(mockInnerClient.rerunWorkflowFromEvent("src", 3, "target", "payload", true)).thenReturn("target"); + + String newId = client.rerunWorkflowFromEvent("src", 3, options); + + verify(mockInnerClient, times(1)).rerunWorkflowFromEvent("src", 3, "target", "payload", true); + assertEquals("target", newId); + } + + @Test + public void rerunWorkflowFromEventRejectsInputWithoutOverwrite() { + RerunWorkflowFromEventOptions options = new RerunWorkflowFromEventOptions().setInput("payload"); + assertThrows(IllegalArgumentException.class, () -> client.rerunWorkflowFromEvent("src", 1, options)); + verify(mockInnerClient, never()).rerunWorkflowFromEvent(any(), anyInt(), any(), any(), anyBoolean()); + } + @Test public void close() throws InterruptedException { client.close(); diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/RerunWorkflowFromEventOptionsTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/RerunWorkflowFromEventOptionsTest.java new file mode 100644 index 0000000000..fe3cb35ded --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/RerunWorkflowFromEventOptionsTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class RerunWorkflowFromEventOptionsTest { + + @Test + public void fluentSettersReturnSameInstanceAndStoreValues() { + RerunWorkflowFromEventOptions options = new RerunWorkflowFromEventOptions(); + assertSame(options, options.setNewInstanceId("target")); + assertSame(options, options.setInput("payload")); + assertSame(options, options.setOverwriteInput(true)); + + assertEquals("target", options.getNewInstanceId()); + assertEquals("payload", options.getInput()); + assertEquals(true, options.isOverwriteInput()); + } + + @Test + public void overwriteInputDefaultsToFalse() { + assertFalse(new RerunWorkflowFromEventOptions().isOverwriteInput()); + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowClientConverterTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowClientConverterTest.java new file mode 100644 index 0000000000..a3e6f9b303 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowClientConverterTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +import com.google.protobuf.Timestamp; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents; +import io.dapr.durabletask.implementation.protobuf.HistoryEvents.HistoryEvent; +import io.dapr.durabletask.implementation.protobuf.OrchestratorService.ListInstanceIDsResponse; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class WorkflowClientConverterTest { + + @Test + public void mapsEventTypeCases() { + // Execution events + assertEquals(WorkflowHistoryEventType.EXECUTION_STARTED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONSTARTED)); + assertEquals(WorkflowHistoryEventType.EXECUTION_COMPLETED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONCOMPLETED)); + assertEquals(WorkflowHistoryEventType.EXECUTION_TERMINATED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONTERMINATED)); + assertEquals(WorkflowHistoryEventType.EXECUTION_SUSPENDED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONSUSPENDED)); + assertEquals(WorkflowHistoryEventType.EXECUTION_RESUMED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONRESUMED)); + assertEquals(WorkflowHistoryEventType.EXECUTION_STALLED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EXECUTIONSTALLED)); + + // Task events + assertEquals(WorkflowHistoryEventType.TASK_SCHEDULED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TASKSCHEDULED)); + assertEquals(WorkflowHistoryEventType.TASK_COMPLETED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TASKCOMPLETED)); + assertEquals(WorkflowHistoryEventType.TASK_FAILED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TASKFAILED)); + + // Child workflow events + assertEquals(WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_CREATED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.CHILDWORKFLOWINSTANCECREATED)); + assertEquals(WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_CREATED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.DETACHEDWORKFLOWINSTANCECREATED)); + assertEquals(WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_COMPLETED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.CHILDWORKFLOWINSTANCECOMPLETED)); + assertEquals(WorkflowHistoryEventType.CHILD_WORKFLOW_INSTANCE_FAILED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.CHILDWORKFLOWINSTANCEFAILED)); + + // Timer events + assertEquals(WorkflowHistoryEventType.TIMER_CREATED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TIMERCREATED)); + assertEquals(WorkflowHistoryEventType.TIMER_FIRED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.TIMERFIRED)); + + // Workflow events + assertEquals(WorkflowHistoryEventType.WORKFLOW_STARTED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.WORKFLOWSTARTED)); + assertEquals(WorkflowHistoryEventType.WORKFLOW_COMPLETED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.WORKFLOWCOMPLETED)); + + // Event communication + assertEquals(WorkflowHistoryEventType.EVENT_SENT, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EVENTSENT)); + assertEquals(WorkflowHistoryEventType.EVENT_RAISED, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EVENTRAISED)); + + // Continue as new + assertEquals(WorkflowHistoryEventType.CONTINUE_AS_NEW, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.CONTINUEASNEW)); + + // Unknown/unset + assertEquals(WorkflowHistoryEventType.UNKNOWN, + WorkflowClientConverter.toEventType(HistoryEvent.EventTypeCase.EVENTTYPE_NOT_SET)); + } + + @Test + public void mapsHistoryEvent() { + HistoryEvent event = HistoryEvent.newBuilder() + .setEventId(7) + .setTimestamp(Timestamp.newBuilder().setSeconds(1500).setNanos(500).build()) + .setExecutionStarted(HistoryEvents.ExecutionStartedEvent.getDefaultInstance()) + .build(); + + WorkflowHistoryEvent result = WorkflowClientConverter.toWorkflowHistoryEvent(event); + + assertEquals(7, result.getEventId()); + assertEquals(WorkflowHistoryEventType.EXECUTION_STARTED, result.getEventType()); + assertEquals(Instant.ofEpochSecond(1500, 500), result.getTimestamp()); + } + + @Test + public void mapsHistoryList() { + HistoryEvent event = HistoryEvent.newBuilder() + .setEventId(1) + .setTimerCreated(HistoryEvents.TimerCreatedEvent.getDefaultInstance()) + .build(); + + assertEquals(1, WorkflowClientConverter.toWorkflowHistory(Arrays.asList(event)).size()); + assertEquals(WorkflowHistoryEventType.TIMER_CREATED, + WorkflowClientConverter.toWorkflowHistory(Arrays.asList(event)).get(0).getEventType()); + } + + @Test + public void usesEpochTimestampWhenNotSet() { + HistoryEvent event = HistoryEvent.newBuilder() + .setEventId(42) + .setTaskScheduled(HistoryEvents.TaskScheduledEvent.getDefaultInstance()) + .build(); + + WorkflowHistoryEvent result = WorkflowClientConverter.toWorkflowHistoryEvent(event); + + assertEquals(Instant.EPOCH, result.getTimestamp()); + } + + @Test + public void mapsInstancePageWithToken() { + ListInstanceIDsResponse response = ListInstanceIDsResponse.newBuilder() + .addInstanceIds("a").addInstanceIds("b") + .setContinuationToken("next") + .build(); + + WorkflowInstancePage page = WorkflowClientConverter.toWorkflowInstancePage(response); + + assertEquals(Arrays.asList("a", "b"), page.getInstanceIds()); + assertEquals("next", page.getContinuationToken()); + } + + @Test + public void mapsInstancePageWithoutToken() { + ListInstanceIDsResponse response = ListInstanceIDsResponse.newBuilder().addInstanceIds("a").build(); + + WorkflowInstancePage page = WorkflowClientConverter.toWorkflowInstancePage(response); + + assertNull(page.getContinuationToken()); + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowHistoryEventTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowHistoryEventTest.java new file mode 100644 index 0000000000..5feeca5f59 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowHistoryEventTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WorkflowHistoryEventTest { + + @Test + public void exposesFields() { + Instant now = Instant.ofEpochSecond(1000, 5); + WorkflowHistoryEvent event = new WorkflowHistoryEvent(3, WorkflowHistoryEventType.TASK_SCHEDULED, now); + assertEquals(3, event.getEventId()); + assertEquals(WorkflowHistoryEventType.TASK_SCHEDULED, event.getEventType()); + assertEquals(now, event.getTimestamp()); + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstancePageTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstancePageTest.java new file mode 100644 index 0000000000..8f9dc73d43 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/WorkflowInstancePageTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.workflows.client; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class WorkflowInstancePageTest { + + @Test + public void exposesInstanceIdsAndToken() { + WorkflowInstancePage page = new WorkflowInstancePage(Arrays.asList("a", "b"), "next"); + assertEquals(Arrays.asList("a", "b"), page.getInstanceIds()); + assertEquals("next", page.getContinuationToken()); + } + + @Test + public void allowsNullContinuationToken() { + WorkflowInstancePage page = new WorkflowInstancePage(Arrays.asList("a"), null); + assertNull(page.getContinuationToken()); + } + + @Test + public void instanceIdsListIsUnmodifiable() { + WorkflowInstancePage page = new WorkflowInstancePage(Arrays.asList("a"), null); + assertThrows(UnsupportedOperationException.class, () -> page.getInstanceIds().add("b")); + } +}