diff --git a/releases/v1.28.4 b/releases/v1.28.4 new file mode 100644 index 0000000000..4b3e62d1ca --- /dev/null +++ b/releases/v1.28.4 @@ -0,0 +1,7 @@ +# **Highlights** +* Attaching multiple Nexus callers to an underlying handler Workflow is now available in Pre-release. + +# What's Changed + +2025-04-02 - 68149062 - Ensure heartbeat details aren't cleared (#2460) +2025-04-02 - cdd64971 - Unblock UseExisting conflict policy for Nexus WorkflowRunOperation (#2440) diff --git a/temporal-sdk/src/main/java/io/temporal/internal/common/InternalUtils.java b/temporal-sdk/src/main/java/io/temporal/internal/common/InternalUtils.java index aa8d8a1452..e3817fbcb0 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/common/InternalUtils.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/common/InternalUtils.java @@ -22,11 +22,9 @@ import com.google.common.base.Defaults; import io.nexusrpc.Header; -import io.nexusrpc.handler.HandlerException; import io.nexusrpc.handler.ServiceImplInstance; import io.temporal.api.common.v1.Callback; import io.temporal.api.enums.v1.TaskQueueKind; -import io.temporal.api.enums.v1.WorkflowIdConflictPolicy; import io.temporal.api.taskqueue.v1.TaskQueue; import io.temporal.client.OnConflictOptions; import io.temporal.client.WorkflowOptions; @@ -37,7 +35,6 @@ import io.temporal.internal.client.NexusStartWorkflowRequest; import java.util.Arrays; import java.util.Map; -import java.util.Objects; import java.util.TreeMap; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -155,16 +152,6 @@ public static WorkflowStub createNexusBoundStub( .setAttachCompletionCallbacks(true) .build()); - // TODO(klassenq) temporarily blocking conflict policy USE_EXISTING. - if (Objects.equals( - WorkflowIdConflictPolicy.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, - options.getWorkflowIdConflictPolicy())) { - throw new HandlerException( - HandlerException.ErrorType.INTERNAL, - new IllegalArgumentException( - "Workflow ID conflict policy UseExisting is not supported for Nexus WorkflowRunOperation."), - HandlerException.RetryBehavior.NON_RETRYABLE); - } return stub.newInstance(nexusWorkflowOptions.build()); } diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleUseExistingOnConflictCancelTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleUseExistingOnConflictCancelTest.java index fb68370c9d..7761944a33 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleUseExistingOnConflictCancelTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleUseExistingOnConflictCancelTest.java @@ -38,7 +38,6 @@ import java.util.UUID; import org.junit.*; -@Ignore("Skipping until we can support USE_EXISTING") public class WorkflowHandleUseExistingOnConflictCancelTest { @Rule public SDKTestWorkflowRule testWorkflowRule = diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleUseExistingOnConflictTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleUseExistingOnConflictTest.java index 3037ba53ae..548ac48659 100644 --- a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleUseExistingOnConflictTest.java +++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/WorkflowHandleUseExistingOnConflictTest.java @@ -36,7 +36,6 @@ import java.util.UUID; import org.junit.*; -@Ignore("Skipping until we can support USE_EXISTING") public class WorkflowHandleUseExistingOnConflictTest { @Rule public SDKTestWorkflowRule testWorkflowRule = diff --git a/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java b/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java index 6e988cc98d..4887d2b695 100644 --- a/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java +++ b/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java @@ -1989,11 +1989,15 @@ private static State failActivityTask( RequestContext ctx, ActivityTaskData data, Object request, long notUsed) { if (request instanceof RespondActivityTaskFailedRequest) { RespondActivityTaskFailedRequest req = (RespondActivityTaskFailedRequest) request; - data.heartbeatDetails = req.getLastHeartbeatDetails(); + if (req.hasLastHeartbeatDetails()) { + data.heartbeatDetails = req.getLastHeartbeatDetails(); + } return failActivityTaskByRequestType(ctx, data, req.getFailure(), req.getIdentity()); } else if (request instanceof RespondActivityTaskFailedByIdRequest) { RespondActivityTaskFailedByIdRequest req = (RespondActivityTaskFailedByIdRequest) request; - data.heartbeatDetails = req.getLastHeartbeatDetails(); + if (req.hasLastHeartbeatDetails()) { + data.heartbeatDetails = req.getLastHeartbeatDetails(); + } return failActivityTaskByRequestType(ctx, data, req.getFailure(), req.getIdentity()); } else { throw new IllegalArgumentException("Unknown request: " + request); diff --git a/temporal-test-server/src/test/java/io/temporal/testserver/functional/activity/ActivityHeartbeat.java b/temporal-test-server/src/test/java/io/temporal/testserver/functional/activity/ActivityHeartbeat.java new file mode 100644 index 0000000000..4557ef91e2 --- /dev/null +++ b/temporal-test-server/src/test/java/io/temporal/testserver/functional/activity/ActivityHeartbeat.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.testserver.functional.activity; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import com.google.protobuf.ByteString; +import io.temporal.activity.Activity; +import io.temporal.activity.ActivityInfo; +import io.temporal.activity.ActivityOptions; +import io.temporal.api.common.v1.Payloads; +import io.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest; +import io.temporal.common.RetryOptions; +import io.temporal.common.converter.DefaultDataConverter; +import io.temporal.failure.ActivityFailure; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.testserver.functional.common.TestActivities; +import io.temporal.testserver.functional.common.TestWorkflows; +import io.temporal.workflow.Workflow; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.junit.Rule; +import org.junit.Test; + +public class ActivityHeartbeat { + private static final ConcurrentLinkedQueue> activityHeartbeats = + new ConcurrentLinkedQueue<>(); + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(TestWorkflow.class) + .setActivityImplementations(new TestActivity()) + .build(); + + @Test + public void testActivityHeartbeatNoLastHeartbeatDetails() { + // Test that when last heartbeat details are not set on failure, the test server + // clear the heartbeat details. + String result = + testWorkflowRule.newWorkflowStub(TestWorkflows.WorkflowReturnsString.class).execute(); + assertEquals("", result); + assertEquals(2, activityHeartbeats.size()); + assertFalse(activityHeartbeats.poll().isPresent()); + assertEquals( + "heartbeat details", + DefaultDataConverter.STANDARD_INSTANCE.fromPayloads( + 0, activityHeartbeats.poll(), String.class, String.class)); + } + + public static class TestActivity implements TestActivities.ActivityReturnsString { + @Override + public String execute() { + ActivityInfo info = Activity.getExecutionContext().getInfo(); + activityHeartbeats.add(info.getHeartbeatDetails()); + // Heartbeat with the raw service stub to avoid the SDK keeping track of the heartbeat + Activity.getExecutionContext() + .getWorkflowClient() + .getWorkflowServiceStubs() + .blockingStub() + .recordActivityTaskHeartbeat( + RecordActivityTaskHeartbeatRequest.newBuilder() + .setNamespace(info.getNamespace()) + .setTaskToken(ByteString.copyFrom(info.getTaskToken())) + .setDetails( + DefaultDataConverter.STANDARD_INSTANCE.toPayloads("heartbeat details").get()) + .build()); + throw new IllegalStateException("simulated failure"); + } + } + + public static class TestWorkflow implements TestWorkflows.WorkflowReturnsString { + @Override + public String execute() { + ActivityOptions options = + ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(2).build()) + .build(); + + try { + Workflow.newActivityStub(TestActivities.ActivityReturnsString.class, options).execute(); + } catch (ActivityFailure e) { + // Expected + } + return ""; + } + } +}