From 74d75524fabfaaa863e30466bb45a6d14336c7f9 Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Tue, 30 Jun 2026 13:41:43 -0700 Subject: [PATCH 1/5] initial commit --- .../durabletask/DurableTaskClient.java | 28 ++ .../durabletask/DurableTaskGrpcClient.java | 31 +++ .../durabletask/HistoryEventConverter.java | 246 ++++++++++++++++++ .../durabletask/ListInstanceIdsQuery.java | 114 ++++++++ .../durabletask/ListInstanceIdsResult.java | 43 +++ .../history/ContinueAsNewEvent.java | 31 +++ .../history/EntityLockGrantedEvent.java | 29 +++ .../history/EntityLockRequestedEvent.java | 65 +++++ .../history/EntityOperationCalledEvent.java | 92 +++++++ .../EntityOperationCompletedEvent.java | 40 +++ .../history/EntityOperationFailedEvent.java | 42 +++ .../history/EntityOperationSignaledEvent.java | 72 +++++ .../history/EntityUnlockSentEvent.java | 53 ++++ .../durabletask/history/EventRaisedEvent.java | 39 +++ .../durabletask/history/EventSentEvent.java | 47 ++++ .../history/ExecutionCompletedEvent.java | 56 ++++ .../history/ExecutionResumedEvent.java | 31 +++ .../history/ExecutionRewoundEvent.java | 116 +++++++++ .../history/ExecutionStartedEvent.java | 115 ++++++++ .../history/ExecutionSuspendedEvent.java | 31 +++ .../history/ExecutionTerminatedEvent.java | 39 +++ .../durabletask/history/GenericEvent.java | 31 +++ .../durabletask/history/HistoryEvent.java | 40 +++ .../history/HistoryStateEvent.java | 34 +++ .../history/OrchestrationInstance.java | 35 +++ .../history/OrchestrationState.java | 175 +++++++++++++ .../history/OrchestratorCompletedEvent.java | 20 ++ .../history/OrchestratorStartedEvent.java | 20 ++ .../history/ParentInstanceInfo.java | 57 ++++ ...ubOrchestrationInstanceCompletedEvent.java | 40 +++ .../SubOrchestrationInstanceCreatedEvent.java | 84 ++++++ .../SubOrchestrationInstanceFailedEvent.java | 42 +++ .../history/TaskCompletedEvent.java | 39 +++ .../durabletask/history/TaskFailedEvent.java | 42 +++ .../history/TaskScheduledEvent.java | 75 ++++++ .../history/TimerCreatedEvent.java | 29 +++ .../durabletask/history/TimerFiredEvent.java | 37 +++ .../durabletask/history/TraceContext.java | 35 +++ .../HistoryEventConverterTest.java | 226 ++++++++++++++++ .../durabletask/IntegrationTests.java | 70 +++++ 40 files changed, 2491 insertions(+) create mode 100644 client/src/main/java/com/microsoft/durabletask/HistoryEventConverter.java create mode 100644 client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java create mode 100644 client/src/main/java/com/microsoft/durabletask/ListInstanceIdsResult.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/ContinueAsNewEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EntityLockGrantedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EntityLockRequestedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EntityOperationCalledEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EntityOperationCompletedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EntityOperationFailedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EntityOperationSignaledEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EntityUnlockSentEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EventRaisedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/EventSentEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/ExecutionCompletedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/ExecutionResumedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/ExecutionRewoundEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/ExecutionStartedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/ExecutionSuspendedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/ExecutionTerminatedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/GenericEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/HistoryEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/HistoryStateEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/OrchestrationInstance.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/OrchestratorCompletedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/OrchestratorStartedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/ParentInstanceInfo.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceCompletedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceCreatedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/SubOrchestrationInstanceFailedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/TaskCompletedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/TaskFailedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/TaskScheduledEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/TimerCreatedEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/TimerFiredEvent.java create mode 100644 client/src/main/java/com/microsoft/durabletask/history/TraceContext.java create mode 100644 client/src/test/java/com/microsoft/durabletask/HistoryEventConverterTest.java 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..da4af20 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,35 @@ private OrchestrationStatusQueryResult toQueryResult(QueryInstancesResponse quer return new OrchestrationStatusQueryResult(metadataList, queryInstancesResponse.getContinuationToken().getValue()); } + @Override + public ListInstanceIdsResult listInstanceIds(ListInstanceIdsQuery 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) { + 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/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..70c70fc --- /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; + 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/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..1b9d628 --- /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#streamInstanceHistory(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..79d32c3 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java @@ -0,0 +1,175 @@ +// 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.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(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"; From 0cd9ef9e38f6ebb9542472adfd2fa76cbd695330 Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Wed, 1 Jul 2026 16:49:38 -0700 Subject: [PATCH 2/5] normalize null runtimeStatusList in ListInstanceIdsQuery --- .../durabletask/ListInstanceIdsQuery.java | 2 +- .../durabletask/ListInstanceIdsQueryTest.java | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 client/src/test/java/com/microsoft/durabletask/ListInstanceIdsQueryTest.java diff --git a/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java b/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java index 70c70fc..15a08a1 100644 --- a/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java +++ b/client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java @@ -36,7 +36,7 @@ public ListInstanceIdsQuery() { * @return this query object */ public ListInstanceIdsQuery setRuntimeStatusList(@Nullable List runtimeStatusList) { - this.runtimeStatusList = runtimeStatusList; + this.runtimeStatusList = runtimeStatusList != null ? new ArrayList<>(runtimeStatusList) : new ArrayList<>(); return this; } 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()); + } +} From 06cd428876704fcd76c282231aba9b0e2997ca8e Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Thu, 2 Jul 2026 11:35:58 -0700 Subject: [PATCH 3/5] fix javadoc broken link and heading levels --- .../java/com/microsoft/durabletask/EntityQueryPageable.java | 4 ++-- .../java/com/microsoft/durabletask/TypedEntityMetadata.java | 2 +- .../com/microsoft/durabletask/TypedEntityQueryPageable.java | 2 +- .../java/com/microsoft/durabletask/history/HistoryEvent.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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/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/HistoryEvent.java b/client/src/main/java/com/microsoft/durabletask/history/HistoryEvent.java
index 1b9d628..59511cc 100644
--- a/client/src/main/java/com/microsoft/durabletask/history/HistoryEvent.java
+++ b/client/src/main/java/com/microsoft/durabletask/history/HistoryEvent.java
@@ -7,7 +7,7 @@
 /**
  * Base class for the events that make up an orchestration instance's execution history.
  * 

- * Instances are obtained from {@link com.microsoft.durabletask.DurableTaskClient#streamInstanceHistory(String)}. Each + * 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. */ From 203d86daf1d198bcc24b618b98f2606ca05144da Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Thu, 2 Jul 2026 11:38:07 -0700 Subject: [PATCH 4/5] add Helpers.throwIfArgumentNull --- .../java/com/microsoft/durabletask/DurableTaskGrpcClient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java index da4af20..02e62bf 100644 --- a/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java +++ b/client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java @@ -299,6 +299,7 @@ private OrchestrationStatusQueryResult toQueryResult(QueryInstancesResponse quer @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))); @@ -313,6 +314,7 @@ public ListInstanceIdsResult listInstanceIds(ListInstanceIdsQuery query) { @Override public List getOrchestrationHistory(String instanceId) { + Helpers.throwIfArgumentNull(instanceId, "instanceId"); StreamInstanceHistoryRequest request = StreamInstanceHistoryRequest.newBuilder() .setInstanceId(instanceId) .build(); From fe65bf49bec543e7e2e631984e4544e13cbae15e Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Thu, 2 Jul 2026 12:43:24 -0700 Subject: [PATCH 5/5] defensively copy tags in OrchestrationState --- .../com/microsoft/durabletask/history/OrchestrationState.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java b/client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java index 79d32c3..40fcd78 100644 --- a/client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java +++ b/client/src/main/java/com/microsoft/durabletask/history/OrchestrationState.java @@ -8,6 +8,7 @@ import javax.annotation.Nullable; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.Map; /** @@ -83,7 +84,7 @@ public OrchestrationState( this.failureDetails = failureDetails; this.executionId = executionId; this.parentInstanceId = parentInstanceId; - this.tags = tags == null ? null : Collections.unmodifiableMap(tags); + this.tags = tags == null ? null : Collections.unmodifiableMap(new HashMap<>(tags)); } /** @return the orchestration instance ID. */