Skip to content

Commit 1ed76a4

Browse files
committed
Merge remote-tracking branch 'origin/main' into vabachu/exporthistory-module
2 parents 963516c + c2c7efa commit 1ed76a4

15 files changed

Lines changed: 1186 additions & 22 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
## Unreleased
2+
* 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))
3+
* Add `createReplaySafeLogger` to suppress orchestration log output during replay ([#295](https://github.com/microsoft/durabletask-java/pull/295)).
24
* Add `getParentInstance()` API to `TaskOrchestrationContext` for discovering parent orchestration info ([#284](https://github.com/microsoft/durabletask-java/pull/284))
35

46
## v1.9.0

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,30 @@ result += ctx.callActivity("SayHello", "Seattle", String.class).await();
1616
return result;
1717
```
1818

19+
### Replay-safe orchestration logging
20+
21+
Orchestrator code re-executes while rebuilding state from history. Wrap an existing
22+
`java.util.logging.Logger` to suppress log output during those replay segments:
23+
24+
```java
25+
Logger logger = ctx.createReplaySafeLogger(
26+
Logger.getLogger(MyOrchestration.class.getName()));
27+
28+
logger.info(() -> "Starting orchestration " + ctx.getInstanceId());
29+
String result = ctx.callActivity("ProcessItem", input, String.class).await();
30+
logger.info(() -> "Activity returned: " + result);
31+
```
32+
33+
In Azure Functions, pass `ExecutionContext.getLogger()` instead of creating a named
34+
logger so the output retains its invocation ID and normal host routing:
35+
36+
```java
37+
Logger logger = ctx.createReplaySafeLogger(executionContext.getLogger());
38+
```
39+
40+
Replay-safe logging suppresses calls made while replaying; it does not guarantee
41+
exactly-once log delivery across failed or retried live orchestration turns.
42+
1943
### Reliable fan-out / fan-in orchestration pattern
2044

2145
```java

client/src/main/java/com/microsoft/durabletask/DurableTaskClient.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,16 @@ public ListInstanceIdsResult listInstanceIds(ListInstanceIdsQuery query) {
247247
"Listing instance IDs is not supported by this client implementation.");
248248
}
249249

250-
/**
251-
* Gets the full history of an orchestration instance as an ordered list of {@link HistoryEvent} objects.
250+
/**
251+
* Retrieves the history of the specified orchestration instance as a list of {@link HistoryEvent} objects.
252252
* <p>
253253
* The events are returned in execution order. This is useful for archiving or offline analysis of an instance's
254254
* execution history. Use {@code instanceof} to inspect each concrete event type.
255255
*
256-
* @param instanceId the unique ID of the orchestration instance whose history to fetch
257-
* @return the instance's history events in order; empty if the instance has no history
256+
* @param instanceId the instance ID of the orchestration
257+
* @return the list of {@link HistoryEvent} objects representing the orchestration's history, in execution order;
258+
* empty if the instance has no history
259+
* @throws IllegalArgumentException if {@code instanceId} is null
258260
* @throws UnsupportedOperationException if the current client implementation does not support history retrieval
259261
*/
260262
public List<HistoryEvent> getOrchestrationHistory(String instanceId) {

client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,13 @@ public ListInstanceIdsResult listInstanceIds(ListInstanceIdsQuery query) {
308308
builder.setPageSize(query.getPageSize());
309309
query.getRuntimeStatusList().forEach(runtimeStatus -> Optional.ofNullable(runtimeStatus).ifPresent(status -> builder.addRuntimeStatus(OrchestrationRuntimeStatus.toProtobuf(status))));
310310
ListInstanceIdsResponse response = this.sidecarClient.listInstanceIds(builder.build());
311-
String continuationToken = response.hasLastInstanceKey() ? response.getLastInstanceKey().getValue() : null;
311+
// Treat an absent OR empty lastInstanceKey as end-of-results. A caller that loops while the
312+
// continuation token is non-null would otherwise re-request with an empty cursor and restart
313+
// paging from the beginning, causing an infinite loop.
314+
String continuationToken =
315+
response.hasLastInstanceKey() && !response.getLastInstanceKey().getValue().isEmpty()
316+
? response.getLastInstanceKey().getValue()
317+
: null;
312318
return new ListInstanceIdsResult(new ArrayList<>(response.getInstanceIdsList()), continuationToken);
313319
}
314320

@@ -319,11 +325,22 @@ public List<HistoryEvent> getOrchestrationHistory(String instanceId) {
319325
.setInstanceId(instanceId)
320326
.build();
321327
List<HistoryEvent> historyEvents = new ArrayList<>();
322-
Iterator<HistoryChunk> chunks = this.sidecarClient.streamInstanceHistory(request);
323-
while (chunks.hasNext()) {
324-
for (OrchestratorService.HistoryEvent protoEvent : chunks.next().getEventsList()) {
325-
historyEvents.add(HistoryEventConverter.fromProto(protoEvent));
328+
// Bind the server-streaming call to a cancellable context so the RPC is always released, even if
329+
// draining the stream exits early via an exception. Without this, an early exit abandons the
330+
// blocking iterator without cancelling the call -- a known grpc-java resource-leak pattern.
331+
Context.CancellableContext streamContext = Context.current().withCancellation();
332+
Context previous = streamContext.attach();
333+
try {
334+
Iterator<HistoryChunk> chunks = this.sidecarClient.streamInstanceHistory(request);
335+
while (chunks.hasNext()) {
336+
for (OrchestratorService.HistoryEvent protoEvent : chunks.next().getEventsList()) {
337+
historyEvents.add(HistoryEventConverter.fromProto(protoEvent));
338+
}
326339
}
340+
} finally {
341+
streamContext.detach(previous);
342+
// Cancels the RPC if it is still open (e.g. early exit mid-stream); no-op once it completed normally.
343+
streamContext.cancel(null);
327344
}
328345
return historyEvents;
329346
}

client/src/main/java/com/microsoft/durabletask/ListInstanceIdsQuery.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import javax.annotation.Nullable;
66
import java.time.Instant;
77
import java.util.ArrayList;
8+
import java.util.Collections;
89
import java.util.List;
910

1011
/**
@@ -93,26 +94,46 @@ public ListInstanceIdsQuery setContinuationToken(@Nullable String continuationTo
9394
return this;
9495
}
9596

96-
List<OrchestrationRuntimeStatus> getRuntimeStatusList() {
97-
return this.runtimeStatusList;
97+
/**
98+
* Gets the configured terminal runtime status filter, or an empty list if none was configured.
99+
* @return an unmodifiable view of the configured terminal runtime status filter
100+
*/
101+
public List<OrchestrationRuntimeStatus> getRuntimeStatusList() {
102+
return Collections.unmodifiableList(this.runtimeStatusList);
98103
}
99104

105+
/**
106+
* Gets the configured minimum completion time, or {@code null} if none was configured.
107+
* @return the configured minimum completion time, or {@code null} if none was configured
108+
*/
100109
@Nullable
101-
Instant getCompletedTimeFrom() {
110+
public Instant getCompletedTimeFrom() {
102111
return this.completedTimeFrom;
103112
}
104113

114+
/**
115+
* Gets the configured maximum completion time, or {@code null} if none was configured.
116+
* @return the configured maximum completion time, or {@code null} if none was configured
117+
*/
105118
@Nullable
106-
Instant getCompletedTimeTo() {
119+
public Instant getCompletedTimeTo() {
107120
return this.completedTimeTo;
108121
}
109122

110-
int getPageSize() {
123+
/**
124+
* Gets the configured maximum number of instance IDs to return per page.
125+
* @return the configured page size
126+
*/
127+
public int getPageSize() {
111128
return this.pageSize;
112129
}
113130

131+
/**
132+
* Gets the configured pagination cursor, or {@code null} if none was configured.
133+
* @return the configured pagination cursor, or {@code null} if none was configured
134+
*/
114135
@Nullable
115-
String getContinuationToken() {
136+
public String getContinuationToken() {
116137
return this.continuationToken;
117138
}
118139
}

0 commit comments

Comments
 (0)