Skip to content

Commit 4d52632

Browse files
committed
updates
1 parent 0b9d338 commit 4d52632

7 files changed

Lines changed: 145 additions & 88 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## Unreleased
2-
* Add `getParent()` API to `TaskOrchestrationContext` for discovering parent orchestration info ([#284](https://github.com/microsoft/durabletask-java/pull/284))
2+
* Add `getParentInstance()` API to `TaskOrchestrationContext` for discovering parent orchestration info ([#284](https://github.com/microsoft/durabletask-java/pull/284))
33

44
## v1.9.0
55
* Fix entity locking deserialization and add Jackson support for EntityInstanceId/EntityMetadata ([#281](https://github.com/microsoft/durabletask-java/pull/281))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
/**
99
* Represents the parent orchestration of a sub-orchestration.
10-
* This is available via {@link TaskOrchestrationContext#getParent()} when the
10+
* This is available via {@link TaskOrchestrationContext#getParentInstance()} when the
1111
* current orchestration was started as a sub-orchestration.
1212
*/
1313
public final class ParentOrchestrationInstance {

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -797,13 +797,15 @@ default Task<AutoCloseable> lockEntities(@Nonnull EntityInstanceId... entityIds)
797797
/**
798798
* Gets the parent orchestration instance, or {@code null} if this orchestration
799799
* was not started as a sub-orchestration.
800+
* <p>
801+
* Implementers that wrap or decorate another {@link TaskOrchestrationContext}
802+
* must delegate to the wrapped instance. Returning {@code null} unconditionally
803+
* will incorrectly report sub-orchestrations as standalone.
800804
*
801805
* @return the parent orchestration instance, or {@code null}
802806
*/
803807
@Nullable
804-
default ParentOrchestrationInstance getParent() {
805-
return null;
806-
}
808+
ParentOrchestrationInstance getParentInstance();
807809

808810
/**
809811
* Makes a durable HTTP request using the specified {@link DurableHttpRequest} and returns a {@link Task}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public String getInstanceId() {
210210

211211
@Override
212212
@Nullable
213-
public ParentOrchestrationInstance getParent() {
213+
public ParentOrchestrationInstance getParentInstance() {
214214
return this.parentInstance;
215215
}
216216

@@ -1714,9 +1714,11 @@ private void processEvent(HistoryEvent e) {
17141714
}
17151715
if (startedEvent.hasParentInstance()) {
17161716
ParentInstanceInfo parentInfo = startedEvent.getParentInstance();
1717-
this.parentInstance = new ParentOrchestrationInstance(
1718-
parentInfo.getName().getValue(),
1719-
parentInfo.getOrchestrationInstance().getInstanceId());
1717+
String parentName = parentInfo.hasName() ? parentInfo.getName().getValue() : "";
1718+
String parentInstanceId = parentInfo.hasOrchestrationInstance()
1719+
? parentInfo.getOrchestrationInstance().getInstanceId()
1720+
: "";
1721+
this.parentInstance = new ParentOrchestrationInstance(parentName, parentInstanceId);
17201722
} else {
17211723
this.parentInstance = null;
17221724
}

client/src/test/java/com/microsoft/durabletask/EntityProxyTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ static class RecordingContext implements TaskOrchestrationContext {
128128
@Override
129129
public String getInstanceId() { return "test-instance"; }
130130

131+
@Override
132+
public ParentOrchestrationInstance getParentInstance() { return null; }
133+
131134
@Override
132135
public Instant getCurrentInstant() { return Instant.now(); }
133136

client/src/test/java/com/microsoft/durabletask/TaskOrchestrationExecutorTest.java

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ public TaskOrchestration create() {
291291
// region Parent Instance Tests
292292

293293
@Test
294-
void execute_withParentInstance_getParentReturnsParent() {
295-
// Arrange: orchestration that captures getParent()
294+
void execute_withParentInstance_getParentInstanceReturnsParent() {
295+
// Arrange: orchestration that captures getParentInstance()
296296
String orchName = "ChildOrch";
297297
ParentOrchestrationInstance[] captured = new ParentOrchestrationInstance[1];
298298

@@ -303,7 +303,7 @@ void execute_withParentInstance_getParentReturnsParent() {
303303
@Override
304304
public TaskOrchestration create() {
305305
return ctx -> {
306-
captured[0] = ctx.getParent();
306+
captured[0] = ctx.getParentInstance();
307307
ctx.complete("done");
308308
};
309309
}
@@ -347,13 +347,13 @@ public TaskOrchestration create() {
347347
executor.execute(Collections.emptyList(), newEvents, null);
348348

349349
// Assert
350-
assertNotNull(captured[0], "getParent() should not be null for a sub-orchestration");
350+
assertNotNull(captured[0], "getParentInstance() should not be null for a sub-orchestration");
351351
assertEquals("ParentOrch", captured[0].getName());
352352
assertEquals("parent-123", captured[0].getInstanceId());
353353
}
354354

355355
@Test
356-
void execute_withoutParentInstance_getParentReturnsNull() {
356+
void execute_withoutParentInstance_getParentInstanceReturnsNull() {
357357
// Arrange: orchestration without parent instance
358358
String orchName = "StandaloneOrch";
359359
ParentOrchestrationInstance[] captured = new ParentOrchestrationInstance[1];
@@ -366,7 +366,7 @@ void execute_withoutParentInstance_getParentReturnsNull() {
366366
@Override
367367
public TaskOrchestration create() {
368368
return ctx -> {
369-
captured[0] = ctx.getParent();
369+
captured[0] = ctx.getParentInstance();
370370
wasCalled[0] = true;
371371
ctx.complete("done");
372372
};
@@ -406,7 +406,7 @@ public TaskOrchestration create() {
406406

407407
// Assert
408408
assertTrue(wasCalled[0], "Orchestrator should have been called");
409-
assertNull(captured[0], "getParent() should be null for a standalone orchestration");
409+
assertNull(captured[0], "getParentInstance() should be null for a standalone orchestration");
410410
}
411411

412412
@Test
@@ -424,7 +424,7 @@ void execute_withParentInstance_preservesExactValues() {
424424
@Override
425425
public TaskOrchestration create() {
426426
return ctx -> {
427-
captured[0] = ctx.getParent();
427+
captured[0] = ctx.getParentInstance();
428428
ctx.complete("done");
429429
};
430430
}
@@ -486,7 +486,7 @@ void execute_withParentInstance_emptyFields_acceptsValues() {
486486
@Override
487487
public TaskOrchestration create() {
488488
return ctx -> {
489-
captured[0] = ctx.getParent();
489+
captured[0] = ctx.getParentInstance();
490490
ctx.complete("done");
491491
};
492492
}
@@ -530,45 +530,69 @@ public TaskOrchestration create() {
530530
executor.execute(Collections.emptyList(), newEvents, null);
531531

532532
// Assert: permissive — empty values accepted as-is, matching .NET behavior
533-
assertNotNull(captured[0], "getParent() should not be null when parentInstance is present");
533+
assertNotNull(captured[0], "getParentInstance() should not be null when parentInstance is present");
534534
assertEquals("", captured[0].getName());
535535
assertEquals("", captured[0].getInstanceId());
536536
}
537537

538538
@Test
539-
void taskOrchestrationContext_defaultGetParent_returnsNull() {
540-
// Minimal implementation that does NOT override getParent().
541-
// All abstract methods are stubbed to satisfy the interface contract.
542-
TaskOrchestrationContext minimalContext = new TaskOrchestrationContext() {
543-
@Override public String getName() { return "test"; }
544-
@Override public <V> V getInput(Class<V> t) { return null; }
545-
@Override public String getInstanceId() { return "id"; }
546-
@Override public Instant getCurrentInstant() { return Instant.now(); }
547-
@Override public boolean getIsReplaying() { return false; }
548-
@Override public String getVersion() { return ""; }
549-
@Override public <V> Task<List<V>> allOf(List<Task<V>> tasks) { return null; }
550-
@Override public Task<Task<?>> anyOf(List<Task<?>> tasks) { return null; }
551-
@Override public <V> Task<V> callActivity(String name, Object input, TaskOptions options, Class<V> returnType) { return null; }
552-
@Override public <V> Task<V> callSubOrchestrator(String name, Object input, String instanceId, TaskOptions options, Class<V> returnType) { return null; }
553-
@Override public Task<Void> createTimer(Duration delay) { return null; }
554-
@Override public Task<Void> createTimer(ZonedDateTime zonedDateTime) { return null; }
555-
@Override public <V> Task<V> waitForExternalEvent(String name, Duration timeout, Class<V> dataType) { return null; }
556-
@Override public void continueAsNew(Object input, boolean preserveUnprocessedEvents) {}
557-
@Override public void sendEvent(String instanceId, String eventName, Object eventData) {}
558-
@Override public void complete(Object output) {}
559-
@Override public UUID newUUID() { return UUID.randomUUID(); }
560-
@Override public void signalEntity(EntityInstanceId entityId, String operationName, Object input, SignalEntityOptions options) {}
561-
@Override public <V> Task<V> callEntity(EntityInstanceId entityId, String operationName, Object input, Class<V> returnType) { return null; }
562-
@Override public <V> Task<V> callEntity(EntityInstanceId entityId, String operationName, Object input, Class<V> returnType, CallEntityOptions options) { return null; }
563-
@Override public Task<AutoCloseable> lockEntities(List<EntityInstanceId> entityIds) { return null; }
564-
@Override public boolean isInCriticalSection() { return false; }
565-
@Override public List<EntityInstanceId> getLockedEntities() { return Collections.emptyList(); }
566-
@Override public void setCustomStatus(Object customStatus) {}
567-
@Override public void clearCustomStatus() {}
568-
};
569-
570-
// Assert: default method returns null
571-
assertNull(minimalContext.getParent(), "Default getParent() should return null");
539+
void execute_withParentInstance_unsetProtoFields_defaultsToEmpty() {
540+
// Arrange: ParentInstanceInfo is present, but its inner fields
541+
// (name StringValue and OrchestrationInstance) are unset.
542+
// The executor must not NPE — it should default to empty strings.
543+
String orchName = "ChildOrch";
544+
ParentOrchestrationInstance[] captured = new ParentOrchestrationInstance[1];
545+
546+
HashMap<String, TaskOrchestrationFactory> factories = new HashMap<>();
547+
factories.put(orchName, new TaskOrchestrationFactory() {
548+
@Override
549+
public String getName() { return orchName; }
550+
@Override
551+
public TaskOrchestration create() {
552+
return ctx -> {
553+
captured[0] = ctx.getParentInstance();
554+
ctx.complete("done");
555+
};
556+
}
557+
});
558+
559+
TaskOrchestrationExecutor executor = new TaskOrchestrationExecutor(
560+
factories, new JacksonDataConverter(), Duration.ofDays(3), logger, null);
561+
562+
List<HistoryEvent> newEvents = Arrays.asList(
563+
HistoryEvent.newBuilder()
564+
.setEventId(-1)
565+
.setTimestamp(Timestamp.getDefaultInstance())
566+
.setOrchestratorStarted(OrchestratorStartedEvent.getDefaultInstance())
567+
.build(),
568+
HistoryEvent.newBuilder()
569+
.setEventId(-1)
570+
.setTimestamp(Timestamp.getDefaultInstance())
571+
.setExecutionStarted(ExecutionStartedEvent.newBuilder()
572+
.setName(orchName)
573+
.setVersion(StringValue.of(""))
574+
.setInput(StringValue.of("\"test\""))
575+
.setOrchestrationInstance(OrchestrationInstance.newBuilder()
576+
.setInstanceId("child-instance")
577+
.build())
578+
// ParentInstanceInfo present but inner fields unset (no setName, no setOrchestrationInstance)
579+
.setParentInstance(ParentInstanceInfo.getDefaultInstance())
580+
.build())
581+
.build(),
582+
HistoryEvent.newBuilder()
583+
.setEventId(-1)
584+
.setTimestamp(Timestamp.getDefaultInstance())
585+
.setOrchestratorCompleted(OrchestratorCompletedEvent.getDefaultInstance())
586+
.build()
587+
);
588+
589+
// Act — must not throw NPE
590+
executor.execute(Collections.emptyList(), newEvents, null);
591+
592+
// Assert: parent is non-null, fields default to empty
593+
assertNotNull(captured[0], "getParentInstance() should not be null when ParentInstanceInfo is present");
594+
assertEquals("", captured[0].getName());
595+
assertEquals("", captured[0].getInstanceId());
572596
}
573597

574598
@Test
@@ -585,7 +609,7 @@ void executorReplay_parentValueStableAcrossReplays() {
585609
@Override
586610
public TaskOrchestration create() {
587611
return ctx -> {
588-
captured[0] = ctx.getParent();
612+
captured[0] = ctx.getParentInstance();
589613
ctx.complete("done");
590614
};
591615
}
@@ -644,7 +668,7 @@ public TaskOrchestration create() {
644668
executor.execute(pastEvents, newEvents, null);
645669

646670
// Assert: parent is still available and unchanged during replay
647-
assertNotNull(captured[0], "getParent() should not be null during replay");
671+
assertNotNull(captured[0], "getParentInstance() should not be null during replay");
648672
assertEquals("ParentOrch", captured[0].getName());
649673
assertEquals("parent-456", captured[0].getInstanceId());
650674
}

0 commit comments

Comments
 (0)