diff --git a/temporal-kotlin/src/main/kotlin/io/temporal/activity/ActivityExecutionContextExt.kt b/temporal-kotlin/src/main/kotlin/io/temporal/activity/ActivityExecutionContextExt.kt index 5721b03e5f..73bd7a5f3a 100644 --- a/temporal-kotlin/src/main/kotlin/io/temporal/activity/ActivityExecutionContextExt.kt +++ b/temporal-kotlin/src/main/kotlin/io/temporal/activity/ActivityExecutionContextExt.kt @@ -5,7 +5,8 @@ import kotlin.reflect.javaType import kotlin.reflect.typeOf /** - * Extracts Heartbeat details from the last failed attempt. + * Extracts heartbeat details from the last heartbeat of the current activity attempt or from the + * last failed attempt if no heartbeats were sent yet. * * @param T type of the Heartbeat details * @see ActivityExecutionContext.getHeartbeatDetails diff --git a/temporal-sdk/src/main/java/io/temporal/activity/ActivityExecutionContext.java b/temporal-sdk/src/main/java/io/temporal/activity/ActivityExecutionContext.java index 3067d0636d..8918effc4b 100644 --- a/temporal-sdk/src/main/java/io/temporal/activity/ActivityExecutionContext.java +++ b/temporal-sdk/src/main/java/io/temporal/activity/ActivityExecutionContext.java @@ -33,33 +33,56 @@ public interface ActivityExecutionContext { void heartbeat(V details) throws ActivityCompletionException; /** - * Extracts Heartbeat details from the last failed attempt. This is used in combination with retry - * options. An Activity Execution could be scheduled with optional {@link - * io.temporal.common.RetryOptions} via {@link io.temporal.activity.ActivityOptions}. If an - * Activity Execution failed then the server would attempt to dispatch another Activity Task to - * retry the execution according to the retry options. If there were Heartbeat details reported by - * the last Activity Execution that failed, they would be delivered along with the Activity Task - * for the next retry attempt and can be extracted by the Activity implementation. + * Extracts Heartbeat details from the last heartbeat of this Activity Execution attempt. If there + * were no heartbeats in this attempt, details from the last failed attempt are returned instead. + * This is used in combination with retry options. An Activity Execution could be scheduled with + * optional {@link io.temporal.common.RetryOptions} via {@link + * io.temporal.activity.ActivityOptions}. If an Activity Execution failed then the server would + * attempt to dispatch another Activity Task to retry the execution according to the retry + * options. If there were Heartbeat details reported by the last Activity Execution that failed, + * they would be delivered along with the Activity Task for the next retry attempt and can be + * extracted by the Activity implementation. * * @param detailsClass Class of the Heartbeat details */ Optional getHeartbeatDetails(Class detailsClass); /** - * Extracts Heartbeat details from the last failed attempt. This is used in combination with retry - * options. An Activity Execution could be scheduled with optional {@link - * io.temporal.common.RetryOptions} via {@link io.temporal.activity.ActivityOptions}. If an - * Activity Execution failed then the server would attempt to dispatch another Activity Task to - * retry the execution according to the retry options. If there were Heartbeat details reported by - * the last Activity Execution that failed, the details would be delivered along with the Activity - * Task for the next retry attempt. The Activity implementation can extract the details via {@link - * #getHeartbeatDetails(Class)}() and resume progress. + * Extracts Heartbeat details from the last heartbeat of this Activity Execution attempt. If there + * were no heartbeats in this attempt, details from the last failed attempt are returned instead. + * It is useful in combination with retry options. An Activity Execution could be scheduled with + * optional {@link io.temporal.common.RetryOptions} via {@link + * io.temporal.activity.ActivityOptions}. If an Activity Execution failed then the server would + * attempt to dispatch another Activity Task to retry the execution according to the retry + * options. If there were Heartbeat details reported by the last Activity Execution that failed, + * the details would be delivered along with the Activity Task for the next retry attempt. The + * Activity implementation can extract the details via {@link #getHeartbeatDetails(Class)}() and + * resume progress. * * @param detailsClass Class of the Heartbeat details * @param detailsGenericType Type of the Heartbeat details */ Optional getHeartbeatDetails(Class detailsClass, Type detailsGenericType); + /** + * Returns details from the last failed attempt of this Activity Execution. Unlike {@link + * #getHeartbeatDetails(Class)}, the returned details are not updated on every heartbeat call + * within the current attempt. + * + * @param detailsClass Class of the Heartbeat details + */ + Optional getLastHeartbeatDetails(Class detailsClass); + + /** + * Returns details from the last failed attempt of this Activity Execution. Unlike {@link + * #getHeartbeatDetails(Class, Type)}, the returned details are not updated on every heartbeat + * call within the current attempt. + * + * @param detailsClass Class of the Heartbeat details + * @param detailsGenericType Type of the Heartbeat details + */ + Optional getLastHeartbeatDetails(Class detailsClass, Type detailsGenericType); + /** * Gets a correlation token that can be used to complete the Activity Execution asynchronously * through {@link io.temporal.client.ActivityCompletionClient#complete(byte[], Object)}. diff --git a/temporal-sdk/src/main/java/io/temporal/common/interceptors/ActivityExecutionContextBase.java b/temporal-sdk/src/main/java/io/temporal/common/interceptors/ActivityExecutionContextBase.java index ccc105f183..73adde3784 100644 --- a/temporal-sdk/src/main/java/io/temporal/common/interceptors/ActivityExecutionContextBase.java +++ b/temporal-sdk/src/main/java/io/temporal/common/interceptors/ActivityExecutionContextBase.java @@ -37,6 +37,16 @@ public Optional getHeartbeatDetails(Class detailsClass, Type detailsGe return next.getHeartbeatDetails(detailsClass, detailsGenericType); } + @Override + public Optional getLastHeartbeatDetails(Class detailsClass) { + return next.getLastHeartbeatDetails(detailsClass); + } + + @Override + public Optional getLastHeartbeatDetails(Class detailsClass, Type detailsGenericType) { + return next.getLastHeartbeatDetails(detailsClass, detailsGenericType); + } + @Override public byte[] getTaskToken() { return next.getTaskToken(); diff --git a/temporal-sdk/src/main/java/io/temporal/internal/activity/ActivityExecutionContextImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/activity/ActivityExecutionContextImpl.java index c223596da3..101ca4c047 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/activity/ActivityExecutionContextImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/activity/ActivityExecutionContextImpl.java @@ -89,6 +89,17 @@ public Optional getHeartbeatDetails(Class detailsClass, Type detailsGe return heartbeatContext.getHeartbeatDetails(detailsClass, detailsGenericType); } + @Override + public Optional getLastHeartbeatDetails(Class detailsClass) { + return getLastHeartbeatDetails(detailsClass, detailsClass); + } + + @Override + @SuppressWarnings("unchecked") + public Optional getLastHeartbeatDetails(Class detailsClass, Type detailsGenericType) { + return heartbeatContext.getLastHeartbeatDetails(detailsClass, detailsGenericType); + } + @Override public byte[] getTaskToken() { return info.getTaskToken(); @@ -153,7 +164,7 @@ public ActivityInfo getInfo() { @Override public Object getLastHeartbeatValue() { - return heartbeatContext.getLastHeartbeatDetails(); + return heartbeatContext.getLatestHeartbeatDetails(); } @Override diff --git a/temporal-sdk/src/main/java/io/temporal/internal/activity/HeartbeatContext.java b/temporal-sdk/src/main/java/io/temporal/internal/activity/HeartbeatContext.java index 8a8fa33388..f87f3c637f 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/activity/HeartbeatContext.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/activity/HeartbeatContext.java @@ -16,7 +16,12 @@ interface HeartbeatContext { */ Optional getHeartbeatDetails(Class detailsClass, Type detailsGenericType); - Object getLastHeartbeatDetails(); + /** + * @see io.temporal.activity.ActivityExecutionContext#getLastHeartbeatDetails(Class) + */ + Optional getLastHeartbeatDetails(Class detailsClass, Type detailsGenericType); + + Object getLatestHeartbeatDetails(); /** Cancel any pending heartbeat and discard cached heartbeat details. */ void cancelOutstandingHeartbeat(); diff --git a/temporal-sdk/src/main/java/io/temporal/internal/activity/HeartbeatContextImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/activity/HeartbeatContextImpl.java index b53e943fcb..add22280fa 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/activity/HeartbeatContextImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/activity/HeartbeatContextImpl.java @@ -130,8 +130,24 @@ public Optional getHeartbeatDetails(Class detailsClass, Type detailsGe } } + /** + * @see ActivityExecutionContext#getLastHeartbeatDetails(Class, Type) + */ + @Override + @SuppressWarnings("unchecked") + public Optional getLastHeartbeatDetails(Class detailsClass, Type detailsGenericType) { + lock.lock(); + try { + return Optional.ofNullable( + dataConverterWithActivityContext.fromPayloads( + 0, prevAttemptHeartbeatDetails, detailsClass, detailsGenericType)); + } finally { + lock.unlock(); + } + } + @Override - public Object getLastHeartbeatDetails() { + public Object getLatestHeartbeatDetails() { lock.lock(); try { if (receivedAHeartbeat) { diff --git a/temporal-sdk/src/main/java/io/temporal/internal/activity/LocalActivityExecutionContextImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/activity/LocalActivityExecutionContextImpl.java index 225bf4e200..78b82135a4 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/activity/LocalActivityExecutionContextImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/activity/LocalActivityExecutionContextImpl.java @@ -42,6 +42,16 @@ public Optional getHeartbeatDetails(Class detailsClass, Type detailsGe return Optional.empty(); } + @Override + public Optional getLastHeartbeatDetails(Class detailsClass) { + return Optional.empty(); + } + + @Override + public Optional getLastHeartbeatDetails(Class detailsClass, Type detailsGenericType) { + return Optional.empty(); + } + @Override public byte[] getTaskToken() { throw new UnsupportedOperationException("getTaskToken is not supported for local activities"); diff --git a/temporal-sdk/src/test/java/io/temporal/activity/ActivityHeartbeatSentOnFailureTest.java b/temporal-sdk/src/test/java/io/temporal/activity/ActivityHeartbeatSentOnFailureTest.java index 83971efd50..57f581be7f 100644 --- a/temporal-sdk/src/test/java/io/temporal/activity/ActivityHeartbeatSentOnFailureTest.java +++ b/temporal-sdk/src/test/java/io/temporal/activity/ActivityHeartbeatSentOnFailureTest.java @@ -5,6 +5,7 @@ import io.temporal.workflow.Workflow; import io.temporal.workflow.shared.TestActivities; import io.temporal.workflow.shared.TestWorkflows; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -43,6 +44,10 @@ public static class HeartBeatingActivityImpl implements TestActivities.NoArgsAct public void execute() { // If the heartbeat details are "3", then we know that the last heartbeat was sent. if (Activity.getExecutionContext().getHeartbeatDetails(String.class).orElse("").equals("3")) { + Activity.getExecutionContext().heartbeat("1"); + // Verify that last heartbeat details don't change after a heartbeat + Assert.assertEquals( + "3", Activity.getExecutionContext().getLastHeartbeatDetails(String.class).orElse("")); return; } // Send 3 heartbeats and then fail, expecting the last heartbeat to be sent