From f11dd007cd8e231a43ed9178d6b881f13b77e0e6 Mon Sep 17 00:00:00 2001 From: Nitish Agarwal <1592163+nitishagar@users.noreply.github.com> Date: Fri, 26 Dec 2025 13:26:02 +0530 Subject: [PATCH] Fix UnsupportedOperationException when merging ActivityOptions with immutable context propagators The mergeActivityOptions method was calling addAll() directly on the existing contextPropagators list, which fails when the list is immutable (e.g., created with List.of() or Collections.emptyList()). This fix creates a new ArrayList that combines both lists instead of modifying the existing one. Fixes #2482 --- .../io/temporal/activity/ActivityOptions.java | 5 +- .../activity/ActivityOptionsTest.java | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/temporal-sdk/src/main/java/io/temporal/activity/ActivityOptions.java b/temporal-sdk/src/main/java/io/temporal/activity/ActivityOptions.java index 69032d1f7..f67a4beed 100644 --- a/temporal-sdk/src/main/java/io/temporal/activity/ActivityOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/activity/ActivityOptions.java @@ -6,6 +6,7 @@ import io.temporal.common.context.ContextPropagator; import io.temporal.failure.CanceledFailure; import java.time.Duration; +import java.util.ArrayList; import java.util.List; /** Options used to configure how an activity is invoked. */ @@ -282,7 +283,9 @@ public Builder mergeActivityOptions(ActivityOptions override) { if (this.contextPropagators == null) { this.contextPropagators = override.contextPropagators; } else if (override.contextPropagators != null) { - this.contextPropagators.addAll(override.contextPropagators); + List merged = new ArrayList<>(this.contextPropagators); + merged.addAll(override.contextPropagators); + this.contextPropagators = merged; } if (override.versioningIntent != VersioningIntent.VERSIONING_INTENT_UNSPECIFIED) { this.versioningIntent = override.versioningIntent; diff --git a/temporal-sdk/src/test/java/io/temporal/activity/ActivityOptionsTest.java b/temporal-sdk/src/test/java/io/temporal/activity/ActivityOptionsTest.java index 26f8cc910..fb51ce7c8 100644 --- a/temporal-sdk/src/test/java/io/temporal/activity/ActivityOptionsTest.java +++ b/temporal-sdk/src/test/java/io/temporal/activity/ActivityOptionsTest.java @@ -2,13 +2,17 @@ import static org.junit.Assert.*; +import io.temporal.api.common.v1.Payload; import io.temporal.common.MethodRetry; import io.temporal.common.RetryOptions; +import io.temporal.common.context.ContextPropagator; import io.temporal.testing.TestActivityEnvironment; import io.temporal.workflow.shared.TestActivities.TestActivity; import io.temporal.workflow.shared.TestActivities.TestActivityImpl; import java.lang.reflect.Method; import java.time.Duration; +import java.util.Collections; +import java.util.List; import java.util.Map; import org.junit.*; import org.junit.rules.Timeout; @@ -62,6 +66,69 @@ public void testActivityOptionsMerge() { Assert.assertEquals(methodOps1, merged); } + @Test + public void testActivityOptionsMergeWithImmutableContextPropagators() { + // Local class to avoid code duplication + class TestContextPropagator implements ContextPropagator { + private final String name; + + TestContextPropagator(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Map serializeContext(Object context) { + return Collections.emptyMap(); + } + + @Override + public Object deserializeContext(Map context) { + return null; + } + + @Override + public Object getCurrentContext() { + return null; + } + + @Override + public void setCurrentContext(Object context) {} + } + + ContextPropagator propagator1 = new TestContextPropagator("propagator1"); + ContextPropagator propagator2 = new TestContextPropagator("propagator2"); + + // Create options with immutable singleton lists + // This tests the fix for https://github.com/temporalio/sdk-java/issues/2482 + ActivityOptions options1 = + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(1)) + .setContextPropagators(Collections.singletonList(propagator1)) + .build(); + + ActivityOptions options2 = + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(2)) + .setContextPropagators(Collections.singletonList(propagator2)) + .build(); + + // Merging should not throw UnsupportedOperationException + ActivityOptions merged = + ActivityOptions.newBuilder(options1).mergeActivityOptions(options2).build(); + + // Verify both context propagators are present in the merged result + List mergedPropagators = merged.getContextPropagators(); + assertNotNull(mergedPropagators); + assertEquals(2, mergedPropagators.size()); + assertEquals("propagator1", mergedPropagators.get(0).getName()); + assertEquals("propagator2", mergedPropagators.get(1).getName()); + } + @Test public void testActivityOptionsDefaultInstance() { testEnv.registerActivitiesImplementations(new TestActivityImpl());