Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ public static WorkflowClient newInstance(
.getMetricsScope()
.tagged(MetricsTag.defaultTags(options.getNamespace()));
ExternalStorage externalStorage = options.getExternalStorage();
this.externalStorageRunner =
ExternalStorageRunner externalStorageRunner =
externalStorage == null ? null : ExternalStorageRunner.create(externalStorage);
this.externalStorageRunner = externalStorageRunner;
this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope);
this.interceptors = options.getInterceptors();
this.workflowClientCallsInvoker = initializeClientInvoker();
Expand All @@ -120,7 +121,8 @@ public static WorkflowClient newInstance(
workflowServiceStubs,
options.getNamespace(),
options.getIdentity(),
options.getDataConverter());
options.getDataConverter(),
externalStorageRunner);

java.time.Duration heartbeatInterval = options.getWorkerHeartbeatInterval();
if (!heartbeatInterval.isNegative()) {
Expand All @@ -133,7 +135,8 @@ public static WorkflowClient newInstance(

private WorkflowClientCallsInterceptor initializeClientInvoker() {
WorkflowClientCallsInterceptor workflowClientInvoker =
new RootWorkflowClientInvoker(genericClient, options, workerFactoryRegistry);
new RootWorkflowClientInvoker(
genericClient, options, workerFactoryRegistry, externalStorageRunner);
for (WorkflowClientInterceptor clientInterceptor : interceptors) {
workflowClientInvoker =
clientInterceptor.workflowClientCallsInterceptor(workflowClientInvoker);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.temporal.client;

import io.temporal.api.common.v1.Payload;
import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse;
import io.temporal.common.converter.DataConverter;
import io.temporal.payload.context.WorkflowSerializationContext;
Expand Down Expand Up @@ -29,15 +30,13 @@ public String getStaticSummary() {
if (!response.getExecutionConfig().getUserMetadata().hasSummary()) {
return null;
}
Payload summary = response.getExecutionConfig().getUserMetadata().getSummary();
return dataConverter
.withContext(
new WorkflowSerializationContext(
response.getWorkflowExecutionInfo().getParentNamespaceId(),
response.getWorkflowExecutionInfo().getExecution().getWorkflowId()))
.fromPayload(
response.getExecutionConfig().getUserMetadata().getSummary(),
String.class,
String.class);
.fromPayload(summary, String.class, String.class);
}

/**
Expand All @@ -51,15 +50,13 @@ public String getStaticDetails() {
if (!response.getExecutionConfig().getUserMetadata().hasDetails()) {
return null;
}
Payload details = response.getExecutionConfig().getUserMetadata().getDetails();
return dataConverter
.withContext(
new WorkflowSerializationContext(
response.getWorkflowExecutionInfo().getParentNamespaceId(),
response.getWorkflowExecutionInfo().getExecution().getWorkflowId()))
.fromPayload(
response.getExecutionConfig().getUserMetadata().getDetails(),
String.class,
String.class);
.fromPayload(details, String.class, String.class);
}

/** Returns the raw response from the Temporal service. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import io.temporal.client.WorkflowClient;
import io.temporal.common.converter.DataConverter;
import io.temporal.internal.client.external.ManualActivityCompletionClientFactory;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;

public class ActivityExecutionContextFactoryImpl implements ActivityExecutionContextFactory {
private final WorkflowClient client;
Expand All @@ -21,6 +23,7 @@ public class ActivityExecutionContextFactoryImpl implements ActivityExecutionCon
private final DataConverter dataConverter;
private final ScheduledExecutorService heartbeatExecutor;
private final ManualActivityCompletionClientFactory manualCompletionClientFactory;
private final @Nullable ExternalStorageRunner externalStorage;
private final ConcurrentMap<ByteBuffer, ActivityExecutionContextImpl> activeContexts =
new ConcurrentHashMap<>();

Expand All @@ -31,7 +34,8 @@ public ActivityExecutionContextFactoryImpl(
Duration maxHeartbeatThrottleInterval,
Duration defaultHeartbeatThrottleInterval,
DataConverter dataConverter,
ScheduledExecutorService heartbeatExecutor) {
ScheduledExecutorService heartbeatExecutor,
@Nullable ExternalStorageRunner externalStorage) {
this.client = Objects.requireNonNull(client);
this.identity = identity;
this.namespace = Objects.requireNonNull(namespace);
Expand All @@ -40,9 +44,10 @@ public ActivityExecutionContextFactoryImpl(
Objects.requireNonNull(defaultHeartbeatThrottleInterval);
this.dataConverter = Objects.requireNonNull(dataConverter);
this.heartbeatExecutor = Objects.requireNonNull(heartbeatExecutor);
this.externalStorage = externalStorage;
this.manualCompletionClientFactory =
ManualActivityCompletionClientFactory.newFactory(
client.getWorkflowServiceStubs(), namespace, identity, dataConverter);
client.getWorkflowServiceStubs(), namespace, identity, dataConverter, externalStorage);
}

@Override
Expand All @@ -63,7 +68,8 @@ public InternalActivityExecutionContext createContext(
identity,
maxHeartbeatThrottleInterval,
defaultHeartbeatThrottleInterval,
() -> cleanupContext(info.getTaskToken(), false));
() -> cleanupContext(info.getTaskToken(), false),
externalStorage);
activeContexts.put(taskToken, context);
return context;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.temporal.common.CancellationToken;
import io.temporal.common.converter.DataConverter;
import io.temporal.internal.client.external.ManualActivityCompletionClientFactory;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import io.temporal.payload.context.ActivitySerializationContext;
import io.temporal.workflow.Functions;
import java.lang.reflect.Type;
Expand All @@ -18,6 +19,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

/**
Expand Down Expand Up @@ -55,7 +57,8 @@ class ActivityExecutionContextImpl implements InternalActivityExecutionContext {
String identity,
Duration maxHeartbeatThrottleInterval,
Duration defaultHeartbeatThrottleInterval,
Functions.Proc closeCallback) {
Functions.Proc closeCallback,
@Nullable ExternalStorageRunner externalStorage) {
this.client = client;
this.activity = activity;
this.metricsScope = metricsScope;
Expand All @@ -73,7 +76,8 @@ class ActivityExecutionContextImpl implements InternalActivityExecutionContext {
metricsScope,
identity,
maxHeartbeatThrottleInterval,
defaultHeartbeatThrottleInterval);
defaultHeartbeatThrottleInterval,
externalStorage);
}

/**
Expand Down Expand Up @@ -155,7 +159,10 @@ public ManualActivityCompletionClient useLocalManualCompletion() {
new ActivitySerializationContext(info);
return new CompletionAwareManualCompletionClient(
manualCompletionClientFactory.getClient(
info.getTaskToken(), metricsScope, activitySerializationContext),
info.getTaskToken(),
metricsScope,
activitySerializationContext,
HeartbeatContextImpl.storageTargetForActivity(info.getNamespace(), info)),
completionHandle);
} finally {
lock.unlock();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
package io.temporal.internal.activity;

import com.google.common.base.Strings;
import com.google.protobuf.ByteString;
import com.uber.m3.tally.Scope;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.temporal.activity.ActivityExecutionContext;
import io.temporal.activity.ActivityInfo;
import io.temporal.api.common.v1.Payloads;
import io.temporal.api.enums.v1.TimeoutType;
import io.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest;
import io.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatResponse;
import io.temporal.client.*;
import io.temporal.common.CancellationToken;
import io.temporal.common.converter.DataConverter;
import io.temporal.failure.TimeoutFailure;
import io.temporal.internal.client.ActivityClientHelper;
import io.temporal.internal.concurrent.structured.CancelSource;
import io.temporal.internal.payload.storage.ActivityStorageTargets;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import io.temporal.payload.context.ActivitySerializationContext;
import io.temporal.payload.storage.StorageDriverTargetInfo;
import io.temporal.serviceclient.WorkflowServiceStubs;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -58,6 +66,7 @@ static long getLocalHeartbeatTimeoutBufferMillis() {
private final long heartbeatIntervalMillis;
private final DataConverter dataConverter;
private final DataConverter dataConverterWithActivityContext;
private final @Nullable ExternalStorageRunner externalStorage;

private final Scope metricsScope;
private final Optional<Payloads> prevAttemptHeartbeatDetails;
Expand Down Expand Up @@ -89,7 +98,8 @@ public HeartbeatContextImpl(
Scope metricsScope,
String identity,
Duration maxHeartbeatThrottleInterval,
Duration defaultHeartbeatThrottleInterval) {
Duration defaultHeartbeatThrottleInterval,
@Nullable ExternalStorageRunner externalStorage) {
this(
service,
namespace,
Expand All @@ -100,6 +110,7 @@ public HeartbeatContextImpl(
identity,
maxHeartbeatThrottleInterval,
defaultHeartbeatThrottleInterval,
externalStorage,
getLocalHeartbeatTimeoutBufferMillis());
}

Expand All @@ -113,10 +124,12 @@ public HeartbeatContextImpl(
String identity,
Duration maxHeartbeatThrottleInterval,
Duration defaultHeartbeatThrottleInterval,
@Nullable ExternalStorageRunner externalStorage,
long localHeartbeatTimeoutBufferMillis) {
this.service = service;
this.metricsScope = metricsScope;
this.dataConverter = dataConverter;
this.externalStorage = externalStorage;
this.dataConverterWithActivityContext =
dataConverter.withContext(
new ActivitySerializationContext(
Expand Down Expand Up @@ -330,16 +343,59 @@ private void checkHeartbeatTimeoutDeadlineLocked() {
}
}

private StorageDriverTargetInfo activityStorageTarget() {
return storageTargetForActivity(namespace, info);
}

/**
* Standalone activities target the activity; workflow activities target their workflow, matching
* where {@link io.temporal.internal.worker.ActivityWorker} stores the activity task payloads. A
* non-empty {@code activityRunId} marks a standalone activity.
*/
static StorageDriverTargetInfo storageTargetForActivity(String namespace, ActivityInfo info) {
return ActivityStorageTargets.newBuilder(namespace)
.setActivity(info.getActivityId(), info.getActivityRunId(), info.getActivityType())
.setWorkflow(
Strings.emptyToNull(info.getWorkflowId()),
Strings.emptyToNull(info.getWorkflowRunId()),
info.getWorkflowType())
.build();
}

/**
* Offloads large heartbeat payloads aborting if the store call runs longer than the heartbeat
* interval or if the activity is cancelled.
*/
private void offloadHeartbeat(RecordActivityTaskHeartbeatRequest.Builder builder) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this runs under the heartbeat lock. And the cancellation below looks to be relying on the heartbeat interval, which has a default of 30 seconds. This is could also starve out other operations like getHeartbeatDetails as they queue up. Also, if too many activities get stuck on external storage during heart beating, it might exhaust the thread pool and activity cancels may not run.

CancelSource<CancellationException> offloadCancel =
new CancelSource<>(CancellationException::new);
ScheduledFuture<?> timeout =
heartbeatExecutor.schedule(
(Runnable) offloadCancel::cancel, heartbeatIntervalMillis, TimeUnit.MILLISECONDS);
CancellationToken.Registration onActivityCancel =
cancellationSource.token().onCancel(offloadCancel::cancel);
try {
externalStorage.store(builder, activityStorageTarget(), null, offloadCancel.token());
} finally {
timeout.cancel(false);
onActivityCancel.close();
}
}

private void sendHeartbeatRequest(Object details) {
try {
RecordActivityTaskHeartbeatRequest.Builder builder =
RecordActivityTaskHeartbeatRequest.newBuilder()
.setTaskToken(ByteString.copyFrom(info.getTaskToken()))
.setNamespace(namespace)
.setIdentity(identity);
dataConverterWithActivityContext.toPayloads(details).ifPresent(builder::setDetails);
if (externalStorage != null) {
offloadHeartbeat(builder);
}
RecordActivityTaskHeartbeatRequest request = builder.build();
RecordActivityTaskHeartbeatResponse status =
ActivityClientHelper.sendHeartbeatRequest(
service,
namespace,
identity,
info.getTaskToken(),
dataConverterWithActivityContext.toPayloads(details),
metricsScope);
ActivityClientHelper.sendHeartbeatRequest(service, request, metricsScope);
if (status.getCancelRequested()) {
requestCancelLocked();
} else if (status.getActivityReset()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@

import static io.temporal.serviceclient.MetricsTag.METRICS_TAGS_CALL_OPTIONS_KEY;

import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.uber.m3.tally.Scope;
import io.temporal.activity.ManualActivityCompletionClient;
import io.temporal.api.common.v1.Payloads;
import io.temporal.api.common.v1.WorkflowExecution;
import io.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdRequest;
import io.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatByIdResponse;
import io.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatRequest;
import io.temporal.api.workflowservice.v1.RecordActivityTaskHeartbeatResponse;
import io.temporal.serviceclient.WorkflowServiceStubs;
import java.util.Optional;
import javax.annotation.Nonnull;

/**
* Contains methods that could but didn't become a part of the main {@link
Expand All @@ -26,43 +20,21 @@ private ActivityClientHelper() {}

public static RecordActivityTaskHeartbeatResponse sendHeartbeatRequest(
WorkflowServiceStubs service,
String namespace,
String identity,
byte[] taskToken,
Optional<Payloads> payloads,
RecordActivityTaskHeartbeatRequest request,
Scope metricsScope) {
RecordActivityTaskHeartbeatRequest.Builder request =
RecordActivityTaskHeartbeatRequest.newBuilder()
.setTaskToken(ByteString.copyFrom(taskToken))
.setNamespace(namespace)
.setIdentity(identity);
payloads.ifPresent(request::setDetails);
return service
.blockingStub()
.withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope)
.recordActivityTaskHeartbeat(request.build());
.recordActivityTaskHeartbeat(request);
}

public static RecordActivityTaskHeartbeatByIdResponse recordActivityTaskHeartbeatById(
WorkflowServiceStubs service,
String namespace,
String identity,
WorkflowExecution execution,
@Nonnull String activityId,
Optional<Payloads> payloads,
RecordActivityTaskHeartbeatByIdRequest request,
Scope metricsScope) {
Preconditions.checkNotNull(activityId, "Either activity id or task token are required");
RecordActivityTaskHeartbeatByIdRequest.Builder request =
RecordActivityTaskHeartbeatByIdRequest.newBuilder()
.setRunId(execution.getRunId())
.setWorkflowId(execution.getWorkflowId())
.setActivityId(activityId)
.setNamespace(namespace)
.setIdentity(identity);
payloads.ifPresent(request::setDetails);
return service
.blockingStub()
.withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope)
.recordActivityTaskHeartbeatById(request.build());
.recordActivityTaskHeartbeatById(request);
}
}
Loading
Loading