Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Unreleased
* Add client APIs to list terminal instance IDs by completion time (`listInstanceIds`) and read orchestration history (`getOrchestrationHistory`) ([#292](https://github.com/microsoft/durabletask-java/pull/292))
* Add `createReplaySafeLogger` to suppress orchestration log output during replay ([#295](https://github.com/microsoft/durabletask-java/pull/295)).
* Add `getParentInstance()` API to `TaskOrchestrationContext` for discovering parent orchestration info ([#284](https://github.com/microsoft/durabletask-java/pull/284))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -226,6 +229,41 @@ public abstract OrchestrationMetadata waitForInstanceCompletion(
*/
public abstract OrchestrationStatusQueryResult queryInstances(OrchestrationStatusQuery query);

/**
* Lists the IDs of terminal orchestration instances that completed within a time window.
* <p>
* Unlike {@link #queryInstances(OrchestrationStatusQuery)}, which filters by creation time and returns full
* metadata, this method filters by <em>completion</em> 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
* @throws UnsupportedOperationException if the current client implementation does not support listing instance IDs
*/
public ListInstanceIdsResult listInstanceIds(ListInstanceIdsQuery query) {
throw new UnsupportedOperationException(
"Listing instance IDs is not supported by this client implementation.");
}

/**
* Retrieves the history of the specified orchestration instance as a list of {@link HistoryEvent} objects.
* <p>
* 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 instance ID of the orchestration
* @return the list of {@link HistoryEvent} objects representing the orchestration's history, in execution order;
* empty if the instance has no history
* @throws IllegalArgumentException if {@code instanceId} is null
* @throws UnsupportedOperationException if the current client implementation does not support history retrieval
*/
public List<HistoryEvent> getOrchestrationHistory(String instanceId) {
throw new UnsupportedOperationException(
"Retrieving orchestration history is not supported by this client implementation.");
}

/**
* Initializes the target task hub data store.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -295,6 +297,54 @@ private OrchestrationStatusQueryResult toQueryResult(QueryInstancesResponse quer
return new OrchestrationStatusQueryResult(metadataList, queryInstancesResponse.getContinuationToken().getValue());
}

@Override
public ListInstanceIdsResult listInstanceIds(ListInstanceIdsQuery query) {
Helpers.throwIfArgumentNull(query, "query");
ListInstanceIdsRequest.Builder builder = ListInstanceIdsRequest.newBuilder();
Optional.ofNullable(query.getCompletedTimeFrom()).ifPresent(from -> builder.setCompletedTimeFrom(DataConverter.getTimestampFromInstant(from)));
Optional.ofNullable(query.getCompletedTimeTo()).ifPresent(to -> builder.setCompletedTimeTo(DataConverter.getTimestampFromInstant(to)));
String requestContinuationToken = query.getContinuationToken() != null ? query.getContinuationToken() : "";
builder.setLastInstanceKey(StringValue.of(requestContinuationToken));
Comment thread
bachuv marked this conversation as resolved.
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());
// Treat an absent OR empty lastInstanceKey as end-of-results. A caller that loops while the
// continuation token is non-null would otherwise re-request with an empty cursor and restart
// paging from the beginning, causing an infinite loop.
String continuationToken =
response.hasLastInstanceKey() && !response.getLastInstanceKey().getValue().isEmpty()
? response.getLastInstanceKey().getValue()
: null;
return new ListInstanceIdsResult(new ArrayList<>(response.getInstanceIdsList()), continuationToken);
}

@Override
public List<HistoryEvent> getOrchestrationHistory(String instanceId) {
Helpers.throwIfArgumentNull(instanceId, "instanceId");
StreamInstanceHistoryRequest request = StreamInstanceHistoryRequest.newBuilder()
.setInstanceId(instanceId)
.build();
List<HistoryEvent> historyEvents = new ArrayList<>();
// Bind the server-streaming call to a cancellable context so the RPC is always released, even if
// draining the stream exits early via an exception. Without this, an early exit abandons the
// blocking iterator without cancelling the call -- a known grpc-java resource-leak pattern.
Context.CancellableContext streamContext = Context.current().withCancellation();
Context previous = streamContext.attach();
try {
Iterator<HistoryChunk> chunks = this.sidecarClient.streamInstanceHistory(request);
while (chunks.hasNext()) {
for (OrchestratorService.HistoryEvent protoEvent : chunks.next().getEventsList()) {
historyEvents.add(HistoryEventConverter.fromProto(protoEvent));
}
}
} finally {
streamContext.detach(previous);
// Cancels the RPC if it is still open (e.g. early exit mid-stream); no-op once it completed normally.
streamContext.cancel(null);
}
return historyEvents;
Comment thread
bachuv marked this conversation as resolved.
}

@Override
public void createTaskHub(boolean recreateIfExists) {
this.sidecarClient.createTaskHub(CreateTaskHubRequest.newBuilder().setRecreateIfExists(recreateIfExists).build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* <p>
* Use {@link DurableEntityClient#getAllEntities(EntityQuery)} to obtain an instance of this class.
*
* <h3>Example: iterate over all entities</h3>
* <h2>Example: iterate over all entities</h2>
* <pre>{@code
* EntityQuery query = new EntityQuery()
* .setInstanceIdStartsWith("counter")
Expand All @@ -28,7 +28,7 @@
* }
* }</pre>
*
* <h3>Example: iterate page by page</h3>
* <h2>Example: iterate page by page</h2>
* <pre>{@code
* for (EntityQueryResult page : client.getEntities().getAllEntities(query).byPage()) {
* System.out.println("Got " + page.getEntities().size() + " entities");
Expand Down
Loading
Loading