From 3295d53e91a04273da9f1b227b5965187fc993e2 Mon Sep 17 00:00:00 2001 From: Oleksandr Porunov Date: Mon, 25 May 2026 22:02:56 +0100 Subject: [PATCH] feat(#2790): Add ChildWorkflowOptions support to WorkflowImplementationOptions Fixes #2790 - Add childWorkflowOptions map and defaultChildWorkflowOptions fields - Add setChildWorkflowOptions() and setDefaultChildWorkflowOptions() builder methods - Add getChildWorkflowOptions() and getDefaultChildWorkflowOptions() getters - Update SyncWorkflowContext to store and expose child workflow options - Update WorkflowInternal.newChildWorkflowStub() to merge predefined options - Add mergeChildWorkflowOptions() method to ChildWorkflowOptions.Builder - Add integration and unit tests --------- Signed-off-by: Oleksandr Porunov --- .../internal/sync/SyncWorkflowContext.java | 16 +++ .../internal/sync/WorkflowInternal.java | 21 ++++ .../worker/WorkflowImplementationOptions.java | 59 ++++++++++ .../workflow/ChildWorkflowOptions.java | 69 +++++++++++ ...nsInWorkflowImplementationOptionsTest.java | 108 ++++++++++++++++++ ...ChildWorkflowOptionsSetOnWorkflowTest.java | 74 ++++++++++++ 6 files changed, 347 insertions(+) create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/ChildWorkflowOptionsInWorkflowImplementationOptionsTest.java create mode 100644 temporal-sdk/src/test/java/io/temporal/workflow/DefaultChildWorkflowOptionsSetOnWorkflowTest.java diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java index f83d2bcd4a..a70a0e6396 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java @@ -100,6 +100,8 @@ final class SyncWorkflowContext implements WorkflowContext, WorkflowOutboundCall private Map localActivityOptionsMap; private NexusServiceOptions defaultNexusServiceOptions = null; private Map nexusServiceOptionsMap; + private ChildWorkflowOptions defaultChildWorkflowOptions = null; + private Map childWorkflowOptionsMap; private boolean readOnly = false; private final WorkflowThreadLocal currentUpdateInfo = new WorkflowThreadLocal<>(); @Nullable private String currentDetails; @@ -136,6 +138,10 @@ public SyncWorkflowContext( workflowImplementationOptions.getDefaultNexusServiceOptions(); this.nexusServiceOptionsMap = new HashMap<>(workflowImplementationOptions.getNexusServiceOptions()); + this.defaultChildWorkflowOptions = + workflowImplementationOptions.getDefaultChildWorkflowOptions(); + this.childWorkflowOptionsMap = + new HashMap<>(workflowImplementationOptions.getChildWorkflowOptions()); } this.workflowImplementationOptions = workflowImplementationOptions == null @@ -215,6 +221,16 @@ public NexusServiceOptions getDefaultNexusServiceOptions() { : Collections.emptyMap(); } + public ChildWorkflowOptions getDefaultChildWorkflowOptions() { + return defaultChildWorkflowOptions; + } + + public @Nonnull Map getChildWorkflowOptions() { + return childWorkflowOptionsMap != null + ? Collections.unmodifiableMap(childWorkflowOptionsMap) + : Collections.emptyMap(); + } + public void setDefaultActivityOptions(ActivityOptions defaultActivityOptions) { this.defaultActivityOptions = (this.defaultActivityOptions == null) diff --git a/temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java b/temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java index 79495694b4..b12e1900a5 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java @@ -391,6 +391,27 @@ public static ActivityStub newUntypedLocalActivityStub(LocalActivityOptions opti @SuppressWarnings("unchecked") public static T newChildWorkflowStub( Class workflowInterface, ChildWorkflowOptions options) { + SyncWorkflowContext context = getRootWorkflowContext(); + options = (options == null) ? context.getDefaultChildWorkflowOptions() : options; + + POJOWorkflowInterfaceMetadata workflowMetadata = + POJOWorkflowInterfaceMetadata.newInstance(workflowInterface); + Optional workflowMethodMetadata = + workflowMetadata.getWorkflowMethod(); + if (workflowMethodMetadata.isPresent()) { + String workflowType = workflowMethodMetadata.get().getName(); + @Nonnull + Map predefinedChildWorkflowOptions = + context.getChildWorkflowOptions(); + ChildWorkflowOptions perTypeOptions = predefinedChildWorkflowOptions.get(workflowType); + if (perTypeOptions != null) { + options = + ChildWorkflowOptions.newBuilder(perTypeOptions) + .mergeChildWorkflowOptions(options) + .build(); + } + } + return (T) Proxy.newProxyInstance( workflowInterface.getClassLoader(), diff --git a/temporal-sdk/src/main/java/io/temporal/worker/WorkflowImplementationOptions.java b/temporal-sdk/src/main/java/io/temporal/worker/WorkflowImplementationOptions.java index c2d0aa033c..a3b428d231 100644 --- a/temporal-sdk/src/main/java/io/temporal/worker/WorkflowImplementationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/worker/WorkflowImplementationOptions.java @@ -3,6 +3,7 @@ import io.temporal.activity.ActivityOptions; import io.temporal.activity.LocalActivityOptions; import io.temporal.common.Experimental; +import io.temporal.workflow.ChildWorkflowOptions; import io.temporal.workflow.NexusServiceOptions; import io.temporal.workflow.Workflow; import java.util.*; @@ -42,6 +43,8 @@ public static final class Builder { private LocalActivityOptions defaultLocalActivityOptions; private Map nexusServiceOptions; private NexusServiceOptions defaultNexusServiceOptions; + private Map childWorkflowOptions; + private ChildWorkflowOptions defaultChildWorkflowOptions; private boolean enableUpsertVersionSearchAttributes; private Builder() {} @@ -57,6 +60,8 @@ private Builder(WorkflowImplementationOptions options) { this.defaultLocalActivityOptions = options.getDefaultLocalActivityOptions(); this.nexusServiceOptions = options.getNexusServiceOptions(); this.defaultNexusServiceOptions = options.getDefaultNexusServiceOptions(); + this.childWorkflowOptions = options.getChildWorkflowOptions(); + this.defaultChildWorkflowOptions = options.getDefaultChildWorkflowOptions(); this.enableUpsertVersionSearchAttributes = options.isEnableUpsertVersionSearchAttributes(); } @@ -158,6 +163,34 @@ public Builder setDefaultNexusServiceOptions(NexusServiceOptions defaultNexusSer return this; } + /** + * Set individual child workflow options per workflow type. Will be merged with the options from + * {@link io.temporal.workflow.Workflow#newChildWorkflowStub(Class, ChildWorkflowOptions)} which + * has the highest precedence. + * + * @param childWorkflowOptions map from workflow type to ChildWorkflowOptions + */ + public Builder setChildWorkflowOptions(Map childWorkflowOptions) { + this.childWorkflowOptions = new HashMap<>(Objects.requireNonNull(childWorkflowOptions)); + return this; + } + + /** + * These child workflow options have the lowest precedence across all child workflow options. + * Will be overwritten entirely by {@link + * io.temporal.workflow.Workflow#newChildWorkflowStub(Class, ChildWorkflowOptions)} and then by + * the individual child workflow options if any are set through {@link + * #setChildWorkflowOptions(Map)} + * + * @param defaultChildWorkflowOptions ChildWorkflowOptions for all child workflows in the + * workflow. + */ + public Builder setDefaultChildWorkflowOptions( + ChildWorkflowOptions defaultChildWorkflowOptions) { + this.defaultChildWorkflowOptions = Objects.requireNonNull(defaultChildWorkflowOptions); + return this; + } + /** * Enable upserting version search attributes on {@link Workflow#getVersion}. This will cause * the SDK to automatically add the TemporalChangeVersion search attributes to the @@ -187,6 +220,8 @@ public WorkflowImplementationOptions build() { defaultLocalActivityOptions, nexusServiceOptions == null ? null : nexusServiceOptions, defaultNexusServiceOptions, + childWorkflowOptions == null ? null : childWorkflowOptions, + defaultChildWorkflowOptions, enableUpsertVersionSearchAttributes); } } @@ -198,6 +233,8 @@ public WorkflowImplementationOptions build() { private final LocalActivityOptions defaultLocalActivityOptions; private final @Nullable Map nexusServiceOptions; private final NexusServiceOptions defaultNexusServiceOptions; + private final @Nullable Map childWorkflowOptions; + private final ChildWorkflowOptions defaultChildWorkflowOptions; private final boolean enableUpsertVersionSearchAttributes; public WorkflowImplementationOptions( @@ -208,6 +245,8 @@ public WorkflowImplementationOptions( LocalActivityOptions defaultLocalActivityOptions, @Nullable Map nexusServiceOptions, NexusServiceOptions defaultNexusServiceOptions, + @Nullable Map childWorkflowOptions, + ChildWorkflowOptions defaultChildWorkflowOptions, boolean enableUpsertVersionSearchAttributes) { this.failWorkflowExceptionTypes = failWorkflowExceptionTypes; this.activityOptions = activityOptions; @@ -216,6 +255,8 @@ public WorkflowImplementationOptions( this.defaultLocalActivityOptions = defaultLocalActivityOptions; this.nexusServiceOptions = nexusServiceOptions; this.defaultNexusServiceOptions = defaultNexusServiceOptions; + this.childWorkflowOptions = childWorkflowOptions; + this.defaultChildWorkflowOptions = defaultChildWorkflowOptions; this.enableUpsertVersionSearchAttributes = enableUpsertVersionSearchAttributes; } @@ -253,6 +294,16 @@ public NexusServiceOptions getDefaultNexusServiceOptions() { return defaultNexusServiceOptions; } + public @Nonnull Map getChildWorkflowOptions() { + return childWorkflowOptions != null + ? Collections.unmodifiableMap(childWorkflowOptions) + : Collections.emptyMap(); + } + + public ChildWorkflowOptions getDefaultChildWorkflowOptions() { + return defaultChildWorkflowOptions; + } + @Experimental public boolean isEnableUpsertVersionSearchAttributes() { return enableUpsertVersionSearchAttributes; @@ -275,6 +326,10 @@ public String toString() { + nexusServiceOptions + ", defaultNexusServiceOptions=" + defaultNexusServiceOptions + + ", childWorkflowOptions=" + + childWorkflowOptions + + ", defaultChildWorkflowOptions=" + + defaultChildWorkflowOptions + ", enableUpsertVersionSearchAttributes=" + enableUpsertVersionSearchAttributes + '}'; @@ -292,6 +347,8 @@ public boolean equals(Object o) { && Objects.equals(defaultLocalActivityOptions, that.defaultLocalActivityOptions) && Objects.equals(nexusServiceOptions, that.nexusServiceOptions) && Objects.equals(defaultNexusServiceOptions, that.defaultNexusServiceOptions) + && Objects.equals(childWorkflowOptions, that.childWorkflowOptions) + && Objects.equals(defaultChildWorkflowOptions, that.defaultChildWorkflowOptions) && Objects.equals( enableUpsertVersionSearchAttributes, that.enableUpsertVersionSearchAttributes); } @@ -306,6 +363,8 @@ public int hashCode() { defaultLocalActivityOptions, nexusServiceOptions, defaultNexusServiceOptions, + childWorkflowOptions, + defaultChildWorkflowOptions, enableUpsertVersionSearchAttributes); result = 31 * result + Arrays.hashCode(failWorkflowExceptionTypes); return result; diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/ChildWorkflowOptions.java b/temporal-sdk/src/main/java/io/temporal/workflow/ChildWorkflowOptions.java index 261cd97247..0bdf087f63 100644 --- a/temporal-sdk/src/main/java/io/temporal/workflow/ChildWorkflowOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/workflow/ChildWorkflowOptions.java @@ -338,6 +338,75 @@ public Builder setPriority(Priority priority) { return this; } + /** + * Merges the provided override options into this builder. Any non-null fields in the override + * will take precedence over the fields in this builder. + * + * @param override ChildWorkflowOptions that overrides the current builder values. + * @return this builder. + */ + @SuppressWarnings("deprecation") + public Builder mergeChildWorkflowOptions(ChildWorkflowOptions override) { + if (override == null) { + return this; + } + this.namespace = (override.getNamespace() == null) ? this.namespace : override.getNamespace(); + this.workflowId = + (override.getWorkflowId() == null) ? this.workflowId : override.getWorkflowId(); + this.workflowIdReusePolicy = + (override.getWorkflowIdReusePolicy() == null) + ? this.workflowIdReusePolicy + : override.getWorkflowIdReusePolicy(); + this.workflowRunTimeout = + (override.getWorkflowRunTimeout() == null) + ? this.workflowRunTimeout + : override.getWorkflowRunTimeout(); + this.workflowExecutionTimeout = + (override.getWorkflowExecutionTimeout() == null) + ? this.workflowExecutionTimeout + : override.getWorkflowExecutionTimeout(); + this.workflowTaskTimeout = + (override.getWorkflowTaskTimeout() == null) + ? this.workflowTaskTimeout + : override.getWorkflowTaskTimeout(); + this.taskQueue = (override.getTaskQueue() == null) ? this.taskQueue : override.getTaskQueue(); + this.retryOptions = + (override.getRetryOptions() == null) ? this.retryOptions : override.getRetryOptions(); + this.cronSchedule = + (override.getCronSchedule() == null) ? this.cronSchedule : override.getCronSchedule(); + this.parentClosePolicy = + (override.getParentClosePolicy() == null) + ? this.parentClosePolicy + : override.getParentClosePolicy(); + this.memo = (override.getMemo() == null) ? this.memo : override.getMemo(); + this.searchAttributes = + (override.getSearchAttributes() == null) + ? this.searchAttributes + : override.getSearchAttributes(); + this.typedSearchAttributes = + (override.getTypedSearchAttributes() == null) + ? this.typedSearchAttributes + : override.getTypedSearchAttributes(); + this.contextPropagators = + (override.getContextPropagators() == null) + ? this.contextPropagators + : override.getContextPropagators(); + this.cancellationType = + (override.getCancellationType() == null) + ? this.cancellationType + : override.getCancellationType(); + this.versioningIntent = + (override.getVersioningIntent() == null) + ? this.versioningIntent + : override.getVersioningIntent(); + this.staticSummary = + (override.getStaticSummary() == null) ? this.staticSummary : override.getStaticSummary(); + this.staticDetails = + (override.getStaticDetails() == null) ? this.staticDetails : override.getStaticDetails(); + this.priority = (override.getPriority() == null) ? this.priority : override.getPriority(); + return this; + } + public ChildWorkflowOptions build() { return new ChildWorkflowOptions( namespace, diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/ChildWorkflowOptionsInWorkflowImplementationOptionsTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/ChildWorkflowOptionsInWorkflowImplementationOptionsTest.java new file mode 100644 index 0000000000..bbd90525a2 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/ChildWorkflowOptionsInWorkflowImplementationOptionsTest.java @@ -0,0 +1,108 @@ +package io.temporal.workflow; + +import static org.junit.Assert.*; + +import io.temporal.worker.WorkflowImplementationOptions; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import org.junit.Test; + +public class ChildWorkflowOptionsInWorkflowImplementationOptionsTest { + + @Test + public void testBuilderSetAndGet() { + ChildWorkflowOptions defaultOpts = + ChildWorkflowOptions.newBuilder() + .setWorkflowExecutionTimeout(Duration.ofSeconds(100)) + .setTaskQueue("default-queue") + .build(); + + ChildWorkflowOptions perTypeOpts = + ChildWorkflowOptions.newBuilder() + .setWorkflowExecutionTimeout(Duration.ofSeconds(200)) + .setTaskQueue("per-type-queue") + .build(); + + Map optionsMap = + Collections.singletonMap("MyWorkflow", perTypeOpts); + + WorkflowImplementationOptions options = + WorkflowImplementationOptions.newBuilder() + .setDefaultChildWorkflowOptions(defaultOpts) + .setChildWorkflowOptions(optionsMap) + .build(); + + assertEquals(defaultOpts, options.getDefaultChildWorkflowOptions()); + assertEquals(1, options.getChildWorkflowOptions().size()); + assertEquals(perTypeOpts, options.getChildWorkflowOptions().get("MyWorkflow")); + } + + @Test + public void testDefaultInstanceHasEmptyChildWorkflowOptions() { + WorkflowImplementationOptions options = WorkflowImplementationOptions.getDefaultInstance(); + assertNull(options.getDefaultChildWorkflowOptions()); + assertNotNull(options.getChildWorkflowOptions()); + assertTrue(options.getChildWorkflowOptions().isEmpty()); + } + + @Test + public void testToBuilder() { + ChildWorkflowOptions defaultOpts = + ChildWorkflowOptions.newBuilder() + .setWorkflowExecutionTimeout(Duration.ofSeconds(100)) + .build(); + + Map optionsMap = + Collections.singletonMap("MyWorkflow", defaultOpts); + + WorkflowImplementationOptions original = + WorkflowImplementationOptions.newBuilder() + .setDefaultChildWorkflowOptions(defaultOpts) + .setChildWorkflowOptions(optionsMap) + .build(); + + WorkflowImplementationOptions copy = original.toBuilder().build(); + assertEquals(original.getDefaultChildWorkflowOptions(), copy.getDefaultChildWorkflowOptions()); + assertEquals(original.getChildWorkflowOptions(), copy.getChildWorkflowOptions()); + } + + @Test + public void testMergeChildWorkflowOptionsOverridesNonNull() { + ChildWorkflowOptions base = + ChildWorkflowOptions.newBuilder() + .setWorkflowExecutionTimeout(Duration.ofSeconds(100)) + .setTaskQueue("base-queue") + .setWorkflowRunTimeout(Duration.ofSeconds(50)) + .build(); + + ChildWorkflowOptions override = + ChildWorkflowOptions.newBuilder() + .setWorkflowExecutionTimeout(Duration.ofSeconds(200)) + .build(); + + ChildWorkflowOptions merged = + ChildWorkflowOptions.newBuilder(base).mergeChildWorkflowOptions(override).build(); + + // Override takes precedence for workflowExecutionTimeout + assertEquals(Duration.ofSeconds(200), merged.getWorkflowExecutionTimeout()); + // Base values are preserved for fields not set in override + assertEquals("base-queue", merged.getTaskQueue()); + assertEquals(Duration.ofSeconds(50), merged.getWorkflowRunTimeout()); + } + + @Test + public void testMergeChildWorkflowOptionsWithNull() { + ChildWorkflowOptions base = + ChildWorkflowOptions.newBuilder() + .setWorkflowExecutionTimeout(Duration.ofSeconds(100)) + .setTaskQueue("base-queue") + .build(); + + ChildWorkflowOptions merged = + ChildWorkflowOptions.newBuilder(base).mergeChildWorkflowOptions(null).build(); + + assertEquals(Duration.ofSeconds(100), merged.getWorkflowExecutionTimeout()); + assertEquals("base-queue", merged.getTaskQueue()); + } +} diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/DefaultChildWorkflowOptionsSetOnWorkflowTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/DefaultChildWorkflowOptionsSetOnWorkflowTest.java new file mode 100644 index 0000000000..fe8cd73e24 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/workflow/DefaultChildWorkflowOptionsSetOnWorkflowTest.java @@ -0,0 +1,74 @@ +package io.temporal.workflow; + +import static org.junit.Assert.assertEquals; + +import io.temporal.client.WorkflowOptions; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.worker.WorkflowImplementationOptions; +import io.temporal.workflow.shared.TestWorkflows.TestWorkflow1; +import io.temporal.workflow.shared.TestWorkflows.TestWorkflowReturnString; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; + +public class DefaultChildWorkflowOptionsSetOnWorkflowTest { + + private static final Duration DEFAULT_WORKFLOW_EXECUTION_TIMEOUT = Duration.ofSeconds(100); + private static final Duration PER_TYPE_WORKFLOW_EXECUTION_TIMEOUT = Duration.ofSeconds(200); + + private static final ChildWorkflowOptions defaultChildWorkflowOptions = + ChildWorkflowOptions.newBuilder() + .setWorkflowExecutionTimeout(DEFAULT_WORKFLOW_EXECUTION_TIMEOUT) + .build(); + + private static final ChildWorkflowOptions perTypeChildWorkflowOptions = + ChildWorkflowOptions.newBuilder() + .setWorkflowExecutionTimeout(PER_TYPE_WORKFLOW_EXECUTION_TIMEOUT) + .build(); + + private static final Map childWorkflowOptionsMap = + Collections.singletonMap("TestWorkflow1", perTypeChildWorkflowOptions); + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes( + WorkflowImplementationOptions.newBuilder() + .setDefaultChildWorkflowOptions(defaultChildWorkflowOptions) + .setChildWorkflowOptions(childWorkflowOptionsMap) + .build(), + ParentWorkflowImpl.class, + ChildWorkflowImpl.class) + .build(); + + @Test + public void testDefaultChildWorkflowOptionsApplied() { + TestWorkflowReturnString workflowStub = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + TestWorkflowReturnString.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + String result = workflowStub.execute(); + assertEquals("child_done", result); + } + + public static class ParentWorkflowImpl implements TestWorkflowReturnString { + @Override + public String execute() { + // This uses the per-type options from WorkflowImplementationOptions + // because the workflow type "TestWorkflow1" has specific options set. + TestWorkflow1 child = Workflow.newChildWorkflowStub(TestWorkflow1.class); + return child.execute("input"); + } + } + + public static class ChildWorkflowImpl implements TestWorkflow1 { + @Override + public String execute(String arg) { + return "child_done"; + } + } +}