diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/ActivityWorker.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/ActivityWorker.java index 2a90c7780..1c2da2d4a 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/worker/ActivityWorker.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/ActivityWorker.java @@ -337,6 +337,32 @@ public Throwable wrapFailure(ActivityTask t, Throwable failure) { failure); } + /** + * Executes a gRPC call with proper interrupted flag handling. + * Activities that return with the 'interrupted' flag set, were unable to report their completion to the server. + * We need to clear 'interrupted' flag to allow gRPC calls to succeed,then restore it after reporting completion, + * to ensure gRPC calls succeed even when the thread has been interrupted. + * + * @param grpcCall the gRPC call to execute + * @see GitHub Issue #731 + */ + private void executeGrpcCallWithInterruptHandling(Runnable grpcCall) { + // Check if the current thread is interrupted before making gRPC calls + // If it is, we need to clear the flag to allow gRPC calls to succeed,then restore it after reporting. + // This handles the case where an activity catches InterruptedException, restores the interrupted flag, + // and continues to return a result. + + boolean wasInterrupted = Thread.interrupted(); // This clears the flag + try { + grpcCall.run(); + } finally { + // Restore the interrupted flag if it was set + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + } + } + // TODO: Suppress warning until the SDK supports deployment @SuppressWarnings("deprecation") private void sendReply( @@ -351,13 +377,15 @@ private void sendReply( .setWorkerVersion(options.workerVersionStamp()) .build(); - grpcRetryer.retry( + executeGrpcCallWithInterruptHandling( () -> - service - .blockingStub() - .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) - .respondActivityTaskCompleted(request), - replyGrpcRetryerOptions); + grpcRetryer.retry( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .respondActivityTaskCompleted(request), + replyGrpcRetryerOptions)); } else { Result.TaskFailedResult taskFailed = response.getTaskFailed(); if (taskFailed != null) { @@ -369,13 +397,15 @@ private void sendReply( .setWorkerVersion(options.workerVersionStamp()) .build(); - grpcRetryer.retry( + executeGrpcCallWithInterruptHandling( () -> - service - .blockingStub() - .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) - .respondActivityTaskFailed(request), - replyGrpcRetryerOptions); + grpcRetryer.retry( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .respondActivityTaskFailed(request), + replyGrpcRetryerOptions)); } else { RespondActivityTaskCanceledRequest taskCanceled = response.getTaskCanceled(); if (taskCanceled != null) { @@ -387,13 +417,15 @@ private void sendReply( .setWorkerVersion(options.workerVersionStamp()) .build(); - grpcRetryer.retry( + executeGrpcCallWithInterruptHandling( () -> - service - .blockingStub() - .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) - .respondActivityTaskCanceled(request), - replyGrpcRetryerOptions); + grpcRetryer.retry( + () -> + service + .blockingStub() + .withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope) + .respondActivityTaskCanceled(request), + replyGrpcRetryerOptions)); } } } diff --git a/temporal-sdk/src/test/java/io/temporal/worker/InterruptedActivityCompletionValidationTest.java b/temporal-sdk/src/test/java/io/temporal/worker/InterruptedActivityCompletionValidationTest.java new file mode 100644 index 000000000..8663b4ba9 --- /dev/null +++ b/temporal-sdk/src/test/java/io/temporal/worker/InterruptedActivityCompletionValidationTest.java @@ -0,0 +1,172 @@ +package io.temporal.worker; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; +import io.temporal.activity.ActivityOptions; +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.api.enums.v1.EventType; +import io.temporal.api.history.v1.HistoryEvent; +import io.temporal.client.WorkflowClient; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Rule; +import org.junit.Test; + +/** + * Validation test for the interrupted activity completion fix. + * + *
This test demonstrates that the fix for https://github.com/temporalio/sdk-java/issues/731 is working correctly. + * Before the fix, activities that returned with the interrupted flag set would fail to report their results + * due to gRPC call failures. + * + *
The fix was applied in ActivityWorker.sendReply() method to temporarily clear the interrupted flag + * during gRPC calls and restore it afterward. + */ +public class InterruptedActivityCompletionValidationTest { + + private static final String SUCCESS_RESULT = "completed-with-interrupted-flag"; + private static final AtomicInteger executionCount = new AtomicInteger(0); + private static final AtomicBoolean interruptedFlagWasSet = new AtomicBoolean(false); + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestWorkflowImpl.class) + .setActivityImplementations(new TestActivityImpl()) + .build(); + + @WorkflowInterface + public interface TestWorkflow { + @WorkflowMethod + String execute(); + } + + @ActivityInterface + public interface TestActivity { + @ActivityMethod + String processWithInterruptedFlag(); + } + + /** + * This test validates that the fix is working by demonstrating that: + * + *
+ * 1. An activity can set the interrupted flag and still return a result + * 2. The result is successfully reported to the Temporal server + * 3. The workflow completes with the expected result + * 4. The activity completion is properly recorded in the workflow history + * + *
Before the fix: This test would fail with CancellationException during gRPC calls After the
+ * fix: This test passes, proving activities can complete despite interrupted flag
+ */
+ @Test
+ public void testActivityCompletionWithInterruptedFlag() {
+ // Reset counters
+ executionCount.set(0);
+ interruptedFlagWasSet.set(false);
+
+ // Execute workflow
+ TestWorkflow workflow = testWorkflowRule.newWorkflowStub(TestWorkflow.class);
+ WorkflowExecution execution = WorkflowClient.start(workflow::execute);
+
+ // Wait for completion and get result
+ String result =
+ testWorkflowRule
+ .getWorkflowClient()
+ .newUntypedWorkflowStub(execution, null)
+ .getResult(String.class);
+
+ // Validate the workflow completed successfully with expected result
+ assertEquals("Activity should return the expected result", SUCCESS_RESULT, result);
+
+ // Validate the activity was executed exactly once
+ assertEquals("Activity should be executed exactly once", 1, executionCount.get());
+
+ // Validate that the interrupted flag was actually set during execution
+ assertTrue("Activity should have set the interrupted flag", interruptedFlagWasSet.get());
+
+ // Validate that the activity completion was properly recorded in workflow history
+ List