From caa9902673c31f6d55e52844b1055bdf87ae5df6 Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Mon, 4 May 2026 22:21:35 -0700 Subject: [PATCH 1/4] initial commit --- .../ParentOrchestrationInstance.java | 65 +++ .../durabletask/TaskOrchestrationContext.java | 11 + .../TaskOrchestrationExecutor.java | 15 + .../TaskOrchestrationExecutorTest.java | 378 ++++++++++++++++++ samples/build.gradle | 6 + .../samples/ParentInstanceSample.java | 96 +++++ 6 files changed, 571 insertions(+) create mode 100644 client/src/main/java/com/microsoft/durabletask/ParentOrchestrationInstance.java create mode 100644 samples/src/main/java/io/durabletask/samples/ParentInstanceSample.java diff --git a/client/src/main/java/com/microsoft/durabletask/ParentOrchestrationInstance.java b/client/src/main/java/com/microsoft/durabletask/ParentOrchestrationInstance.java new file mode 100644 index 00000000..d8933d80 --- /dev/null +++ b/client/src/main/java/com/microsoft/durabletask/ParentOrchestrationInstance.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.durabletask; + +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * Represents the parent orchestration of a sub-orchestration. + * This is available via {@link TaskOrchestrationContext#getParent()} when the + * current orchestration was started as a sub-orchestration. + */ +public final class ParentOrchestrationInstance { + private final String name; + private final String instanceId; + + /** + * Creates a new ParentOrchestrationInstance. + * + * @param name the name of the parent orchestration + * @param instanceId the instance ID of the parent orchestration + */ + public ParentOrchestrationInstance(String name, String instanceId) { + this.name = name; + this.instanceId = instanceId; + } + + /** + * Gets the name of the parent orchestration. + * + * @return the parent orchestration name + */ + @Nullable + public String getName() { + return this.name; + } + + /** + * Gets the instance ID of the parent orchestration. + * + * @return the parent orchestration instance ID + */ + @Nullable + public String getInstanceId() { + return this.instanceId; + } + + @Override + public String toString() { + return String.format("ParentOrchestrationInstance{name='%s', instanceId='%s'}", name, instanceId); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ParentOrchestrationInstance)) return false; + ParentOrchestrationInstance that = (ParentOrchestrationInstance) o; + return Objects.equals(name, that.name) && Objects.equals(instanceId, that.instanceId); + } + + @Override + public int hashCode() { + return Objects.hash(name, instanceId); + } +} diff --git a/client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java b/client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java index e94acde4..7e17f056 100644 --- a/client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java +++ b/client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java @@ -794,6 +794,17 @@ default Task lockEntities(@Nonnull EntityInstanceId... entityIds) */ void clearCustomStatus(); + /** + * Gets the parent orchestration instance, or {@code null} if this orchestration + * was not started as a sub-orchestration. + * + * @return the parent orchestration instance, or {@code null} + */ + @Nullable + default ParentOrchestrationInstance getParent() { + return null; + } + /** * Makes a durable HTTP request using the specified {@link DurableHttpRequest} and returns a {@link Task} * that completes when the HTTP call completes. diff --git a/client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java b/client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java index 8e60080e..de6e9fb7 100644 --- a/client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java +++ b/client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java @@ -146,6 +146,7 @@ private class ContextImplTask implements TaskOrchestrationContext { private boolean preserveUnprocessedEvents; private Object customStatus; private TraceContext parentTraceContext; + private ParentOrchestrationInstance parentInstance; // Entity integration state (Phase 4) private String executionId; @@ -207,6 +208,12 @@ public String getInstanceId() { return this.instanceId; } + @Override + @Nullable + public ParentOrchestrationInstance getParent() { + return this.parentInstance; + } + private void setInstanceId(String instanceId) { // TODO: Throw if instance ID is not null this.instanceId = instanceId; @@ -1705,6 +1712,14 @@ private void processEvent(HistoryEvent e) { } else { this.parentTraceContext = null; } + if (startedEvent.hasParentInstance()) { + ParentInstanceInfo parentInfo = startedEvent.getParentInstance(); + this.parentInstance = new ParentOrchestrationInstance( + parentInfo.getName().getValue(), + parentInfo.getOrchestrationInstance().getInstanceId()); + } else { + this.parentInstance = null; + } TaskOrchestrationFactory factory = TaskOrchestrationExecutor.this.orchestrationFactories.get(name); if (factory == null) { // Try getting the default orchestrator diff --git a/client/src/test/java/com/microsoft/durabletask/TaskOrchestrationExecutorTest.java b/client/src/test/java/com/microsoft/durabletask/TaskOrchestrationExecutorTest.java index 52c2e887..bc3ae1c9 100644 --- a/client/src/test/java/com/microsoft/durabletask/TaskOrchestrationExecutorTest.java +++ b/client/src/test/java/com/microsoft/durabletask/TaskOrchestrationExecutorTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test; import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; import java.util.*; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -285,4 +287,380 @@ public TaskOrchestration create() { assertFalse(scheduleAction.getScheduleTask().hasParentTraceContext(), "ScheduleTaskAction should not have parentTraceContext when none was provided"); } + + // region Parent Instance Tests + + @Test + void execute_withParentInstance_getParentReturnsParent() { + // Arrange: orchestration that captures getParent() + String orchName = "ChildOrch"; + ParentOrchestrationInstance[] captured = new ParentOrchestrationInstance[1]; + + HashMap factories = new HashMap<>(); + factories.put(orchName, new TaskOrchestrationFactory() { + @Override + public String getName() { return orchName; } + @Override + public TaskOrchestration create() { + return ctx -> { + captured[0] = ctx.getParent(); + ctx.complete("done"); + }; + } + }); + + TaskOrchestrationExecutor executor = new TaskOrchestrationExecutor( + factories, new JacksonDataConverter(), Duration.ofDays(3), logger, null); + + List newEvents = Arrays.asList( + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorStarted(OrchestratorStartedEvent.getDefaultInstance()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setExecutionStarted(ExecutionStartedEvent.newBuilder() + .setName(orchName) + .setVersion(StringValue.of("")) + .setInput(StringValue.of("\"test\"")) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId("child-instance") + .build()) + .setParentInstance(ParentInstanceInfo.newBuilder() + .setName(StringValue.of("ParentOrch")) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId("parent-123") + .build()) + .build()) + .build()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorCompleted(OrchestratorCompletedEvent.getDefaultInstance()) + .build() + ); + + // Act + executor.execute(Collections.emptyList(), newEvents, null); + + // Assert + assertNotNull(captured[0], "getParent() should not be null for a sub-orchestration"); + assertEquals("ParentOrch", captured[0].getName()); + assertEquals("parent-123", captured[0].getInstanceId()); + } + + @Test + void execute_withoutParentInstance_getParentReturnsNull() { + // Arrange: orchestration without parent instance + String orchName = "StandaloneOrch"; + ParentOrchestrationInstance[] captured = new ParentOrchestrationInstance[1]; + boolean[] wasCalled = {false}; + + HashMap factories = new HashMap<>(); + factories.put(orchName, new TaskOrchestrationFactory() { + @Override + public String getName() { return orchName; } + @Override + public TaskOrchestration create() { + return ctx -> { + captured[0] = ctx.getParent(); + wasCalled[0] = true; + ctx.complete("done"); + }; + } + }); + + TaskOrchestrationExecutor executor = new TaskOrchestrationExecutor( + factories, new JacksonDataConverter(), Duration.ofDays(3), logger, null); + + List newEvents = Arrays.asList( + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorStarted(OrchestratorStartedEvent.getDefaultInstance()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setExecutionStarted(ExecutionStartedEvent.newBuilder() + .setName(orchName) + .setVersion(StringValue.of("")) + .setInput(StringValue.of("\"test\"")) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId("standalone-instance") + .build()) + .build()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorCompleted(OrchestratorCompletedEvent.getDefaultInstance()) + .build() + ); + + // Act + executor.execute(Collections.emptyList(), newEvents, null); + + // Assert + assertTrue(wasCalled[0], "Orchestrator should have been called"); + assertNull(captured[0], "getParent() should be null for a standalone orchestration"); + } + + @Test + void execute_withParentInstance_preservesExactValues() { + // Arrange: use mixed casing and special characters + String orchName = "ChildOrch"; + String parentName = "Parent.Orch-V2"; + String parentInstanceId = "abc-DEF-123_special!@#"; + ParentOrchestrationInstance[] captured = new ParentOrchestrationInstance[1]; + + HashMap factories = new HashMap<>(); + factories.put(orchName, new TaskOrchestrationFactory() { + @Override + public String getName() { return orchName; } + @Override + public TaskOrchestration create() { + return ctx -> { + captured[0] = ctx.getParent(); + ctx.complete("done"); + }; + } + }); + + TaskOrchestrationExecutor executor = new TaskOrchestrationExecutor( + factories, new JacksonDataConverter(), Duration.ofDays(3), logger, null); + + List newEvents = Arrays.asList( + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorStarted(OrchestratorStartedEvent.getDefaultInstance()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setExecutionStarted(ExecutionStartedEvent.newBuilder() + .setName(orchName) + .setVersion(StringValue.of("")) + .setInput(StringValue.of("\"test\"")) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId("child-instance") + .build()) + .setParentInstance(ParentInstanceInfo.newBuilder() + .setName(StringValue.of(parentName)) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId(parentInstanceId) + .build()) + .build()) + .build()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorCompleted(OrchestratorCompletedEvent.getDefaultInstance()) + .build() + ); + + // Act + executor.execute(Collections.emptyList(), newEvents, null); + + // Assert: values must match exactly, no normalization + assertNotNull(captured[0]); + assertEquals(parentName, captured[0].getName()); + assertEquals(parentInstanceId, captured[0].getInstanceId()); + } + + @Test + void execute_withParentInstance_emptyFields_acceptsValues() { + // Arrange: parent instance with empty/default StringValue fields + String orchName = "ChildOrch"; + ParentOrchestrationInstance[] captured = new ParentOrchestrationInstance[1]; + + HashMap factories = new HashMap<>(); + factories.put(orchName, new TaskOrchestrationFactory() { + @Override + public String getName() { return orchName; } + @Override + public TaskOrchestration create() { + return ctx -> { + captured[0] = ctx.getParent(); + ctx.complete("done"); + }; + } + }); + + TaskOrchestrationExecutor executor = new TaskOrchestrationExecutor( + factories, new JacksonDataConverter(), Duration.ofDays(3), logger, null); + + List newEvents = Arrays.asList( + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorStarted(OrchestratorStartedEvent.getDefaultInstance()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setExecutionStarted(ExecutionStartedEvent.newBuilder() + .setName(orchName) + .setVersion(StringValue.of("")) + .setInput(StringValue.of("\"test\"")) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId("child-instance") + .build()) + .setParentInstance(ParentInstanceInfo.newBuilder() + .setName(StringValue.of("")) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId("") + .build()) + .build()) + .build()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorCompleted(OrchestratorCompletedEvent.getDefaultInstance()) + .build() + ); + + // Act + executor.execute(Collections.emptyList(), newEvents, null); + + // Assert: permissive — empty values accepted as-is, matching .NET behavior + assertNotNull(captured[0], "getParent() should not be null when parentInstance is present"); + assertEquals("", captured[0].getName()); + assertEquals("", captured[0].getInstanceId()); + } + + @Test + void taskOrchestrationContext_defaultGetParent_returnsNull() { + // Minimal implementation that does NOT override getParent(). + // All abstract methods are stubbed to satisfy the interface contract. + TaskOrchestrationContext minimalContext = new TaskOrchestrationContext() { + @Override public String getName() { return "test"; } + @Override public V getInput(Class t) { return null; } + @Override public String getInstanceId() { return "id"; } + @Override public Instant getCurrentInstant() { return Instant.now(); } + @Override public boolean getIsReplaying() { return false; } + @Override public String getVersion() { return ""; } + @Override public Task> allOf(List> tasks) { return null; } + @Override public Task> anyOf(List> tasks) { return null; } + @Override public Task callActivity(String name, Object input, TaskOptions options, Class returnType) { return null; } + @Override public Task callSubOrchestrator(String name, Object input, String instanceId, TaskOptions options, Class returnType) { return null; } + @Override public Task createTimer(Duration delay) { return null; } + @Override public Task createTimer(ZonedDateTime zonedDateTime) { return null; } + @Override public Task waitForExternalEvent(String name, Duration timeout, Class dataType) { return null; } + @Override public void continueAsNew(Object input, boolean preserveUnprocessedEvents) {} + @Override public void sendEvent(String instanceId, String eventName, Object eventData) {} + @Override public void complete(Object output) {} + @Override public UUID newUUID() { return UUID.randomUUID(); } + @Override public void signalEntity(EntityInstanceId entityId, String operationName, Object input, SignalEntityOptions options) {} + @Override public Task callEntity(EntityInstanceId entityId, String operationName, Object input, Class returnType) { return null; } + @Override public Task callEntity(EntityInstanceId entityId, String operationName, Object input, Class returnType, CallEntityOptions options) { return null; } + @Override public Task lockEntities(List entityIds) { return null; } + @Override public boolean isInCriticalSection() { return false; } + @Override public List getLockedEntities() { return Collections.emptyList(); } + @Override public void setCustomStatus(Object customStatus) {} + @Override public void clearCustomStatus() {} + }; + + // Assert: default method returns null + assertNull(minimalContext.getParent(), "Default getParent() should return null"); + } + + @Test + void executorReplay_parentValueStableAcrossReplays() { + // Arrange: simulate replay — ExecutionStartedEvent is in pastEvents, + // new dispatch has OrchestratorStarted + OrchestratorCompleted. + String orchName = "ChildOrch"; + ParentOrchestrationInstance[] captured = new ParentOrchestrationInstance[1]; + + HashMap factories = new HashMap<>(); + factories.put(orchName, new TaskOrchestrationFactory() { + @Override + public String getName() { return orchName; } + @Override + public TaskOrchestration create() { + return ctx -> { + captured[0] = ctx.getParent(); + ctx.complete("done"); + }; + } + }); + + TaskOrchestrationExecutor executor = new TaskOrchestrationExecutor( + factories, new JacksonDataConverter(), Duration.ofDays(3), logger, null); + + // Past events: the initial execution (already processed) + List pastEvents = Arrays.asList( + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorStarted(OrchestratorStartedEvent.getDefaultInstance()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setExecutionStarted(ExecutionStartedEvent.newBuilder() + .setName(orchName) + .setVersion(StringValue.of("")) + .setInput(StringValue.of("\"test\"")) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId("child-instance") + .build()) + .setParentInstance(ParentInstanceInfo.newBuilder() + .setName(StringValue.of("ParentOrch")) + .setOrchestrationInstance(OrchestrationInstance.newBuilder() + .setInstanceId("parent-456") + .build()) + .build()) + .build()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorCompleted(OrchestratorCompletedEvent.getDefaultInstance()) + .build() + ); + + // New events: a new dispatch re-enters the orchestrator + List newEvents = Arrays.asList( + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorStarted(OrchestratorStartedEvent.getDefaultInstance()) + .build(), + HistoryEvent.newBuilder() + .setEventId(-1) + .setTimestamp(Timestamp.getDefaultInstance()) + .setOrchestratorCompleted(OrchestratorCompletedEvent.getDefaultInstance()) + .build() + ); + + // Act: execute with replay history + executor.execute(pastEvents, newEvents, null); + + // Assert: parent is still available and unchanged during replay + assertNotNull(captured[0], "getParent() should not be null during replay"); + assertEquals("ParentOrch", captured[0].getName()); + assertEquals("parent-456", captured[0].getInstanceId()); + } + + @Test + void parentOrchestrationInstance_equalsAndHashCode() { + ParentOrchestrationInstance a = new ParentOrchestrationInstance("Orch", "id-1"); + ParentOrchestrationInstance b = new ParentOrchestrationInstance("Orch", "id-1"); + ParentOrchestrationInstance c = new ParentOrchestrationInstance("Other", "id-1"); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertNotEquals(a, c); + assertNotEquals(a, null); + assertEquals("ParentOrchestrationInstance{name='Orch', instanceId='id-1'}", a.toString()); + } + + // endregion } diff --git a/samples/build.gradle b/samples/build.gradle index c8f67721..766a8c07 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -83,6 +83,12 @@ task runTypedEntityProxySample(type: JavaExec) { environment dtsEnv } +task runParentInstanceSample(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'io.durabletask.samples.ParentInstanceSample' + environment dtsEnv +} + task printClasspath { doLast { println sourceSets.main.runtimeClasspath.asPath diff --git a/samples/src/main/java/io/durabletask/samples/ParentInstanceSample.java b/samples/src/main/java/io/durabletask/samples/ParentInstanceSample.java new file mode 100644 index 00000000..69a4e84b --- /dev/null +++ b/samples/src/main/java/io/durabletask/samples/ParentInstanceSample.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package io.durabletask.samples; + +import com.microsoft.durabletask.*; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.TimeoutException; + +/** + * Demonstrates the {@link TaskOrchestrationContext#getParent()} API. + * + *

A parent orchestration starts a child sub-orchestration. The child uses + * {@code ctx.getParent()} to discover its parent's name and instance ID. + * The same child orchestration is also started standalone to show that + * {@code getParent()} returns {@code null} in that case. + * + *

Run with: {@code ./gradlew runParentInstanceSample} + */ +final class ParentInstanceSample { + public static void main(String[] args) throws IOException, InterruptedException, TimeoutException { + DurableTaskGrpcWorkerBuilder workerBuilder = SampleUtils.newWorkerBuilder(); + + // --- Register orchestrations --- + + // Parent orchestration: calls ChildOrchestrator as a sub-orchestration + workerBuilder.addOrchestration(new TaskOrchestrationFactory() { + @Override + public String getName() { return "ParentOrchestrator"; } + + @Override + public TaskOrchestration create() { + return ctx -> { + System.out.println(" [Parent] Starting child sub-orchestration..."); + String childResult = ctx.callSubOrchestrator( + "ChildOrchestrator", null, String.class).await(); + System.out.println(" [Parent] Child returned: " + childResult); + ctx.complete(childResult); + }; + } + }); + + // Child orchestration: reports its parent (if any) + workerBuilder.addOrchestration(new TaskOrchestrationFactory() { + @Override + public String getName() { return "ChildOrchestrator"; } + + @Override + public TaskOrchestration create() { + return ctx -> { + ParentOrchestrationInstance parent = ctx.getParent(); + String result; + if (parent != null) { + result = String.format("I was called by '%s' (instance: %s)", + parent.getName(), parent.getInstanceId()); + System.out.println(" [Child] " + result); + } else { + result = "No parent — I was started standalone"; + System.out.println(" [Child] " + result); + } + ctx.complete(result); + }; + } + }); + + DurableTaskGrpcWorker worker = workerBuilder.build(); + worker.start(); + + DurableTaskClient client = SampleUtils.newClientBuilder().build(); + + // --- Step 1: Start parent orchestration (child will see its parent) --- + System.out.println("=== Step 1: Sub-orchestration (child has parent) ==="); + String parentId = client.scheduleNewOrchestrationInstance("ParentOrchestrator"); + System.out.println(" Scheduled ParentOrchestrator: " + parentId); + OrchestrationMetadata result1 = client.waitForInstanceCompletion( + parentId, Duration.ofSeconds(30), true); + System.out.println(" Status: " + result1.getRuntimeStatus()); + if (result1.getRuntimeStatus() == OrchestrationRuntimeStatus.COMPLETED) { + System.out.println(" Result: " + result1.readOutputAs(String.class)); + } else { + System.out.println(" Failure: " + result1.getFailureDetails()); + } + + // --- Step 2: Start child orchestration directly (no parent) --- + System.out.println("\n=== Step 2: Standalone orchestration (no parent) ==="); + String standaloneId = client.scheduleNewOrchestrationInstance("ChildOrchestrator"); + System.out.println(" Scheduled ChildOrchestrator standalone: " + standaloneId); + OrchestrationMetadata result2 = client.waitForInstanceCompletion( + standaloneId, Duration.ofSeconds(30), true); + System.out.println(" Result: " + result2.readOutputAs(String.class)); + + System.out.println("\n=== Sample completed ==="); + worker.stop(); + } +} From 556dd31cd67e84e71544efd918c2cba0a63c269c Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Mon, 4 May 2026 22:31:48 -0700 Subject: [PATCH 2/4] updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 160e790f..a3198c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## Unreleased +* Add `getParent()` API to `TaskOrchestrationContext` for discovering parent orchestration info ([#284](https://github.com/microsoft/durabletask-java/pull/284)) ## v1.9.0 * Fix entity locking deserialization and add Jackson support for EntityInstanceId/EntityMetadata ([#281](https://github.com/microsoft/durabletask-java/pull/281)) From 1f339a218e89fbedaa3f8fc16299428c6f9f37cf Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Wed, 6 May 2026 16:52:49 -0700 Subject: [PATCH 3/4] updated ParentOrchestrationInstance to nonnull --- .../durabletask/ParentOrchestrationInstance.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/main/java/com/microsoft/durabletask/ParentOrchestrationInstance.java b/client/src/main/java/com/microsoft/durabletask/ParentOrchestrationInstance.java index d8933d80..999522f9 100644 --- a/client/src/main/java/com/microsoft/durabletask/ParentOrchestrationInstance.java +++ b/client/src/main/java/com/microsoft/durabletask/ParentOrchestrationInstance.java @@ -3,7 +3,7 @@ package com.microsoft.durabletask; import java.util.Objects; -import javax.annotation.Nullable; +import javax.annotation.Nonnull; /** * Represents the parent orchestration of a sub-orchestration. @@ -20,9 +20,9 @@ public final class ParentOrchestrationInstance { * @param name the name of the parent orchestration * @param instanceId the instance ID of the parent orchestration */ - public ParentOrchestrationInstance(String name, String instanceId) { - this.name = name; - this.instanceId = instanceId; + public ParentOrchestrationInstance(@Nonnull String name, @Nonnull String instanceId) { + this.name = Objects.requireNonNull(name, "name"); + this.instanceId = Objects.requireNonNull(instanceId, "instanceId"); } /** @@ -30,7 +30,7 @@ public ParentOrchestrationInstance(String name, String instanceId) { * * @return the parent orchestration name */ - @Nullable + @Nonnull public String getName() { return this.name; } @@ -40,7 +40,7 @@ public String getName() { * * @return the parent orchestration instance ID */ - @Nullable + @Nonnull public String getInstanceId() { return this.instanceId; } From 56523d73c2cc65d50252d449540f888852eaf6ce Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Wed, 6 May 2026 16:55:01 -0700 Subject: [PATCH 4/4] added isReplaying checks --- .../samples/ParentInstanceSample.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/samples/src/main/java/io/durabletask/samples/ParentInstanceSample.java b/samples/src/main/java/io/durabletask/samples/ParentInstanceSample.java index 69a4e84b..68e94f72 100644 --- a/samples/src/main/java/io/durabletask/samples/ParentInstanceSample.java +++ b/samples/src/main/java/io/durabletask/samples/ParentInstanceSample.java @@ -32,10 +32,14 @@ public static void main(String[] args) throws IOException, InterruptedException, @Override public TaskOrchestration create() { return ctx -> { - System.out.println(" [Parent] Starting child sub-orchestration..."); + if (!ctx.getIsReplaying()) { + System.out.println(" [Parent] Starting child sub-orchestration..."); + } String childResult = ctx.callSubOrchestrator( "ChildOrchestrator", null, String.class).await(); - System.out.println(" [Parent] Child returned: " + childResult); + if (!ctx.getIsReplaying()) { + System.out.println(" [Parent] Child returned: " + childResult); + } ctx.complete(childResult); }; } @@ -54,10 +58,14 @@ public TaskOrchestration create() { if (parent != null) { result = String.format("I was called by '%s' (instance: %s)", parent.getName(), parent.getInstanceId()); - System.out.println(" [Child] " + result); + if (!ctx.getIsReplaying()) { + System.out.println(" [Child] " + result); + } } else { result = "No parent — I was started standalone"; - System.out.println(" [Child] " + result); + if (!ctx.getIsReplaying()) { + System.out.println(" [Child] " + result); + } } ctx.complete(result); };