From 8955ea0aff3292ed66980eac08b4caab4b7b7aad 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 | 85 +++++++++++++++++++ 2 files changed, 89 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..3fdaf9e9a 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,87 @@ public void testActivityOptionsMerge() { Assert.assertEquals(methodOps1, merged); } + @Test + public void testActivityOptionsMergeWithImmutableContextPropagators() { + // Create simple test context propagators + ContextPropagator propagator1 = + new ContextPropagator() { + @Override + public String getName() { + return "propagator1"; + } + + @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 propagator2 = + new ContextPropagator() { + @Override + public String getName() { + return "propagator2"; + } + + @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) {} + }; + + // 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());