diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskClient.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskClient.java index 117f9d6..5c1a467 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskClient.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskClient.java @@ -2,8 +2,11 @@ // Licensed under the MIT License. package com.microsoft.durabletask; +import com.microsoft.durabletask.history.HistoryEvent; + import javax.annotation.Nullable; import java.time.Duration; +import java.util.List; import java.util.concurrent.TimeoutException; /** @@ -226,6 +229,31 @@ public abstract OrchestrationMetadata waitForInstanceCompletion( */ public abstract OrchestrationStatusQueryResult queryInstances(OrchestrationStatusQuery query); + /** + * Lists the IDs of terminal orchestration instances that completed within a time window. + *

+ * Unlike {@link #queryInstances(OrchestrationStatusQuery)}, which filters by creation time and returns full + * metadata, this method filters by completion time and returns only instance IDs, making it efficient + * for bulk enumeration such as archival/export. Results are paged; pass + * {@link ListInstanceIdsResult#getContinuationToken()} back via + * {@link ListInstanceIdsQuery#setContinuationToken(String)} to fetch subsequent pages. + * + * @param query filter criteria: completion-time window, terminal runtime statuses, page size, and pagination cursor + * @return a page of matching instance IDs and a cursor for the next page + */ + public abstract ListInstanceIdsResult listInstanceIds(ListInstanceIdsQuery query); + + /** + * Gets the full history of an orchestration instance as an ordered list of {@link HistoryEvent} objects. + *

+ * The events are returned in execution order. This is useful for archiving or offline analysis of an instance's + * execution history. Use {@code instanceof} to inspect each concrete event type. + * + * @param instanceId the unique ID of the orchestration instance whose history to fetch + * @return the instance's history events in order; empty if the instance has no history + */ + public abstract List getOrchestrationHistory(String instanceId); + /** * Initializes the target task hub data store. *

diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java index 3c588cd..02e62bf 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java @@ -4,6 +4,8 @@ import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; +import com.microsoft.durabletask.history.HistoryEvent; +import com.microsoft.durabletask.implementation.protobuf.OrchestratorService; import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.*; import com.microsoft.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc; import com.microsoft.durabletask.implementation.protobuf.TaskHubSidecarServiceGrpc.*; @@ -295,6 +297,37 @@ private OrchestrationStatusQueryResult toQueryResult(QueryInstancesResponse quer return new OrchestrationStatusQueryResult(metadataList, queryInstancesResponse.getContinuationToken().getValue()); } + @Override + public ListInstanceIdsResult listInstanceIds(ListInstanceIdsQuery query) { + Helpers.throwIfArgumentNull(query, "query"); + ListInstanceIdsRequest.Builder builder = ListInstanceIdsRequest.newBuilder(); + Optional.ofNullable(query.getCompletedTimeFrom()).ifPresent(from -> builder.setCompletedTimeFrom(DataConverter.getTimestampFromInstant(from))); + Optional.ofNullable(query.getCompletedTimeTo()).ifPresent(to -> builder.setCompletedTimeTo(DataConverter.getTimestampFromInstant(to))); + String requestContinuationToken = query.getContinuationToken() != null ? query.getContinuationToken() : ""; + builder.setLastInstanceKey(StringValue.of(requestContinuationToken)); + builder.setPageSize(query.getPageSize()); + query.getRuntimeStatusList().forEach(runtimeStatus -> Optional.ofNullable(runtimeStatus).ifPresent(status -> builder.addRuntimeStatus(OrchestrationRuntimeStatus.toProtobuf(status)))); + ListInstanceIdsResponse response = this.sidecarClient.listInstanceIds(builder.build()); + String continuationToken = response.hasLastInstanceKey() ? response.getLastInstanceKey().getValue() : null; + return new ListInstanceIdsResult(new ArrayList<>(response.getInstanceIdsList()), continuationToken); + } + + @Override + public List getOrchestrationHistory(String instanceId) { + Helpers.throwIfArgumentNull(instanceId, "instanceId"); + StreamInstanceHistoryRequest request = StreamInstanceHistoryRequest.newBuilder() + .setInstanceId(instanceId) + .build(); + List historyEvents = new ArrayList<>(); + Iterator chunks = this.sidecarClient.streamInstanceHistory(request); + while (chunks.hasNext()) { + for (OrchestratorService.HistoryEvent protoEvent : chunks.next().getEventsList()) { + historyEvents.add(HistoryEventConverter.fromProto(protoEvent)); + } + } + return historyEvents; + } + @Override public void createTaskHub(boolean recreateIfExists) { this.sidecarClient.createTaskHub(CreateTaskHubRequest.newBuilder().setRecreateIfExists(recreateIfExists).build()); diff --git a/client/src/main/java/com/microsoft/durabletask/EntityQueryPageable.java b/client/src/main/java/com/microsoft/durabletask/EntityQueryPageable.java index fcf07ab..07158bf 100644 --- a/client/src/main/java/com/microsoft/durabletask/EntityQueryPageable.java +++ b/client/src/main/java/com/microsoft/durabletask/EntityQueryPageable.java @@ -17,7 +17,7 @@ *

* Use {@link DurableEntityClient#getAllEntities(EntityQuery)} to obtain an instance of this class. * - *

Example: iterate over all entities

+ *

Example: iterate over all entities

*
{@code
  * EntityQuery query = new EntityQuery()
  *     .setInstanceIdStartsWith("counter")
@@ -28,7 +28,7 @@
  * }
  * }
* - *

Example: iterate page by page

+ *

Example: iterate page by page

*
{@code
  * for (EntityQueryResult page : client.getEntities().getAllEntities(query).byPage()) {
  *     System.out.println("Got " + page.getEntities().size() + " entities");
diff --git a/client/src/main/java/com/microsoft/durabletask/HistoryEventConverter.java b/client/src/main/java/com/microsoft/durabletask/HistoryEventConverter.java
new file mode 100644
index 0000000..53e3a8d
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/HistoryEventConverter.java
@@ -0,0 +1,246 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask;
+
+import com.microsoft.durabletask.history.*;
+import com.microsoft.durabletask.implementation.protobuf.OrchestratorService;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * Converts protobuf {@code HistoryEvent} messages into the public {@link HistoryEvent} domain model.
+ */
+final class HistoryEventConverter {
+    private HistoryEventConverter() {
+    }
+
+    /**
+     * Converts a protobuf history event into its domain representation.
+     *
+     * @param proto the protobuf history event
+     * @return the domain {@link HistoryEvent}
+     * @throws IllegalArgumentException if the event type is not set
+     * @throws UnsupportedOperationException if the event type is not recognized
+     */
+    static HistoryEvent fromProto(OrchestratorService.HistoryEvent proto) {
+        int id = proto.getEventId();
+        Instant ts = DataConverter.getInstantFromTimestamp(proto.getTimestamp());
+        switch (proto.getEventTypeCase()) {
+            case EXECUTIONSTARTED: {
+                OrchestratorService.ExecutionStartedEvent p = proto.getExecutionStarted();
+                return new ExecutionStartedEvent(id, ts,
+                        p.getName(),
+                        stringOrNull(p.hasVersion(), p.getVersion()),
+                        stringOrNull(p.hasInput(), p.getInput()),
+                        p.hasOrchestrationInstance() ? toInstance(p.getOrchestrationInstance()) : null,
+                        p.hasParentInstance() ? toParentInfo(p.getParentInstance()) : null,
+                        p.hasScheduledStartTimestamp()
+                                ? DataConverter.getInstantFromTimestamp(p.getScheduledStartTimestamp()) : null,
+                        p.hasParentTraceContext() ? toTrace(p.getParentTraceContext()) : null,
+                        stringOrNull(p.hasOrchestrationSpanID(), p.getOrchestrationSpanID()),
+                        p.getTagsMap());
+            }
+            case EXECUTIONCOMPLETED: {
+                OrchestratorService.ExecutionCompletedEvent p = proto.getExecutionCompleted();
+                return new ExecutionCompletedEvent(id, ts,
+                        OrchestrationRuntimeStatus.fromProtobuf(p.getOrchestrationStatus()),
+                        stringOrNull(p.hasResult(), p.getResult()),
+                        p.hasFailureDetails() ? new FailureDetails(p.getFailureDetails()) : null);
+            }
+            case EXECUTIONTERMINATED: {
+                OrchestratorService.ExecutionTerminatedEvent p = proto.getExecutionTerminated();
+                return new ExecutionTerminatedEvent(id, ts, stringOrNull(p.hasInput(), p.getInput()), p.getRecurse());
+            }
+            case TASKSCHEDULED: {
+                OrchestratorService.TaskScheduledEvent p = proto.getTaskScheduled();
+                return new TaskScheduledEvent(id, ts,
+                        p.getName(),
+                        stringOrNull(p.hasVersion(), p.getVersion()),
+                        stringOrNull(p.hasInput(), p.getInput()),
+                        p.hasParentTraceContext() ? toTrace(p.getParentTraceContext()) : null,
+                        p.getTagsMap());
+            }
+            case TASKCOMPLETED: {
+                OrchestratorService.TaskCompletedEvent p = proto.getTaskCompleted();
+                return new TaskCompletedEvent(id, ts, p.getTaskScheduledId(), stringOrNull(p.hasResult(), p.getResult()));
+            }
+            case TASKFAILED: {
+                OrchestratorService.TaskFailedEvent p = proto.getTaskFailed();
+                return new TaskFailedEvent(id, ts, p.getTaskScheduledId(),
+                        p.hasFailureDetails() ? new FailureDetails(p.getFailureDetails()) : null);
+            }
+            case SUBORCHESTRATIONINSTANCECREATED: {
+                OrchestratorService.SubOrchestrationInstanceCreatedEvent p = proto.getSubOrchestrationInstanceCreated();
+                return new SubOrchestrationInstanceCreatedEvent(id, ts,
+                        p.getInstanceId(),
+                        p.getName(),
+                        stringOrNull(p.hasVersion(), p.getVersion()),
+                        stringOrNull(p.hasInput(), p.getInput()),
+                        p.hasParentTraceContext() ? toTrace(p.getParentTraceContext()) : null,
+                        p.getTagsMap());
+            }
+            case SUBORCHESTRATIONINSTANCECOMPLETED: {
+                OrchestratorService.SubOrchestrationInstanceCompletedEvent p =
+                        proto.getSubOrchestrationInstanceCompleted();
+                return new SubOrchestrationInstanceCompletedEvent(id, ts,
+                        p.getTaskScheduledId(), stringOrNull(p.hasResult(), p.getResult()));
+            }
+            case SUBORCHESTRATIONINSTANCEFAILED: {
+                OrchestratorService.SubOrchestrationInstanceFailedEvent p = proto.getSubOrchestrationInstanceFailed();
+                return new SubOrchestrationInstanceFailedEvent(id, ts, p.getTaskScheduledId(),
+                        p.hasFailureDetails() ? new FailureDetails(p.getFailureDetails()) : null);
+            }
+            case TIMERCREATED: {
+                OrchestratorService.TimerCreatedEvent p = proto.getTimerCreated();
+                return new TimerCreatedEvent(id, ts, DataConverter.getInstantFromTimestamp(p.getFireAt()));
+            }
+            case TIMERFIRED: {
+                OrchestratorService.TimerFiredEvent p = proto.getTimerFired();
+                return new TimerFiredEvent(id, ts, DataConverter.getInstantFromTimestamp(p.getFireAt()), p.getTimerId());
+            }
+            case ORCHESTRATORSTARTED:
+                return new OrchestratorStartedEvent(id, ts);
+            case ORCHESTRATORCOMPLETED:
+                return new OrchestratorCompletedEvent(id, ts);
+            case EVENTSENT: {
+                OrchestratorService.EventSentEvent p = proto.getEventSent();
+                return new EventSentEvent(id, ts, p.getInstanceId(), p.getName(),
+                        stringOrNull(p.hasInput(), p.getInput()));
+            }
+            case EVENTRAISED: {
+                OrchestratorService.EventRaisedEvent p = proto.getEventRaised();
+                return new EventRaisedEvent(id, ts, p.getName(), stringOrNull(p.hasInput(), p.getInput()));
+            }
+            case GENERICEVENT: {
+                OrchestratorService.GenericEvent p = proto.getGenericEvent();
+                return new GenericEvent(id, ts, stringOrNull(p.hasData(), p.getData()));
+            }
+            case HISTORYSTATE: {
+                OrchestratorService.HistoryStateEvent p = proto.getHistoryState();
+                return new HistoryStateEvent(id, ts,
+                        p.hasOrchestrationState() ? toOrchestrationState(p.getOrchestrationState()) : null);
+            }
+            case CONTINUEASNEW: {
+                OrchestratorService.ContinueAsNewEvent p = proto.getContinueAsNew();
+                return new ContinueAsNewEvent(id, ts, stringOrNull(p.hasInput(), p.getInput()));
+            }
+            case EXECUTIONSUSPENDED: {
+                OrchestratorService.ExecutionSuspendedEvent p = proto.getExecutionSuspended();
+                return new ExecutionSuspendedEvent(id, ts, stringOrNull(p.hasInput(), p.getInput()));
+            }
+            case EXECUTIONRESUMED: {
+                OrchestratorService.ExecutionResumedEvent p = proto.getExecutionResumed();
+                return new ExecutionResumedEvent(id, ts, stringOrNull(p.hasInput(), p.getInput()));
+            }
+            case ENTITYOPERATIONSIGNALED: {
+                OrchestratorService.EntityOperationSignaledEvent p = proto.getEntityOperationSignaled();
+                return new EntityOperationSignaledEvent(id, ts,
+                        p.getRequestId(),
+                        p.getOperation(),
+                        p.hasScheduledTime() ? DataConverter.getInstantFromTimestamp(p.getScheduledTime()) : null,
+                        stringOrNull(p.hasInput(), p.getInput()),
+                        stringOrNull(p.hasTargetInstanceId(), p.getTargetInstanceId()));
+            }
+            case ENTITYOPERATIONCALLED: {
+                OrchestratorService.EntityOperationCalledEvent p = proto.getEntityOperationCalled();
+                return new EntityOperationCalledEvent(id, ts,
+                        p.getRequestId(),
+                        p.getOperation(),
+                        p.hasScheduledTime() ? DataConverter.getInstantFromTimestamp(p.getScheduledTime()) : null,
+                        stringOrNull(p.hasInput(), p.getInput()),
+                        stringOrNull(p.hasParentInstanceId(), p.getParentInstanceId()),
+                        stringOrNull(p.hasParentExecutionId(), p.getParentExecutionId()),
+                        stringOrNull(p.hasTargetInstanceId(), p.getTargetInstanceId()));
+            }
+            case ENTITYOPERATIONCOMPLETED: {
+                OrchestratorService.EntityOperationCompletedEvent p = proto.getEntityOperationCompleted();
+                return new EntityOperationCompletedEvent(id, ts, p.getRequestId(),
+                        stringOrNull(p.hasOutput(), p.getOutput()));
+            }
+            case ENTITYOPERATIONFAILED: {
+                OrchestratorService.EntityOperationFailedEvent p = proto.getEntityOperationFailed();
+                return new EntityOperationFailedEvent(id, ts, p.getRequestId(),
+                        p.hasFailureDetails() ? new FailureDetails(p.getFailureDetails()) : null);
+            }
+            case ENTITYLOCKREQUESTED: {
+                OrchestratorService.EntityLockRequestedEvent p = proto.getEntityLockRequested();
+                return new EntityLockRequestedEvent(id, ts,
+                        p.getCriticalSectionId(),
+                        p.getLockSetList(),
+                        p.getPosition(),
+                        stringOrNull(p.hasParentInstanceId(), p.getParentInstanceId()));
+            }
+            case ENTITYLOCKGRANTED: {
+                OrchestratorService.EntityLockGrantedEvent p = proto.getEntityLockGranted();
+                return new EntityLockGrantedEvent(id, ts, p.getCriticalSectionId());
+            }
+            case ENTITYUNLOCKSENT: {
+                OrchestratorService.EntityUnlockSentEvent p = proto.getEntityUnlockSent();
+                return new EntityUnlockSentEvent(id, ts,
+                        p.getCriticalSectionId(),
+                        stringOrNull(p.hasParentInstanceId(), p.getParentInstanceId()),
+                        stringOrNull(p.hasTargetInstanceId(), p.getTargetInstanceId()));
+            }
+            case EXECUTIONREWOUND: {
+                OrchestratorService.ExecutionRewoundEvent p = proto.getExecutionRewound();
+                return new ExecutionRewoundEvent(id, ts,
+                        stringOrNull(p.hasReason(), p.getReason()),
+                        stringOrNull(p.hasParentExecutionId(), p.getParentExecutionId()),
+                        stringOrNull(p.hasInstanceId(), p.getInstanceId()),
+                        p.hasParentTraceContext() ? toTrace(p.getParentTraceContext()) : null,
+                        stringOrNull(p.hasName(), p.getName()),
+                        stringOrNull(p.hasVersion(), p.getVersion()),
+                        stringOrNull(p.hasInput(), p.getInput()),
+                        p.hasParentInstance() ? toParentInfo(p.getParentInstance()) : null,
+                        p.getTagsMap());
+            }
+            case EVENTTYPE_NOT_SET:
+                throw new IllegalArgumentException("History event does not have an eventType set.");
+            default:
+                throw new UnsupportedOperationException(
+                        "Deserialization of history event type " + proto.getEventTypeCase() + " is not supported.");
+        }
+    }
+
+    @Nullable
+    private static String stringOrNull(boolean present, com.google.protobuf.StringValue value) {
+        return present ? value.getValue() : null;
+    }
+
+    private static OrchestrationInstance toInstance(OrchestratorService.OrchestrationInstance p) {
+        return new OrchestrationInstance(p.getInstanceId(), stringOrNull(p.hasExecutionId(), p.getExecutionId()));
+    }
+
+    private static ParentInstanceInfo toParentInfo(OrchestratorService.ParentInstanceInfo p) {
+        return new ParentInstanceInfo(
+                p.getTaskScheduledId(),
+                stringOrNull(p.hasName(), p.getName()),
+                stringOrNull(p.hasVersion(), p.getVersion()),
+                p.hasOrchestrationInstance() ? toInstance(p.getOrchestrationInstance()) : null);
+    }
+
+    private static TraceContext toTrace(OrchestratorService.TraceContext p) {
+        return new TraceContext(p.getTraceParent(), stringOrNull(p.hasTraceState(), p.getTraceState()));
+    }
+
+    private static OrchestrationState toOrchestrationState(OrchestratorService.OrchestrationState p) {
+        return new OrchestrationState(
+                p.getInstanceId(),
+                p.getName(),
+                stringOrNull(p.hasVersion(), p.getVersion()),
+                OrchestrationRuntimeStatus.fromProtobuf(p.getOrchestrationStatus()),
+                p.hasScheduledStartTimestamp()
+                        ? DataConverter.getInstantFromTimestamp(p.getScheduledStartTimestamp()) : null,
+                p.hasCreatedTimestamp() ? DataConverter.getInstantFromTimestamp(p.getCreatedTimestamp()) : null,
+                p.hasLastUpdatedTimestamp() ? DataConverter.getInstantFromTimestamp(p.getLastUpdatedTimestamp()) : null,
+                p.hasCompletedTimestamp() ? DataConverter.getInstantFromTimestamp(p.getCompletedTimestamp()) : null,
+                stringOrNull(p.hasInput(), p.getInput()),
+                stringOrNull(p.hasOutput(), p.getOutput()),
+                stringOrNull(p.hasCustomStatus(), p.getCustomStatus()),
+                p.hasFailureDetails() ? new FailureDetails(p.getFailureDetails()) : null,
+                stringOrNull(p.hasExecutionId(), p.getExecutionId()),
+                stringOrNull(p.hasParentInstanceId(), p.getParentInstanceId()),
+                p.getTagsMap());
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java b/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java
new file mode 100644
index 0000000..15a08a1
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Query for listing terminal orchestration instance IDs by completion-time window.
+ * 

+ * Used with {@link DurableTaskClient#listInstanceIds(ListInstanceIdsQuery)} to enumerate the IDs of orchestration + * instances that reached a terminal state within a completion-time window. Unlike {@link OrchestrationStatusQuery} + * (which filters by creation time and returns full metadata), this query filters by completion time and + * returns only instance IDs, making it efficient for bulk enumeration such as archival/export. + */ +public final class ListInstanceIdsQuery { + private List runtimeStatusList = new ArrayList<>(); + private Instant completedTimeFrom; + private Instant completedTimeTo; + private int pageSize = 100; + private String continuationToken; + + /** + * Sole constructor. + */ + public ListInstanceIdsQuery() { + } + + /** + * Sets the terminal runtime status values to filter by. Only instances with a matching status are returned. + * The default empty list disables runtime-status filtering. + * + * @param runtimeStatusList the terminal runtime statuses to filter by (e.g. COMPLETED, FAILED, TERMINATED) + * @return this query object + */ + public ListInstanceIdsQuery setRuntimeStatusList(@Nullable List runtimeStatusList) { + this.runtimeStatusList = runtimeStatusList != null ? new ArrayList<>(runtimeStatusList) : new ArrayList<>(); + return this; + } + + /** + * Includes instances that completed at or after the specified instant. + * + * @param completedTimeFrom the minimum completion time, or {@code null} to disable this filter + * @return this query object + */ + public ListInstanceIdsQuery setCompletedTimeFrom(@Nullable Instant completedTimeFrom) { + this.completedTimeFrom = completedTimeFrom; + return this; + } + + /** + * Includes instances that completed before the specified instant. + * + * @param completedTimeTo the maximum completion time, or {@code null} to disable this filter + * @return this query object + */ + public ListInstanceIdsQuery setCompletedTimeTo(@Nullable Instant completedTimeTo) { + this.completedTimeTo = completedTimeTo; + return this; + } + + /** + * Sets the maximum number of instance IDs to return per page. The default value is 100. + *

+ * A page may contain fewer IDs than the page size even when more results exist; always use + * {@link ListInstanceIdsResult#getContinuationToken()} to determine whether to continue paging. + * + * @param pageSize the maximum number of instance IDs to return per page + * @return this query object + */ + public ListInstanceIdsQuery setPageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } + + /** + * Sets the pagination cursor used to continue listing from a previous page. + *

+ * This should be the {@link ListInstanceIdsResult#getContinuationToken()} value from the previous page. + * + * @param continuationToken the cursor from the previous page, or {@code null} to start from the beginning + * @return this query object + */ + public ListInstanceIdsQuery setContinuationToken(@Nullable String continuationToken) { + this.continuationToken = continuationToken; + return this; + } + + List getRuntimeStatusList() { + return this.runtimeStatusList; + } + + @Nullable + Instant getCompletedTimeFrom() { + return this.completedTimeFrom; + } + + @Nullable + Instant getCompletedTimeTo() { + return this.completedTimeTo; + } + + int getPageSize() { + return this.pageSize; + } + + @Nullable + String getContinuationToken() { + return this.continuationToken; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsResult.java b/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsResult.java new file mode 100644 index 0000000..b5afba9 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsResult.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; + +/** + * Result of a {@link DurableTaskClient#listInstanceIds(ListInstanceIdsQuery)} call. + *

+ * Contains a page of terminal orchestration instance IDs and a pagination cursor for fetching the next page. + */ +public final class ListInstanceIdsResult { + private final List instanceIds; + private final String continuationToken; + + ListInstanceIdsResult(List instanceIds, @Nullable String continuationToken) { + this.instanceIds = Collections.unmodifiableList(instanceIds); + this.continuationToken = continuationToken; + } + + /** + * Gets the page of terminal orchestration instance IDs that matched the query. + * + * @return an unmodifiable list of instance IDs that matched the query + */ + public List getInstanceIds() { + return this.instanceIds; + } + + /** + * Gets the pagination cursor to pass to the next + * {@link DurableTaskClient#listInstanceIds(ListInstanceIdsQuery)} call via + * {@link ListInstanceIdsQuery#setContinuationToken(String)}, or {@code null} when there are no more pages. + * + * @return the cursor for the next page, or {@code null} if no more pages are available + */ + @Nullable + public String getContinuationToken() { + return this.continuationToken; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/TypedEntityMetadata.java b/client/src/main/java/com/microsoft/durabletask/TypedEntityMetadata.java index ff059a9..f47f2de 100644 --- a/client/src/main/java/com/microsoft/durabletask/TypedEntityMetadata.java +++ b/client/src/main/java/com/microsoft/durabletask/TypedEntityMetadata.java @@ -14,7 +14,7 @@ * and provides a typed {@code State} property. In Java, the state is eagerly deserialized and accessible * via {@link #getState()}. * - *

Example:

+ *

Example:

*
{@code
  * TypedEntityMetadata metadata = client.getEntities()
  *     .getEntityMetadata(entityId, Integer.class);
diff --git a/client/src/main/java/com/microsoft/durabletask/TypedEntityQueryPageable.java b/client/src/main/java/com/microsoft/durabletask/TypedEntityQueryPageable.java
index a8a7c1a..98297b5 100644
--- a/client/src/main/java/com/microsoft/durabletask/TypedEntityQueryPageable.java
+++ b/client/src/main/java/com/microsoft/durabletask/TypedEntityQueryPageable.java
@@ -14,7 +14,7 @@
  * 

* Use {@link DurableEntityClient#getAllEntities(EntityQuery, Class)} to obtain an instance. * - *

Example:

+ *

Example:

*
{@code
  * EntityQuery query = new EntityQuery().setInstanceIdStartsWith("counter");
  * for (TypedEntityMetadata entity : client.getEntities().getAllEntities(query, Integer.class)) {
diff --git a/client/src/main/java/com/microsoft/durabletask/history/ContinueAsNewEvent.java b/client/src/main/java/com/microsoft/durabletask/history/ContinueAsNewEvent.java
new file mode 100644
index 0000000..6b80aeb
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/ContinueAsNewEvent.java
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when an orchestration restarts itself via continue-as-new.
+ */
+public final class ContinueAsNewEvent extends HistoryEvent {
+    private final String input;
+
+    /**
+     * Creates a new {@code ContinueAsNewEvent}.
+     *
+     * @param eventId   the event sequence ID
+     * @param timestamp the event timestamp
+     * @param input     the serialized input for the new generation, or {@code null}
+     */
+    public ContinueAsNewEvent(int eventId, Instant timestamp, @Nullable String input) {
+        super(eventId, timestamp);
+        this.input = input;
+    }
+
+    /** @return the serialized input for the new generation, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EntityLockGrantedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EntityLockGrantedEvent.java
new file mode 100644
index 0000000..d542f61
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EntityLockGrantedEvent.java
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import java.time.Instant;
+
+/**
+ * History event recorded when a requested entity lock is granted.
+ */
+public final class EntityLockGrantedEvent extends HistoryEvent {
+    private final String criticalSectionId;
+
+    /**
+     * Creates a new {@code EntityLockGrantedEvent}.
+     *
+     * @param eventId           the event sequence ID
+     * @param timestamp         the event timestamp
+     * @param criticalSectionId the ID of the critical section whose lock was granted
+     */
+    public EntityLockGrantedEvent(int eventId, Instant timestamp, String criticalSectionId) {
+        super(eventId, timestamp);
+        this.criticalSectionId = criticalSectionId;
+    }
+
+    /** @return the ID of the critical section whose lock was granted. */
+    public String getCriticalSectionId() {
+        return this.criticalSectionId;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EntityLockRequestedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EntityLockRequestedEvent.java
new file mode 100644
index 0000000..8a062d9
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EntityLockRequestedEvent.java
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * History event recorded when an orchestration requests a lock over one or more durable entities.
+ */
+public final class EntityLockRequestedEvent extends HistoryEvent {
+    private final String criticalSectionId;
+    private final List lockSet;
+    private final int position;
+    private final String parentInstanceId;
+
+    /**
+     * Creates a new {@code EntityLockRequestedEvent}.
+     *
+     * @param eventId           the event sequence ID
+     * @param timestamp         the event timestamp
+     * @param criticalSectionId the ID of the critical section associated with the lock request
+     * @param lockSet           the ordered set of entity instance IDs being locked, or {@code null}
+     * @param position          the position of this entity within the lock set
+     * @param parentInstanceId  the requesting instance ID, or {@code null}
+     */
+    public EntityLockRequestedEvent(
+            int eventId,
+            Instant timestamp,
+            String criticalSectionId,
+            @Nullable List lockSet,
+            int position,
+            @Nullable String parentInstanceId) {
+        super(eventId, timestamp);
+        this.criticalSectionId = criticalSectionId;
+        this.lockSet = lockSet != null
+                ? Collections.unmodifiableList(new ArrayList<>(lockSet)) : Collections.emptyList();
+        this.position = position;
+        this.parentInstanceId = parentInstanceId;
+    }
+
+    /** @return the ID of the critical section associated with the lock request. */
+    public String getCriticalSectionId() {
+        return this.criticalSectionId;
+    }
+
+    /** @return the ordered set of entity instance IDs being locked (never {@code null}). */
+    public List getLockSet() {
+        return this.lockSet;
+    }
+
+    /** @return the position of this entity within the lock set. */
+    public int getPosition() {
+        return this.position;
+    }
+
+    /** @return the requesting instance ID, or {@code null} if not set. */
+    @Nullable
+    public String getParentInstanceId() {
+        return this.parentInstanceId;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EntityOperationCalledEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EntityOperationCalledEvent.java
new file mode 100644
index 0000000..f85a776
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EntityOperationCalledEvent.java
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when a two-way (call) operation is sent to a durable entity.
+ */
+public final class EntityOperationCalledEvent extends HistoryEvent {
+    private final String requestId;
+    private final String operation;
+    private final Instant scheduledTime;
+    private final String input;
+    private final String parentInstanceId;
+    private final String parentExecutionId;
+    private final String targetInstanceId;
+
+    /**
+     * Creates a new {@code EntityOperationCalledEvent}.
+     *
+     * @param eventId           the event sequence ID
+     * @param timestamp         the event timestamp
+     * @param requestId         the unique request ID of the entity operation
+     * @param operation         the name of the entity operation
+     * @param scheduledTime     the scheduled delivery time, or {@code null}
+     * @param input             the serialized operation input, or {@code null}
+     * @param parentInstanceId  the calling instance ID, or {@code null}
+     * @param parentExecutionId the calling execution ID, or {@code null}
+     * @param targetInstanceId  the target entity instance ID, or {@code null}
+     */
+    public EntityOperationCalledEvent(
+            int eventId,
+            Instant timestamp,
+            String requestId,
+            String operation,
+            @Nullable Instant scheduledTime,
+            @Nullable String input,
+            @Nullable String parentInstanceId,
+            @Nullable String parentExecutionId,
+            @Nullable String targetInstanceId) {
+        super(eventId, timestamp);
+        this.requestId = requestId;
+        this.operation = operation;
+        this.scheduledTime = scheduledTime;
+        this.input = input;
+        this.parentInstanceId = parentInstanceId;
+        this.parentExecutionId = parentExecutionId;
+        this.targetInstanceId = targetInstanceId;
+    }
+
+    /** @return the unique request ID of the entity operation. */
+    public String getRequestId() {
+        return this.requestId;
+    }
+
+    /** @return the name of the entity operation. */
+    public String getOperation() {
+        return this.operation;
+    }
+
+    /** @return the scheduled delivery time, or {@code null} if delivered immediately. */
+    @Nullable
+    public Instant getScheduledTime() {
+        return this.scheduledTime;
+    }
+
+    /** @return the serialized operation input, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+
+    /** @return the calling instance ID, or {@code null} if not set. */
+    @Nullable
+    public String getParentInstanceId() {
+        return this.parentInstanceId;
+    }
+
+    /** @return the calling execution ID, or {@code null} if not set. */
+    @Nullable
+    public String getParentExecutionId() {
+        return this.parentExecutionId;
+    }
+
+    /** @return the target entity instance ID, or {@code null} if not set. */
+    @Nullable
+    public String getTargetInstanceId() {
+        return this.targetInstanceId;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EntityOperationCompletedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EntityOperationCompletedEvent.java
new file mode 100644
index 0000000..d8c188e
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EntityOperationCompletedEvent.java
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when a durable entity operation completes successfully.
+ */
+public final class EntityOperationCompletedEvent extends HistoryEvent {
+    private final String requestId;
+    private final String output;
+
+    /**
+     * Creates a new {@code EntityOperationCompletedEvent}.
+     *
+     * @param eventId   the event sequence ID
+     * @param timestamp the event timestamp
+     * @param requestId the unique request ID of the entity operation
+     * @param output    the serialized operation output, or {@code null}
+     */
+    public EntityOperationCompletedEvent(
+            int eventId, Instant timestamp, String requestId, @Nullable String output) {
+        super(eventId, timestamp);
+        this.requestId = requestId;
+        this.output = output;
+    }
+
+    /** @return the unique request ID of the entity operation. */
+    public String getRequestId() {
+        return this.requestId;
+    }
+
+    /** @return the serialized operation output, or {@code null} if none. */
+    @Nullable
+    public String getOutput() {
+        return this.output;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EntityOperationFailedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EntityOperationFailedEvent.java
new file mode 100644
index 0000000..a4d2cf3
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EntityOperationFailedEvent.java
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import com.microsoft.durabletask.FailureDetails;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when a durable entity operation fails.
+ */
+public final class EntityOperationFailedEvent extends HistoryEvent {
+    private final String requestId;
+    private final FailureDetails failureDetails;
+
+    /**
+     * Creates a new {@code EntityOperationFailedEvent}.
+     *
+     * @param eventId        the event sequence ID
+     * @param timestamp      the event timestamp
+     * @param requestId      the unique request ID of the entity operation
+     * @param failureDetails the failure details, or {@code null}
+     */
+    public EntityOperationFailedEvent(
+            int eventId, Instant timestamp, String requestId, @Nullable FailureDetails failureDetails) {
+        super(eventId, timestamp);
+        this.requestId = requestId;
+        this.failureDetails = failureDetails;
+    }
+
+    /** @return the unique request ID of the entity operation. */
+    public String getRequestId() {
+        return this.requestId;
+    }
+
+    /** @return the failure details, or {@code null} if not available. */
+    @Nullable
+    public FailureDetails getFailureDetails() {
+        return this.failureDetails;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EntityOperationSignaledEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EntityOperationSignaledEvent.java
new file mode 100644
index 0000000..f299d59
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EntityOperationSignaledEvent.java
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when a one-way (signal) operation is sent to a durable entity.
+ */
+public final class EntityOperationSignaledEvent extends HistoryEvent {
+    private final String requestId;
+    private final String operation;
+    private final Instant scheduledTime;
+    private final String input;
+    private final String targetInstanceId;
+
+    /**
+     * Creates a new {@code EntityOperationSignaledEvent}.
+     *
+     * @param eventId          the event sequence ID
+     * @param timestamp        the event timestamp
+     * @param requestId        the unique request ID of the entity operation
+     * @param operation        the name of the entity operation
+     * @param scheduledTime    the scheduled delivery time, or {@code null}
+     * @param input            the serialized operation input, or {@code null}
+     * @param targetInstanceId the target entity instance ID, or {@code null}
+     */
+    public EntityOperationSignaledEvent(
+            int eventId,
+            Instant timestamp,
+            String requestId,
+            String operation,
+            @Nullable Instant scheduledTime,
+            @Nullable String input,
+            @Nullable String targetInstanceId) {
+        super(eventId, timestamp);
+        this.requestId = requestId;
+        this.operation = operation;
+        this.scheduledTime = scheduledTime;
+        this.input = input;
+        this.targetInstanceId = targetInstanceId;
+    }
+
+    /** @return the unique request ID of the entity operation. */
+    public String getRequestId() {
+        return this.requestId;
+    }
+
+    /** @return the name of the entity operation. */
+    public String getOperation() {
+        return this.operation;
+    }
+
+    /** @return the scheduled delivery time, or {@code null} if delivered immediately. */
+    @Nullable
+    public Instant getScheduledTime() {
+        return this.scheduledTime;
+    }
+
+    /** @return the serialized operation input, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+
+    /** @return the target entity instance ID, or {@code null} if not set. */
+    @Nullable
+    public String getTargetInstanceId() {
+        return this.targetInstanceId;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EntityUnlockSentEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EntityUnlockSentEvent.java
new file mode 100644
index 0000000..7c12a92
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EntityUnlockSentEvent.java
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when an entity lock is released (unlock sent).
+ */
+public final class EntityUnlockSentEvent extends HistoryEvent {
+    private final String criticalSectionId;
+    private final String parentInstanceId;
+    private final String targetInstanceId;
+
+    /**
+     * Creates a new {@code EntityUnlockSentEvent}.
+     *
+     * @param eventId           the event sequence ID
+     * @param timestamp         the event timestamp
+     * @param criticalSectionId the ID of the critical section being released
+     * @param parentInstanceId  the releasing instance ID, or {@code null}
+     * @param targetInstanceId  the target entity instance ID, or {@code null}
+     */
+    public EntityUnlockSentEvent(
+            int eventId,
+            Instant timestamp,
+            String criticalSectionId,
+            @Nullable String parentInstanceId,
+            @Nullable String targetInstanceId) {
+        super(eventId, timestamp);
+        this.criticalSectionId = criticalSectionId;
+        this.parentInstanceId = parentInstanceId;
+        this.targetInstanceId = targetInstanceId;
+    }
+
+    /** @return the ID of the critical section being released. */
+    public String getCriticalSectionId() {
+        return this.criticalSectionId;
+    }
+
+    /** @return the releasing instance ID, or {@code null} if not set. */
+    @Nullable
+    public String getParentInstanceId() {
+        return this.parentInstanceId;
+    }
+
+    /** @return the target entity instance ID, or {@code null} if not set. */
+    @Nullable
+    public String getTargetInstanceId() {
+        return this.targetInstanceId;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EventRaisedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EventRaisedEvent.java
new file mode 100644
index 0000000..a8bde47
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EventRaisedEvent.java
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when an external event is delivered to an orchestration instance.
+ */
+public final class EventRaisedEvent extends HistoryEvent {
+    private final String name;
+    private final String input;
+
+    /**
+     * Creates a new {@code EventRaisedEvent}.
+     *
+     * @param eventId   the event sequence ID
+     * @param timestamp the event timestamp
+     * @param name      the name of the event
+     * @param input     the serialized event payload, or {@code null}
+     */
+    public EventRaisedEvent(int eventId, Instant timestamp, String name, @Nullable String input) {
+        super(eventId, timestamp);
+        this.name = name;
+        this.input = input;
+    }
+
+    /** @return the name of the event. */
+    public String getName() {
+        return this.name;
+    }
+
+    /** @return the serialized event payload, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/EventSentEvent.java b/client/src/main/java/com/microsoft/durabletask/history/EventSentEvent.java
new file mode 100644
index 0000000..c4accf8
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/EventSentEvent.java
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when an orchestration sends an external event to another instance.
+ */
+public final class EventSentEvent extends HistoryEvent {
+    private final String instanceId;
+    private final String name;
+    private final String input;
+
+    /**
+     * Creates a new {@code EventSentEvent}.
+     *
+     * @param eventId    the event sequence ID
+     * @param timestamp  the event timestamp
+     * @param instanceId the target instance ID the event was sent to
+     * @param name       the name of the event
+     * @param input      the serialized event payload, or {@code null}
+     */
+    public EventSentEvent(int eventId, Instant timestamp, String instanceId, String name, @Nullable String input) {
+        super(eventId, timestamp);
+        this.instanceId = instanceId;
+        this.name = name;
+        this.input = input;
+    }
+
+    /** @return the target instance ID the event was sent to. */
+    public String getInstanceId() {
+        return this.instanceId;
+    }
+
+    /** @return the name of the event. */
+    public String getName() {
+        return this.name;
+    }
+
+    /** @return the serialized event payload, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/ExecutionCompletedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/ExecutionCompletedEvent.java
new file mode 100644
index 0000000..f0cdae0
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/ExecutionCompletedEvent.java
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import com.microsoft.durabletask.FailureDetails;
+import com.microsoft.durabletask.OrchestrationRuntimeStatus;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when an orchestration instance reaches a terminal state.
+ */
+public final class ExecutionCompletedEvent extends HistoryEvent {
+    private final OrchestrationRuntimeStatus orchestrationStatus;
+    private final String result;
+    private final FailureDetails failureDetails;
+
+    /**
+     * Creates a new {@code ExecutionCompletedEvent}.
+     *
+     * @param eventId             the event sequence ID
+     * @param timestamp           the event timestamp
+     * @param orchestrationStatus the terminal runtime status
+     * @param result              the serialized orchestration output, or {@code null}
+     * @param failureDetails      the failure details if the orchestration failed, or {@code null}
+     */
+    public ExecutionCompletedEvent(
+            int eventId,
+            Instant timestamp,
+            OrchestrationRuntimeStatus orchestrationStatus,
+            @Nullable String result,
+            @Nullable FailureDetails failureDetails) {
+        super(eventId, timestamp);
+        this.orchestrationStatus = orchestrationStatus;
+        this.result = result;
+        this.failureDetails = failureDetails;
+    }
+
+    /** @return the terminal runtime status of the orchestration. */
+    public OrchestrationRuntimeStatus getOrchestrationStatus() {
+        return this.orchestrationStatus;
+    }
+
+    /** @return the serialized orchestration output, or {@code null} if none. */
+    @Nullable
+    public String getResult() {
+        return this.result;
+    }
+
+    /** @return the failure details if the orchestration failed, otherwise {@code null}. */
+    @Nullable
+    public FailureDetails getFailureDetails() {
+        return this.failureDetails;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/ExecutionResumedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/ExecutionResumedEvent.java
new file mode 100644
index 0000000..5dbf2eb
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/ExecutionResumedEvent.java
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when a suspended orchestration instance is resumed.
+ */
+public final class ExecutionResumedEvent extends HistoryEvent {
+    private final String input;
+
+    /**
+     * Creates a new {@code ExecutionResumedEvent}.
+     *
+     * @param eventId   the event sequence ID
+     * @param timestamp the event timestamp
+     * @param input     the serialized resume reason, or {@code null}
+     */
+    public ExecutionResumedEvent(int eventId, Instant timestamp, @Nullable String input) {
+        super(eventId, timestamp);
+        this.input = input;
+    }
+
+    /** @return the serialized resume reason, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/ExecutionRewoundEvent.java b/client/src/main/java/com/microsoft/durabletask/history/ExecutionRewoundEvent.java
new file mode 100644
index 0000000..afe49f0
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/ExecutionRewoundEvent.java
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * History event recorded when an orchestration instance is rewound to a previous good state.
+ */
+public final class ExecutionRewoundEvent extends HistoryEvent {
+    private final String reason;
+    private final String parentExecutionId;
+    private final String instanceId;
+    private final TraceContext parentTraceContext;
+    private final String name;
+    private final String version;
+    private final String input;
+    private final ParentInstanceInfo parentInstance;
+    private final Map tags;
+
+    /**
+     * Creates a new {@code ExecutionRewoundEvent}.
+     *
+     * @param eventId            the event sequence ID
+     * @param timestamp          the event timestamp
+     * @param reason             the reason for the rewind, or {@code null}
+     * @param parentExecutionId  the parent execution ID (sub-orchestration rewind only), or {@code null}
+     * @param instanceId         the instance ID (sub-orchestration rewind only), or {@code null}
+     * @param parentTraceContext the parent distributed-tracing context, or {@code null}
+     * @param name               the orchestrator name, or {@code null}
+     * @param version            the orchestrator version, or {@code null}
+     * @param input              the serialized input, or {@code null}
+     * @param parentInstance     the parent orchestration info, or {@code null}
+     * @param tags               the orchestration tags, or {@code null}
+     */
+    public ExecutionRewoundEvent(
+            int eventId,
+            Instant timestamp,
+            @Nullable String reason,
+            @Nullable String parentExecutionId,
+            @Nullable String instanceId,
+            @Nullable TraceContext parentTraceContext,
+            @Nullable String name,
+            @Nullable String version,
+            @Nullable String input,
+            @Nullable ParentInstanceInfo parentInstance,
+            @Nullable Map tags) {
+        super(eventId, timestamp);
+        this.reason = reason;
+        this.parentExecutionId = parentExecutionId;
+        this.instanceId = instanceId;
+        this.parentTraceContext = parentTraceContext;
+        this.name = name;
+        this.version = version;
+        this.input = input;
+        this.parentInstance = parentInstance;
+        this.tags = tags != null ? Collections.unmodifiableMap(new HashMap<>(tags)) : Collections.emptyMap();
+    }
+
+    /** @return the reason for the rewind, or {@code null} if none. */
+    @Nullable
+    public String getReason() {
+        return this.reason;
+    }
+
+    /** @return the parent execution ID (sub-orchestration rewind only), or {@code null}. */
+    @Nullable
+    public String getParentExecutionId() {
+        return this.parentExecutionId;
+    }
+
+    /** @return the instance ID (sub-orchestration rewind only), or {@code null}. */
+    @Nullable
+    public String getInstanceId() {
+        return this.instanceId;
+    }
+
+    /** @return the parent distributed-tracing context, or {@code null} if not set. */
+    @Nullable
+    public TraceContext getParentTraceContext() {
+        return this.parentTraceContext;
+    }
+
+    /** @return the orchestrator name, or {@code null} if not set. */
+    @Nullable
+    public String getName() {
+        return this.name;
+    }
+
+    /** @return the orchestrator version, or {@code null} if not set. */
+    @Nullable
+    public String getVersion() {
+        return this.version;
+    }
+
+    /** @return the serialized input, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+
+    /** @return the parent orchestration info, or {@code null} if not set. */
+    @Nullable
+    public ParentInstanceInfo getParentInstance() {
+        return this.parentInstance;
+    }
+
+    /** @return the orchestration tags (never {@code null}; empty when none). */
+    public Map getTags() {
+        return this.tags;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/ExecutionStartedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/ExecutionStartedEvent.java
new file mode 100644
index 0000000..a6173e3
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/ExecutionStartedEvent.java
@@ -0,0 +1,115 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * History event recorded when an orchestration instance begins execution.
+ */
+public final class ExecutionStartedEvent extends HistoryEvent {
+    private final String name;
+    private final String version;
+    private final String input;
+    private final OrchestrationInstance orchestrationInstance;
+    private final ParentInstanceInfo parentInstance;
+    private final Instant scheduledStartTimestamp;
+    private final TraceContext parentTraceContext;
+    private final String orchestrationSpanId;
+    private final Map tags;
+
+    /**
+     * Creates a new {@code ExecutionStartedEvent}.
+     *
+     * @param eventId                 the event sequence ID
+     * @param timestamp               the event timestamp
+     * @param name                    the orchestrator name
+     * @param version                 the orchestrator version, or {@code null}
+     * @param input                   the serialized orchestration input, or {@code null}
+     * @param orchestrationInstance   the orchestration instance, or {@code null}
+     * @param parentInstance          the parent orchestration info, or {@code null}
+     * @param scheduledStartTimestamp the scheduled start time for delayed starts, or {@code null}
+     * @param parentTraceContext      the parent distributed-tracing context, or {@code null}
+     * @param orchestrationSpanId     the orchestration's tracing span ID, or {@code null}
+     * @param tags                    the orchestration tags, or {@code null}
+     */
+    public ExecutionStartedEvent(
+            int eventId,
+            Instant timestamp,
+            String name,
+            @Nullable String version,
+            @Nullable String input,
+            @Nullable OrchestrationInstance orchestrationInstance,
+            @Nullable ParentInstanceInfo parentInstance,
+            @Nullable Instant scheduledStartTimestamp,
+            @Nullable TraceContext parentTraceContext,
+            @Nullable String orchestrationSpanId,
+            @Nullable Map tags) {
+        super(eventId, timestamp);
+        this.name = name;
+        this.version = version;
+        this.input = input;
+        this.orchestrationInstance = orchestrationInstance;
+        this.parentInstance = parentInstance;
+        this.scheduledStartTimestamp = scheduledStartTimestamp;
+        this.parentTraceContext = parentTraceContext;
+        this.orchestrationSpanId = orchestrationSpanId;
+        this.tags = tags != null ? Collections.unmodifiableMap(new HashMap<>(tags)) : Collections.emptyMap();
+    }
+
+    /** @return the name of the orchestrator. */
+    public String getName() {
+        return this.name;
+    }
+
+    /** @return the orchestrator version, or {@code null} if not set. */
+    @Nullable
+    public String getVersion() {
+        return this.version;
+    }
+
+    /** @return the serialized orchestration input, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+
+    /** @return the orchestration instance, or {@code null} if not set. */
+    @Nullable
+    public OrchestrationInstance getOrchestrationInstance() {
+        return this.orchestrationInstance;
+    }
+
+    /** @return the parent orchestration info if this is a sub-orchestration, otherwise {@code null}. */
+    @Nullable
+    public ParentInstanceInfo getParentInstance() {
+        return this.parentInstance;
+    }
+
+    /** @return the scheduled start time for delayed starts, or {@code null} if started immediately. */
+    @Nullable
+    public Instant getScheduledStartTimestamp() {
+        return this.scheduledStartTimestamp;
+    }
+
+    /** @return the distributed-tracing context of the parent, or {@code null} if not set. */
+    @Nullable
+    public TraceContext getParentTraceContext() {
+        return this.parentTraceContext;
+    }
+
+    /** @return the orchestration's tracing span ID, or {@code null} if not set. */
+    @Nullable
+    public String getOrchestrationSpanId() {
+        return this.orchestrationSpanId;
+    }
+
+    /** @return the orchestration tags (never {@code null}; empty when none). */
+    public Map getTags() {
+        return this.tags;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/ExecutionSuspendedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/ExecutionSuspendedEvent.java
new file mode 100644
index 0000000..4115f9d
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/ExecutionSuspendedEvent.java
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when an orchestration instance is suspended.
+ */
+public final class ExecutionSuspendedEvent extends HistoryEvent {
+    private final String input;
+
+    /**
+     * Creates a new {@code ExecutionSuspendedEvent}.
+     *
+     * @param eventId   the event sequence ID
+     * @param timestamp the event timestamp
+     * @param input     the serialized suspension reason, or {@code null}
+     */
+    public ExecutionSuspendedEvent(int eventId, Instant timestamp, @Nullable String input) {
+        super(eventId, timestamp);
+        this.input = input;
+    }
+
+    /** @return the serialized suspension reason, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/ExecutionTerminatedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/ExecutionTerminatedEvent.java
new file mode 100644
index 0000000..2498518
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/ExecutionTerminatedEvent.java
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event recorded when an orchestration instance is terminated.
+ */
+public final class ExecutionTerminatedEvent extends HistoryEvent {
+    private final String input;
+    private final boolean recurse;
+
+    /**
+     * Creates a new {@code ExecutionTerminatedEvent}.
+     *
+     * @param eventId   the event sequence ID
+     * @param timestamp the event timestamp
+     * @param input     the serialized termination input/reason, or {@code null}
+     * @param recurse   whether termination recurses into sub-orchestrations
+     */
+    public ExecutionTerminatedEvent(int eventId, Instant timestamp, @Nullable String input, boolean recurse) {
+        super(eventId, timestamp);
+        this.input = input;
+        this.recurse = recurse;
+    }
+
+    /** @return the serialized termination input/reason, or {@code null} if none. */
+    @Nullable
+    public String getInput() {
+        return this.input;
+    }
+
+    /** @return whether termination recurses into sub-orchestrations. */
+    public boolean isRecurse() {
+        return this.recurse;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/GenericEvent.java b/client/src/main/java/com/microsoft/durabletask/history/GenericEvent.java
new file mode 100644
index 0000000..b23b4fa
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/GenericEvent.java
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import javax.annotation.Nullable;
+import java.time.Instant;
+
+/**
+ * History event that carries a generic, free-form data payload.
+ */
+public final class GenericEvent extends HistoryEvent {
+    private final String data;
+
+    /**
+     * Creates a new {@code GenericEvent}.
+     *
+     * @param eventId   the event sequence ID
+     * @param timestamp the event timestamp
+     * @param data      the serialized event data, or {@code null}
+     */
+    public GenericEvent(int eventId, Instant timestamp, @Nullable String data) {
+        super(eventId, timestamp);
+        this.data = data;
+    }
+
+    /** @return the serialized event data, or {@code null} if none. */
+    @Nullable
+    public String getData() {
+        return this.data;
+    }
+}
diff --git a/client/src/main/java/com/microsoft/durabletask/history/HistoryEvent.java b/client/src/main/java/com/microsoft/durabletask/history/HistoryEvent.java
new file mode 100644
index 0000000..59511cc
--- /dev/null
+++ b/client/src/main/java/com/microsoft/durabletask/history/HistoryEvent.java
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.microsoft.durabletask.history;
+
+import java.time.Instant;
+
+/**
+ * Base class for the events that make up an orchestration instance's execution history.
+ * 

+ * Instances are obtained from {@link com.microsoft.durabletask.DurableTaskClient#getOrchestrationHistory(String)}. Each + * concrete subclass (for example {@link ExecutionStartedEvent} or {@link TaskCompletedEvent}) exposes the data specific + * to that event type. Use {@code instanceof} to inspect the concrete event type. + */ +public abstract class HistoryEvent { + private final int eventId; + private final Instant timestamp; + + HistoryEvent(int eventId, Instant timestamp) { + this.eventId = eventId; + this.timestamp = timestamp; + } + + /** + * Gets the sequence ID of this history event, or {@code -1} if the event is not associated with a specific action. + * + * @return the event sequence ID + */ + public int getEventId() { + return this.eventId; + } + + /** + * Gets the UTC timestamp at which this history event was recorded. + * + * @return the event timestamp + */ + public Instant getTimestamp() { + return this.timestamp; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/HistoryStateEvent.java b/client/src/main/java/com/microsoft/durabletask/history/HistoryStateEvent.java new file mode 100644 index 0000000..4f233c1 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/HistoryStateEvent.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import javax.annotation.Nullable; +import java.time.Instant; + +/** + * History event that carries a snapshot of the orchestration's runtime state. + *

+ * This is an internal checkpoint marker. The full state snapshot is surfaced via {@link #getState()}, matching the + * sibling .NET SDK ({@code HistoryStateEvent.State}) and Python SDK ({@code HistoryStateEvent.orchestration_state}). + */ +public final class HistoryStateEvent extends HistoryEvent { + private final OrchestrationState state; + + /** + * Creates a new {@code HistoryStateEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param state the orchestration state snapshot, or {@code null} if not available + */ + public HistoryStateEvent(int eventId, Instant timestamp, @Nullable OrchestrationState state) { + super(eventId, timestamp); + this.state = state; + } + + /** @return the orchestration state snapshot, or {@code null} if not available. */ + @Nullable + public OrchestrationState getState() { + return this.state; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/OrchestrationInstance.java b/client/src/main/java/com/microsoft/durabletask/history/OrchestrationInstance.java new file mode 100644 index 0000000..c55a421 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/OrchestrationInstance.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import javax.annotation.Nullable; + +/** + * Identifies an orchestration instance and, optionally, a specific execution (generation) of it. + */ +public final class OrchestrationInstance { + private final String instanceId; + private final String executionId; + + /** + * Creates a new {@code OrchestrationInstance}. + * + * @param instanceId the orchestration instance ID + * @param executionId the execution (generation) ID, or {@code null} + */ + public OrchestrationInstance(String instanceId, @Nullable String executionId) { + this.instanceId = instanceId; + this.executionId = executionId; + } + + /** @return the unique ID of the orchestration instance. */ + public String getInstanceId() { + return this.instanceId; + } + + /** @return the execution (generation) ID, or {@code null} if not set. */ + @Nullable + public String getExecutionId() { + return this.executionId; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java b/client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java new file mode 100644 index 0000000..40fcd78 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import com.microsoft.durabletask.FailureDetails; +import com.microsoft.durabletask.OrchestrationRuntimeStatus; + +import javax.annotation.Nullable; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * A snapshot of an orchestration instance's runtime state, as carried by a {@link HistoryStateEvent}. + *

+ * This mirrors the {@code OrchestrationState} surfaced by the sibling .NET and Python SDKs (the .NET + * {@code HistoryStateEvent.State} property and the Python {@code HistoryStateEvent.orchestration_state} value), so that + * archival/export consumers can capture the full state checkpoint rather than just the instance ID. + */ +public final class OrchestrationState { + private final String instanceId; + private final String name; + private final String version; + private final OrchestrationRuntimeStatus runtimeStatus; + private final Instant scheduledStartTime; + private final Instant createdTime; + private final Instant lastUpdatedTime; + private final Instant completedTime; + private final String input; + private final String output; + private final String customStatus; + private final FailureDetails failureDetails; + private final String executionId; + private final String parentInstanceId; + private final Map tags; + + /** + * Creates a new {@code OrchestrationState}. + * + * @param instanceId the orchestration instance ID + * @param name the orchestration name + * @param version the orchestration version, or {@code null} + * @param runtimeStatus the runtime status of the orchestration + * @param scheduledStartTime the scheduled start time, or {@code null} + * @param createdTime the creation time, or {@code null} + * @param lastUpdatedTime the last-updated time, or {@code null} + * @param completedTime the completion time, or {@code null} + * @param input the serialized input, or {@code null} + * @param output the serialized output, or {@code null} + * @param customStatus the serialized custom status, or {@code null} + * @param failureDetails the failure details when the orchestration failed, or {@code null} + * @param executionId the execution ID, or {@code null} + * @param parentInstanceId the parent instance ID, or {@code null} + * @param tags the orchestration tags, or {@code null} + */ + public OrchestrationState( + String instanceId, + String name, + @Nullable String version, + OrchestrationRuntimeStatus runtimeStatus, + @Nullable Instant scheduledStartTime, + @Nullable Instant createdTime, + @Nullable Instant lastUpdatedTime, + @Nullable Instant completedTime, + @Nullable String input, + @Nullable String output, + @Nullable String customStatus, + @Nullable FailureDetails failureDetails, + @Nullable String executionId, + @Nullable String parentInstanceId, + @Nullable Map tags) { + this.instanceId = instanceId; + this.name = name; + this.version = version; + this.runtimeStatus = runtimeStatus; + this.scheduledStartTime = scheduledStartTime; + this.createdTime = createdTime; + this.lastUpdatedTime = lastUpdatedTime; + this.completedTime = completedTime; + this.input = input; + this.output = output; + this.customStatus = customStatus; + this.failureDetails = failureDetails; + this.executionId = executionId; + this.parentInstanceId = parentInstanceId; + this.tags = tags == null ? null : Collections.unmodifiableMap(new HashMap<>(tags)); + } + + /** @return the orchestration instance ID. */ + public String getInstanceId() { + return this.instanceId; + } + + /** @return the orchestration name. */ + public String getName() { + return this.name; + } + + /** @return the orchestration version, or {@code null} if not set. */ + @Nullable + public String getVersion() { + return this.version; + } + + /** @return the runtime status of the orchestration. */ + public OrchestrationRuntimeStatus getRuntimeStatus() { + return this.runtimeStatus; + } + + /** @return the scheduled start time, or {@code null} if not set. */ + @Nullable + public Instant getScheduledStartTime() { + return this.scheduledStartTime; + } + + /** @return the creation time, or {@code null} if not set. */ + @Nullable + public Instant getCreatedTime() { + return this.createdTime; + } + + /** @return the last-updated time, or {@code null} if not set. */ + @Nullable + public Instant getLastUpdatedTime() { + return this.lastUpdatedTime; + } + + /** @return the completion time, or {@code null} if not set. */ + @Nullable + public Instant getCompletedTime() { + return this.completedTime; + } + + /** @return the serialized orchestration input, or {@code null} if not set. */ + @Nullable + public String getInput() { + return this.input; + } + + /** @return the serialized orchestration output, or {@code null} if not set. */ + @Nullable + public String getOutput() { + return this.output; + } + + /** @return the serialized custom status, or {@code null} if not set. */ + @Nullable + public String getCustomStatus() { + return this.customStatus; + } + + /** @return the failure details when the orchestration failed, or {@code null} otherwise. */ + @Nullable + public FailureDetails getFailureDetails() { + return this.failureDetails; + } + + /** @return the execution ID, or {@code null} if not set. */ + @Nullable + public String getExecutionId() { + return this.executionId; + } + + /** @return the parent instance ID, or {@code null} if not set. */ + @Nullable + public String getParentInstanceId() { + return this.parentInstanceId; + } + + /** @return an unmodifiable view of the orchestration tags, or {@code null} if not set. */ + @Nullable + public Map getTags() { + return this.tags; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/OrchestratorCompletedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/OrchestratorCompletedEvent.java new file mode 100644 index 0000000..d99a1ef --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/OrchestratorCompletedEvent.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import java.time.Instant; + +/** + * History event recorded at the end of each orchestration replay/episode. Carries no payload. + */ +public final class OrchestratorCompletedEvent extends HistoryEvent { + /** + * Creates a new {@code OrchestratorCompletedEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + */ + public OrchestratorCompletedEvent(int eventId, Instant timestamp) { + super(eventId, timestamp); + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/OrchestratorStartedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/OrchestratorStartedEvent.java new file mode 100644 index 0000000..c470aca --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/OrchestratorStartedEvent.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import java.time.Instant; + +/** + * History event recorded at the start of each orchestration replay/episode. Carries no payload. + */ +public final class OrchestratorStartedEvent extends HistoryEvent { + /** + * Creates a new {@code OrchestratorStartedEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + */ + public OrchestratorStartedEvent(int eventId, Instant timestamp) { + super(eventId, timestamp); + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/ParentInstanceInfo.java b/client/src/main/java/com/microsoft/durabletask/history/ParentInstanceInfo.java new file mode 100644 index 0000000..ef90d65 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/ParentInstanceInfo.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import javax.annotation.Nullable; + +/** + * Information about the parent orchestration that created a sub-orchestration. + */ +public final class ParentInstanceInfo { + private final int taskScheduledId; + private final String name; + private final String version; + private final OrchestrationInstance orchestrationInstance; + + /** + * Creates a new {@code ParentInstanceInfo}. + * + * @param taskScheduledId the task scheduled ID of the sub-orchestration in the parent's history + * @param name the parent orchestrator name, or {@code null} + * @param version the parent orchestrator version, or {@code null} + * @param orchestrationInstance the parent orchestration instance, or {@code null} + */ + public ParentInstanceInfo( + int taskScheduledId, + @Nullable String name, + @Nullable String version, + @Nullable OrchestrationInstance orchestrationInstance) { + this.taskScheduledId = taskScheduledId; + this.name = name; + this.version = version; + this.orchestrationInstance = orchestrationInstance; + } + + /** @return the task scheduled ID of the sub-orchestration in the parent's history. */ + public int getTaskScheduledId() { + return this.taskScheduledId; + } + + /** @return the parent orchestrator name, or {@code null} if not set. */ + @Nullable + public String getName() { + return this.name; + } + + /** @return the parent orchestrator version, or {@code null} if not set. */ + @Nullable + public String getVersion() { + return this.version; + } + + /** @return the parent orchestration instance, or {@code null} if not set. */ + @Nullable + public OrchestrationInstance getOrchestrationInstance() { + return this.orchestrationInstance; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceCompletedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceCompletedEvent.java new file mode 100644 index 0000000..d55f0fe --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceCompletedEvent.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import javax.annotation.Nullable; +import java.time.Instant; + +/** + * History event recorded when a sub-orchestration instance completes successfully. + */ +public final class SubOrchestrationInstanceCompletedEvent extends HistoryEvent { + private final int taskScheduledId; + private final String result; + + /** + * Creates a new {@code SubOrchestrationInstanceCompletedEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param taskScheduledId the event ID of the corresponding {@link SubOrchestrationInstanceCreatedEvent} + * @param result the serialized sub-orchestration result, or {@code null} + */ + public SubOrchestrationInstanceCompletedEvent( + int eventId, Instant timestamp, int taskScheduledId, @Nullable String result) { + super(eventId, timestamp); + this.taskScheduledId = taskScheduledId; + this.result = result; + } + + /** @return the event ID of the corresponding {@link SubOrchestrationInstanceCreatedEvent}. */ + public int getTaskScheduledId() { + return this.taskScheduledId; + } + + /** @return the serialized sub-orchestration result, or {@code null} if none. */ + @Nullable + public String getResult() { + return this.result; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceCreatedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceCreatedEvent.java new file mode 100644 index 0000000..0eb401f --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceCreatedEvent.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import javax.annotation.Nullable; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * History event recorded when a sub-orchestration instance is created by an orchestration. + */ +public final class SubOrchestrationInstanceCreatedEvent extends HistoryEvent { + private final String instanceId; + private final String name; + private final String version; + private final String input; + private final TraceContext parentTraceContext; + private final Map tags; + + /** + * Creates a new {@code SubOrchestrationInstanceCreatedEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param instanceId the instance ID assigned to the sub-orchestration + * @param name the name of the sub-orchestrator + * @param version the sub-orchestrator version, or {@code null} + * @param input the serialized sub-orchestration input, or {@code null} + * @param parentTraceContext the parent distributed-tracing context, or {@code null} + * @param tags the sub-orchestration tags, or {@code null} + */ + public SubOrchestrationInstanceCreatedEvent( + int eventId, + Instant timestamp, + String instanceId, + String name, + @Nullable String version, + @Nullable String input, + @Nullable TraceContext parentTraceContext, + @Nullable Map tags) { + super(eventId, timestamp); + this.instanceId = instanceId; + this.name = name; + this.version = version; + this.input = input; + this.parentTraceContext = parentTraceContext; + this.tags = tags != null ? Collections.unmodifiableMap(new HashMap<>(tags)) : Collections.emptyMap(); + } + + /** @return the instance ID assigned to the sub-orchestration. */ + public String getInstanceId() { + return this.instanceId; + } + + /** @return the name of the sub-orchestrator. */ + public String getName() { + return this.name; + } + + /** @return the sub-orchestrator version, or {@code null} if not set. */ + @Nullable + public String getVersion() { + return this.version; + } + + /** @return the serialized sub-orchestration input, or {@code null} if none. */ + @Nullable + public String getInput() { + return this.input; + } + + /** @return the distributed-tracing context of the parent, or {@code null} if not set. */ + @Nullable + public TraceContext getParentTraceContext() { + return this.parentTraceContext; + } + + /** @return the sub-orchestration tags (never {@code null}; empty when none). */ + public Map getTags() { + return this.tags; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceFailedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceFailedEvent.java new file mode 100644 index 0000000..bb3a2b9 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceFailedEvent.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import com.microsoft.durabletask.FailureDetails; + +import javax.annotation.Nullable; +import java.time.Instant; + +/** + * History event recorded when a sub-orchestration instance fails. + */ +public final class SubOrchestrationInstanceFailedEvent extends HistoryEvent { + private final int taskScheduledId; + private final FailureDetails failureDetails; + + /** + * Creates a new {@code SubOrchestrationInstanceFailedEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param taskScheduledId the event ID of the corresponding {@link SubOrchestrationInstanceCreatedEvent} + * @param failureDetails the failure details, or {@code null} + */ + public SubOrchestrationInstanceFailedEvent( + int eventId, Instant timestamp, int taskScheduledId, @Nullable FailureDetails failureDetails) { + super(eventId, timestamp); + this.taskScheduledId = taskScheduledId; + this.failureDetails = failureDetails; + } + + /** @return the event ID of the corresponding {@link SubOrchestrationInstanceCreatedEvent}. */ + public int getTaskScheduledId() { + return this.taskScheduledId; + } + + /** @return the failure details, or {@code null} if not available. */ + @Nullable + public FailureDetails getFailureDetails() { + return this.failureDetails; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/TaskCompletedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/TaskCompletedEvent.java new file mode 100644 index 0000000..c2b6ab2 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/TaskCompletedEvent.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import javax.annotation.Nullable; +import java.time.Instant; + +/** + * History event recorded when a scheduled activity task completes successfully. + */ +public final class TaskCompletedEvent extends HistoryEvent { + private final int taskScheduledId; + private final String result; + + /** + * Creates a new {@code TaskCompletedEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param taskScheduledId the event ID of the corresponding {@link TaskScheduledEvent} + * @param result the serialized activity result, or {@code null} + */ + public TaskCompletedEvent(int eventId, Instant timestamp, int taskScheduledId, @Nullable String result) { + super(eventId, timestamp); + this.taskScheduledId = taskScheduledId; + this.result = result; + } + + /** @return the event ID of the corresponding {@link TaskScheduledEvent}. */ + public int getTaskScheduledId() { + return this.taskScheduledId; + } + + /** @return the serialized activity result, or {@code null} if none. */ + @Nullable + public String getResult() { + return this.result; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/TaskFailedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/TaskFailedEvent.java new file mode 100644 index 0000000..efa206e --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/TaskFailedEvent.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import com.microsoft.durabletask.FailureDetails; + +import javax.annotation.Nullable; +import java.time.Instant; + +/** + * History event recorded when a scheduled activity task fails. + */ +public final class TaskFailedEvent extends HistoryEvent { + private final int taskScheduledId; + private final FailureDetails failureDetails; + + /** + * Creates a new {@code TaskFailedEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param taskScheduledId the event ID of the corresponding {@link TaskScheduledEvent} + * @param failureDetails the failure details, or {@code null} + */ + public TaskFailedEvent( + int eventId, Instant timestamp, int taskScheduledId, @Nullable FailureDetails failureDetails) { + super(eventId, timestamp); + this.taskScheduledId = taskScheduledId; + this.failureDetails = failureDetails; + } + + /** @return the event ID of the corresponding {@link TaskScheduledEvent}. */ + public int getTaskScheduledId() { + return this.taskScheduledId; + } + + /** @return the failure details, or {@code null} if not available. */ + @Nullable + public FailureDetails getFailureDetails() { + return this.failureDetails; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/TaskScheduledEvent.java b/client/src/main/java/com/microsoft/durabletask/history/TaskScheduledEvent.java new file mode 100644 index 0000000..47ec0ba --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/TaskScheduledEvent.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import javax.annotation.Nullable; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * History event recorded when an activity task is scheduled by an orchestration. + */ +public final class TaskScheduledEvent extends HistoryEvent { + private final String name; + private final String version; + private final String input; + private final TraceContext parentTraceContext; + private final Map tags; + + /** + * Creates a new {@code TaskScheduledEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param name the name of the scheduled activity + * @param version the activity version, or {@code null} + * @param input the serialized activity input, or {@code null} + * @param parentTraceContext the parent distributed-tracing context, or {@code null} + * @param tags the activity tags, or {@code null} + */ + public TaskScheduledEvent( + int eventId, + Instant timestamp, + String name, + @Nullable String version, + @Nullable String input, + @Nullable TraceContext parentTraceContext, + @Nullable Map tags) { + super(eventId, timestamp); + this.name = name; + this.version = version; + this.input = input; + this.parentTraceContext = parentTraceContext; + this.tags = tags != null ? Collections.unmodifiableMap(new HashMap<>(tags)) : Collections.emptyMap(); + } + + /** @return the name of the scheduled activity. */ + public String getName() { + return this.name; + } + + /** @return the activity version, or {@code null} if not set. */ + @Nullable + public String getVersion() { + return this.version; + } + + /** @return the serialized activity input, or {@code null} if none. */ + @Nullable + public String getInput() { + return this.input; + } + + /** @return the distributed-tracing context of the parent, or {@code null} if not set. */ + @Nullable + public TraceContext getParentTraceContext() { + return this.parentTraceContext; + } + + /** @return the activity tags (never {@code null}; empty when none). */ + public Map getTags() { + return this.tags; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/TimerCreatedEvent.java b/client/src/main/java/com/microsoft/durabletask/history/TimerCreatedEvent.java new file mode 100644 index 0000000..4f98c0b --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/TimerCreatedEvent.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import java.time.Instant; + +/** + * History event recorded when a durable timer is created by an orchestration. + */ +public final class TimerCreatedEvent extends HistoryEvent { + private final Instant fireAt; + + /** + * Creates a new {@code TimerCreatedEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param fireAt the time at which the timer is scheduled to fire + */ + public TimerCreatedEvent(int eventId, Instant timestamp, Instant fireAt) { + super(eventId, timestamp); + this.fireAt = fireAt; + } + + /** @return the time at which the timer is scheduled to fire. */ + public Instant getFireAt() { + return this.fireAt; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/TimerFiredEvent.java b/client/src/main/java/com/microsoft/durabletask/history/TimerFiredEvent.java new file mode 100644 index 0000000..50fa923 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/TimerFiredEvent.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import java.time.Instant; + +/** + * History event recorded when a durable timer fires. + */ +public final class TimerFiredEvent extends HistoryEvent { + private final Instant fireAt; + private final int timerId; + + /** + * Creates a new {@code TimerFiredEvent}. + * + * @param eventId the event sequence ID + * @param timestamp the event timestamp + * @param fireAt the time at which the timer was scheduled to fire + * @param timerId the event ID of the corresponding {@link TimerCreatedEvent} + */ + public TimerFiredEvent(int eventId, Instant timestamp, Instant fireAt, int timerId) { + super(eventId, timestamp); + this.fireAt = fireAt; + this.timerId = timerId; + } + + /** @return the time at which the timer was scheduled to fire. */ + public Instant getFireAt() { + return this.fireAt; + } + + /** @return the event ID of the corresponding {@link TimerCreatedEvent}. */ + public int getTimerId() { + return this.timerId; + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/history/TraceContext.java b/client/src/main/java/com/microsoft/durabletask/history/TraceContext.java new file mode 100644 index 0000000..566c7dc --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/TraceContext.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask.history; + +import javax.annotation.Nullable; + +/** + * Distributed-tracing context (W3C Trace Context) associated with a history event. + */ +public final class TraceContext { + private final String traceParent; + private final String traceState; + + /** + * Creates a new {@code TraceContext}. + * + * @param traceParent the W3C {@code traceparent} value + * @param traceState the W3C {@code tracestate} value, or {@code null} + */ + public TraceContext(String traceParent, @Nullable String traceState) { + this.traceParent = traceParent; + this.traceState = traceState; + } + + /** @return the W3C {@code traceparent} value. */ + public String getTraceParent() { + return this.traceParent; + } + + /** @return the W3C {@code tracestate} value, or {@code null} if not set. */ + @Nullable + public String getTraceState() { + return this.traceState; + } +} diff --git a/client/src/test/java/com/microsoft/durabletask/HistoryEventConverterTest.java b/client/src/test/java/com/microsoft/durabletask/HistoryEventConverterTest.java new file mode 100644 index 0000000..1922a73 --- /dev/null +++ b/client/src/test/java/com/microsoft/durabletask/HistoryEventConverterTest.java @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.durabletask; + +import com.google.protobuf.StringValue; +import com.google.protobuf.Timestamp; +import com.microsoft.durabletask.history.ExecutionCompletedEvent; +import com.microsoft.durabletask.history.ExecutionStartedEvent; +import com.microsoft.durabletask.history.GenericEvent; +import com.microsoft.durabletask.history.HistoryEvent; +import com.microsoft.durabletask.history.HistoryStateEvent; +import com.microsoft.durabletask.history.OrchestrationState; +import com.microsoft.durabletask.history.SubOrchestrationInstanceCompletedEvent; +import com.microsoft.durabletask.history.TaskCompletedEvent; +import com.microsoft.durabletask.history.TaskFailedEvent; +import com.microsoft.durabletask.history.TimerFiredEvent; +import com.microsoft.durabletask.implementation.protobuf.OrchestratorService; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests for {@link HistoryEventConverter}, which maps protobuf history events to the public + * {@code com.microsoft.durabletask.history} domain model. + */ +public class HistoryEventConverterTest { + + private static final long EPOCH_SECONDS = 1_700_000_000L; + private static final Instant EXPECTED_TIMESTAMP = Instant.ofEpochSecond(EPOCH_SECONDS); + + private static OrchestratorService.HistoryEvent.Builder baseEvent(int eventId) { + return OrchestratorService.HistoryEvent.newBuilder() + .setEventId(eventId) + .setTimestamp(Timestamp.newBuilder().setSeconds(EPOCH_SECONDS).build()); + } + + @Test + void convertsExecutionStarted() { + OrchestratorService.HistoryEvent proto = baseEvent(1) + .setExecutionStarted(OrchestratorService.ExecutionStartedEvent.newBuilder() + .setName("MyOrchestration") + .setVersion(StringValue.of("2.0")) + .setInput(StringValue.of("\"hello\"")) + .setOrchestrationInstance(OrchestratorService.OrchestrationInstance.newBuilder() + .setInstanceId("inst-1") + .setExecutionId(StringValue.of("exec-1")))) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + ExecutionStartedEvent started = assertInstanceOf(ExecutionStartedEvent.class, event); + assertEquals(1, started.getEventId()); + assertEquals(EXPECTED_TIMESTAMP, started.getTimestamp()); + assertEquals("MyOrchestration", started.getName()); + assertEquals("2.0", started.getVersion()); + assertEquals("\"hello\"", started.getInput()); + assertNotNull(started.getOrchestrationInstance()); + assertEquals("inst-1", started.getOrchestrationInstance().getInstanceId()); + assertEquals("exec-1", started.getOrchestrationInstance().getExecutionId()); + } + + @Test + void convertsExecutionCompletedWithResult() { + OrchestratorService.HistoryEvent proto = baseEvent(2) + .setExecutionCompleted(OrchestratorService.ExecutionCompletedEvent.newBuilder() + .setOrchestrationStatus(OrchestratorService.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED) + .setResult(StringValue.of("\"done\""))) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + ExecutionCompletedEvent completed = assertInstanceOf(ExecutionCompletedEvent.class, event); + assertEquals(OrchestrationRuntimeStatus.COMPLETED, completed.getOrchestrationStatus()); + assertEquals("\"done\"", completed.getResult()); + assertNull(completed.getFailureDetails()); + } + + @Test + void convertsExecutionCompletedWithFailureDetails() { + OrchestratorService.HistoryEvent proto = baseEvent(3) + .setExecutionCompleted(OrchestratorService.ExecutionCompletedEvent.newBuilder() + .setOrchestrationStatus(OrchestratorService.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED) + .setFailureDetails(OrchestratorService.TaskFailureDetails.newBuilder() + .setErrorType("java.lang.RuntimeException") + .setErrorMessage("boom"))) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + ExecutionCompletedEvent completed = assertInstanceOf(ExecutionCompletedEvent.class, event); + assertEquals(OrchestrationRuntimeStatus.FAILED, completed.getOrchestrationStatus()); + assertNotNull(completed.getFailureDetails()); + assertEquals("java.lang.RuntimeException", completed.getFailureDetails().getErrorType()); + assertEquals("boom", completed.getFailureDetails().getErrorMessage()); + } + + @Test + void convertsTaskCompleted() { + OrchestratorService.HistoryEvent proto = baseEvent(4) + .setTaskCompleted(OrchestratorService.TaskCompletedEvent.newBuilder() + .setTaskScheduledId(7) + .setResult(StringValue.of("42"))) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + TaskCompletedEvent completed = assertInstanceOf(TaskCompletedEvent.class, event); + assertEquals(7, completed.getTaskScheduledId()); + assertEquals("42", completed.getResult()); + } + + @Test + void convertsTaskFailedWithFailureDetails() { + OrchestratorService.HistoryEvent proto = baseEvent(5) + .setTaskFailed(OrchestratorService.TaskFailedEvent.newBuilder() + .setTaskScheduledId(9) + .setFailureDetails(OrchestratorService.TaskFailureDetails.newBuilder() + .setErrorType("java.io.IOException") + .setErrorMessage("disk full"))) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + TaskFailedEvent failed = assertInstanceOf(TaskFailedEvent.class, event); + assertEquals(9, failed.getTaskScheduledId()); + assertNotNull(failed.getFailureDetails()); + assertEquals("java.io.IOException", failed.getFailureDetails().getErrorType()); + } + + @Test + void convertsSubOrchestrationCompleted() { + OrchestratorService.HistoryEvent proto = baseEvent(6) + .setSubOrchestrationInstanceCompleted( + OrchestratorService.SubOrchestrationInstanceCompletedEvent.newBuilder() + .setTaskScheduledId(3) + .setResult(StringValue.of("\"child-result\""))) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + SubOrchestrationInstanceCompletedEvent completed = + assertInstanceOf(SubOrchestrationInstanceCompletedEvent.class, event); + assertEquals(3, completed.getTaskScheduledId()); + assertEquals("\"child-result\"", completed.getResult()); + } + + @Test + void convertsTimerFired() { + Instant fireAt = Instant.ofEpochSecond(EPOCH_SECONDS + 60); + OrchestratorService.HistoryEvent proto = baseEvent(7) + .setTimerFired(OrchestratorService.TimerFiredEvent.newBuilder() + .setTimerId(11) + .setFireAt(Timestamp.newBuilder().setSeconds(fireAt.getEpochSecond()).build())) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + TimerFiredEvent fired = assertInstanceOf(TimerFiredEvent.class, event); + assertEquals(11, fired.getTimerId()); + assertEquals(fireAt, fired.getFireAt()); + } + + @Test + void convertsGenericEvent() { + OrchestratorService.HistoryEvent proto = baseEvent(8) + .setGenericEvent(OrchestratorService.GenericEvent.newBuilder() + .setData(StringValue.of("payload"))) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + GenericEvent generic = assertInstanceOf(GenericEvent.class, event); + assertEquals("payload", generic.getData()); + } + + @Test + void convertsHistoryStateExposesFullOrchestrationState() { + Instant created = Instant.ofEpochSecond(EPOCH_SECONDS - 100); + Instant lastUpdated = Instant.ofEpochSecond(EPOCH_SECONDS - 50); + OrchestratorService.HistoryEvent proto = baseEvent(9) + .setHistoryState(OrchestratorService.HistoryStateEvent.newBuilder() + .setOrchestrationState(OrchestratorService.OrchestrationState.newBuilder() + .setInstanceId("inst-9") + .setName("MyOrchestration") + .setVersion(StringValue.of("1.5")) + .setOrchestrationStatus( + OrchestratorService.OrchestrationStatus.ORCHESTRATION_STATUS_RUNNING) + .setCreatedTimestamp(Timestamp.newBuilder().setSeconds(created.getEpochSecond())) + .setLastUpdatedTimestamp( + Timestamp.newBuilder().setSeconds(lastUpdated.getEpochSecond())) + .setInput(StringValue.of("\"in\"")) + .setCustomStatus(StringValue.of("\"working\"")) + .putTags("env", "prod"))) + .build(); + + HistoryEvent event = HistoryEventConverter.fromProto(proto); + + HistoryStateEvent stateEvent = assertInstanceOf(HistoryStateEvent.class, event); + OrchestrationState state = stateEvent.getState(); + assertNotNull(state); + assertEquals("inst-9", state.getInstanceId()); + assertEquals("MyOrchestration", state.getName()); + assertEquals("1.5", state.getVersion()); + assertEquals(OrchestrationRuntimeStatus.RUNNING, state.getRuntimeStatus()); + assertEquals(created, state.getCreatedTime()); + assertEquals(lastUpdated, state.getLastUpdatedTime()); + assertEquals("\"in\"", state.getInput()); + assertEquals("\"working\"", state.getCustomStatus()); + assertNotNull(state.getTags()); + assertEquals("prod", state.getTags().get("env")); + } + + @Test + void throwsWhenEventTypeNotSet() { + OrchestratorService.HistoryEvent proto = baseEvent(10).build(); + + assertThrows(IllegalArgumentException.class, () -> HistoryEventConverter.fromProto(proto)); + } +} diff --git a/client/src/test/java/com/microsoft/durabletask/IntegrationTests.java b/client/src/test/java/com/microsoft/durabletask/IntegrationTests.java index 59db0b7..2c41b3f 100644 --- a/client/src/test/java/com/microsoft/durabletask/IntegrationTests.java +++ b/client/src/test/java/com/microsoft/durabletask/IntegrationTests.java @@ -14,6 +14,9 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import com.microsoft.durabletask.history.ExecutionStartedEvent; +import com.microsoft.durabletask.history.HistoryEvent; +import com.microsoft.durabletask.history.TaskCompletedEvent; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -1202,6 +1205,73 @@ void waitForInstanceCompletionThrowsException() { } } + @Test + void listInstanceIdsByCompletionWindow() throws TimeoutException { + final String orchestratorName = "ListInstanceIdsExport"; + final String plusOne = "PlusOne"; + + DurableTaskGrpcWorker worker = this.createWorkerBuilder() + .addOrchestrator(orchestratorName, ctx -> { + int value = ctx.getInput(int.class); + value = ctx.callActivity(plusOne, value, int.class).await(); + ctx.complete(value); + }) + .addActivity(plusOne, ctx -> ctx.getInput(int.class) + 1) + .buildAndStart(); + + DurableTaskClient client = this.createClientBuilder().build(); + try (worker; client) { + Instant from = Instant.now().minus(Duration.ofMinutes(1)); + + String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); + OrchestrationMetadata metadata = client.waitForInstanceCompletion(instanceId, defaultTimeout, false); + assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + + ListInstanceIdsResult result = client.listInstanceIds(new ListInstanceIdsQuery() + .setCompletedTimeFrom(from) + .setRuntimeStatusList(Collections.singletonList(OrchestrationRuntimeStatus.COMPLETED)) + .setPageSize(100)); + + assertNotNull(result); + assertNotNull(result.getInstanceIds()); + assertTrue(result.getInstanceIds().contains(instanceId), + "Expected listInstanceIds to include the completed instance " + instanceId); + } + } + + @Test + void getOrchestrationHistoryReturnsDomainEvents() throws TimeoutException { + final String orchestratorName = "StreamHistoryExport"; + final String plusOne = "PlusOne"; + + DurableTaskGrpcWorker worker = this.createWorkerBuilder() + .addOrchestrator(orchestratorName, ctx -> { + int value = ctx.getInput(int.class); + value = ctx.callActivity(plusOne, value, int.class).await(); + ctx.complete(value); + }) + .addActivity(plusOne, ctx -> ctx.getInput(int.class) + 1) + .buildAndStart(); + + DurableTaskClient client = this.createClientBuilder().build(); + try (worker; client) { + String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName, 0); + OrchestrationMetadata metadata = client.waitForInstanceCompletion(instanceId, defaultTimeout, false); + assertEquals(OrchestrationRuntimeStatus.COMPLETED, metadata.getRuntimeStatus()); + + List history = client.getOrchestrationHistory(instanceId); + + assertNotNull(history); + assertFalse(history.isEmpty(), "Expected a completed instance to have history events"); + // A completed orchestration's history begins with an ExecutionStartedEvent. + assertTrue(history.stream().anyMatch(e -> e instanceof ExecutionStartedEvent), + "Expected history to contain an ExecutionStartedEvent"); + // And it contains a successful activity completion. + assertTrue(history.stream().anyMatch(e -> e instanceof TaskCompletedEvent), + "Expected history to contain a TaskCompletedEvent"); + } + } + @Test void activityFanOutWithException() throws TimeoutException { final String orchestratorName = "ActivityFanOut"; diff --git a/client/src/test/java/com/microsoft/durabletask/ListInstanceIdsQueryTest.java b/client/src/test/java/com/microsoft/durabletask/ListInstanceIdsQueryTest.java new file mode 100644 index 0000000..bd942ba --- /dev/null +++ b/client/src/test/java/com/microsoft/durabletask/ListInstanceIdsQueryTest.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for {@link ListInstanceIdsQuery}. + */ +public class ListInstanceIdsQueryTest { + + @Test + void getRuntimeStatusList_default_isEmptyNonNull() { + ListInstanceIdsQuery query = new ListInstanceIdsQuery(); + assertNotNull(query.getRuntimeStatusList()); + assertTrue(query.getRuntimeStatusList().isEmpty()); + } + + @Test + void setRuntimeStatusList_null_normalizesToEmptyList() { + ListInstanceIdsQuery query = new ListInstanceIdsQuery().setRuntimeStatusList(null); + assertNotNull(query.getRuntimeStatusList()); + assertTrue(query.getRuntimeStatusList().isEmpty()); + } + + @Test + void setRuntimeStatusList_copiesInput_soExternalMutationDoesNotAffectQuery() { + List source = new ArrayList<>(); + source.add(OrchestrationRuntimeStatus.COMPLETED); + + ListInstanceIdsQuery query = new ListInstanceIdsQuery().setRuntimeStatusList(source); + source.add(OrchestrationRuntimeStatus.FAILED); + + assertEquals(Arrays.asList(OrchestrationRuntimeStatus.COMPLETED), query.getRuntimeStatusList()); + } +}