Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
import io.temporal.internal.client.RootNexusClientInvoker;
import io.temporal.internal.client.external.GenericWorkflowClient;
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
import io.temporal.internal.payload.storage.ExternalStorageDataConverter;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import io.temporal.payload.storage.ExternalStorage;
import io.temporal.serviceclient.MetricsTag;
import io.temporal.serviceclient.WorkflowServiceStubs;
import java.util.List;
Expand Down Expand Up @@ -46,6 +49,16 @@ public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientO
workflowServiceStubs =
new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace());
this.workflowServiceStubs = workflowServiceStubs;
ExternalStorage externalStorageConfig = options.getExternalStorage();
if (externalStorageConfig != null) {
options =
NexusClientOptions.newBuilder(options)

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.

Should this setExternalStorage(null)?

.setDataConverter(
new ExternalStorageDataConverter(
options.getDataConverter(),
ExternalStorageRunner.create(externalStorageConfig)))
.build();
}
this.options = options;
this.metricsScope =
workflowServiceStubs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import io.temporal.common.converter.DataConverter;
import io.temporal.common.converter.GlobalDataConverter;
import io.temporal.common.interceptors.NexusClientInterceptor;
import io.temporal.payload.storage.ExternalStorage;
import java.lang.management.ManagementFactory;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;

/**
* Options that configure a {@link NexusClient} (and the service-bound clients it produces).
Expand Down Expand Up @@ -36,16 +38,19 @@ public class NexusClientOptions {
private final List<NexusClientInterceptor> interceptors;
private final DataConverter dataConverter;
private final String identity;
private final @Nullable ExternalStorage externalStorage;

private NexusClientOptions(
String namespace,
List<NexusClientInterceptor> interceptors,
DataConverter dataConverter,
String identity) {
String identity,
@Nullable ExternalStorage externalStorage) {
this.namespace = namespace;
this.interceptors = interceptors;
this.dataConverter = dataConverter;
this.identity = identity;
this.externalStorage = externalStorage;
}

/** Get the namespace this client will operate on. */
Expand All @@ -63,6 +68,11 @@ public DataConverter getDataConverter() {
return dataConverter;
}

@Nullable
public ExternalStorage getExternalStorage() {

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.

Needs javadoc

return externalStorage;
}

/**
* Human-readable identity of this client. Stamped onto outgoing write requests (start, cancel,
* terminate) so server-side history and audit trails can attribute the action to a caller.
Expand Down Expand Up @@ -101,6 +111,7 @@ public static class Builder {
private List<NexusClientInterceptor> interceptors = Collections.emptyList();
private DataConverter dataConverter = GlobalDataConverter.get();
private String identity;
private ExternalStorage externalStorage;

private Builder() {}

Expand All @@ -112,6 +123,7 @@ private Builder(NexusClientOptions options) {
interceptors = options.interceptors;
dataConverter = options.dataConverter;
identity = options.identity;
externalStorage = options.externalStorage;
}

/** Set the namespace this client will operate on. */
Expand Down Expand Up @@ -148,14 +160,21 @@ public NexusClientOptions.Builder setIdentity(String identity) {
return this;
}

public NexusClientOptions.Builder setExternalStorage(

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.

Needs javadoc

@Nullable ExternalStorage externalStorage) {
this.externalStorage = externalStorage;
return this;
}

public NexusClientOptions build() {
String resolvedIdentity =
identity == null ? ManagementFactory.getRuntimeMXBean().getName() : identity;
return new NexusClientOptions(
namespace == null ? DEFAULT_NAMESPACE : namespace,
interceptors,
dataConverter,
resolvedIdentity);
resolvedIdentity,
externalStorage);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static io.temporal.serviceclient.MetricsTag.TASK_FAILURE_TYPE;

import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import com.uber.m3.tally.Scope;
import com.uber.m3.tally.Stopwatch;
import com.uber.m3.util.Duration;
Expand All @@ -16,7 +17,9 @@
import io.temporal.common.converter.DataConverter;
import io.temporal.internal.common.NexusUtil;
import io.temporal.internal.common.ProtobufTimeUtils;
import io.temporal.internal.concurrent.structured.CancelSource;
import io.temporal.internal.logging.LoggerTag;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import io.temporal.internal.retryer.GrpcRetryer;
import io.temporal.serviceclient.MetricsTag;
import io.temporal.serviceclient.WorkflowServiceStubs;
Expand All @@ -27,6 +30,7 @@
import io.temporal.worker.tuning.PollerBehaviorAutoscaling;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
Expand All @@ -53,6 +57,9 @@ final class NexusWorker implements SuspendableWorker {
private final GrpcRetryer.GrpcRetryerOptions replyGrpcRetryerOptions;
private final TrackingSlotSupplier<NexusSlotInfo> slotSupplier;
private final NamespaceCapabilities namespaceCapabilities;

final CancelSource<CancellationException> storageCancellation =
new CancelSource<>(() -> new CancellationException("Worker shutdown"));
private final boolean forceOldFailureFormat;
private final boolean workerCommandsTaskQueue;
private final TaskCounter taskCounter = new TaskCounter();
Expand Down Expand Up @@ -182,6 +189,9 @@ public boolean start() {

@Override
public CompletableFuture<Void> shutdown(ShutdownManager shutdownManager, boolean interruptTasks) {
if (interruptTasks) {
storageCancellation.cancel();
}
String supplierName = this + "#executorSlots";
return poller
.shutdown(shutdownManager, interruptTasks)
Expand Down Expand Up @@ -274,6 +284,12 @@ public String toString() {
options.getIdentity(), namespace, taskQueue);
}

private static final class ExternalStorageTaskFailure extends RuntimeException {
ExternalStorageTaskFailure(String message, Throwable cause) {
super(message, cause);
}
}

private class TaskHandlerImpl implements PollTaskExecutor.TaskHandler<NexusTask> {

final NexusTaskHandler handler;
Expand Down Expand Up @@ -304,28 +320,38 @@ private String getNexusTaskOperation(PollNexusTaskQueueResponseOrBuilder pollRes

@Override
public void handle(NexusTask task) {
PollNexusTaskQueueResponseOrBuilder pollResponse = task.getResponse();
// Extract service and operation from the request and set them as MDC and metrics
// scope tags. If the request does not have a service or operation, do not set the tags.
// If we don't know how to handle the task, we will fail the task further down the line.
Scope metricsScope = workerMetricsScope;
String service = getNexusTaskService(pollResponse);
if (!service.isEmpty()) {
MDC.put(LoggerTag.NEXUS_SERVICE, service);
metricsScope = metricsScope.tagged(ImmutableMap.of(MetricsTag.NEXUS_SERVICE, service));
}
String operation = getNexusTaskOperation(pollResponse);
if (!operation.isEmpty()) {
MDC.put(LoggerTag.NEXUS_OPERATION, operation);
metricsScope = metricsScope.tagged(ImmutableMap.of(MetricsTag.NEXUS_OPERATION, operation));
}
slotSupplier.markSlotUsed(
new NexusSlotInfo(
service, operation, taskQueue, options.getIdentity(), options.getBuildId()),
task.getPermit());

boolean taskFailed = false;
try {
PollNexusTaskQueueResponseOrBuilder pollResponse = task.getResponse();
// Extract service and operation from the request and set them as MDC and metrics
// scope tags. If the request does not have a service or operation, do not set the tags.
// If we don't know how to handle the task, we will fail the task further down the line.
Scope metricsScope = workerMetricsScope;
String service = getNexusTaskService(pollResponse);
if (!service.isEmpty()) {
MDC.put(LoggerTag.NEXUS_SERVICE, service);
metricsScope = metricsScope.tagged(ImmutableMap.of(MetricsTag.NEXUS_SERVICE, service));
}
String operation = getNexusTaskOperation(pollResponse);
if (!operation.isEmpty()) {
MDC.put(LoggerTag.NEXUS_OPERATION, operation);
metricsScope =
metricsScope.tagged(ImmutableMap.of(MetricsTag.NEXUS_OPERATION, operation));
}
slotSupplier.markSlotUsed(
new NexusSlotInfo(
service, operation, taskQueue, options.getIdentity(), options.getBuildId()),
task.getPermit());

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.

This code and above used to be outside of the try block. Not sure it needs to be inside. It might incidentally fix some existing problems. But its not the focus of this PR. If it doesn't need to be in the try to enable external storage, then let's move it back. If it is fixing something separately, then open a different PR for it.


try {
task = retrieveInboundPayloads(task);
} catch (Throwable e) {
taskFailed = true;
sendStorageFailure(

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.

This is going to log "External storage failed for a nexus task" but external storage is not configured, so the log is misleading.

pollResponse.getTaskToken(), supportsTemporalFailure(pollResponse), metricsScope, e);
return;
}

taskFailed = handleNexusTask(task, metricsScope);
} catch (Throwable e) {
taskFailed = true;
Expand Down Expand Up @@ -416,14 +442,11 @@ private boolean handleNexusTask(NexusTask task, Scope metricsScope) {
}

try {
// Check if the server supports using the Failure directly in responses
boolean supportTemporalFailure =
task.getResponse().getRequest().getCapabilities().getTemporalFailureResponses();
if (forceOldFailureFormat) {
supportTemporalFailure = false;
}

sendReply(taskToken, supportTemporalFailure, result, metricsScope);
sendReply(taskToken, supportsTemporalFailure(pollResponse), result, metricsScope);
} catch (ExternalStorageTaskFailure e) {
sendStorageFailure(
taskToken, supportsTemporalFailure(pollResponse), metricsScope, e.getCause());
return true;
} catch (Exception e) {
logExceptionDuringResultReporting(e, pollResponse, result);
throw e;
Expand Down Expand Up @@ -484,13 +507,14 @@ private void sendReply(
if (!supportTemporalFailure && taskResponse.getStartOperation().hasFailure()) {
taskResponse = getResponseForOldServer(taskResponse);
}
RespondNexusTaskCompletedRequest request =
RespondNexusTaskCompletedRequest.Builder requestBuilder =
RespondNexusTaskCompletedRequest.newBuilder()
.setTaskToken(taskToken)
.setIdentity(options.getIdentity())
.setNamespace(namespace)
.setResponse(taskResponse)
.build();
.setResponse(taskResponse);
storeOutbound(requestBuilder);
RespondNexusTaskCompletedRequest request = requestBuilder.build();

grpcRetryer.retry(
() ->
Expand All @@ -512,17 +536,71 @@ private void sendReply(
} else {
request.setError(NexusUtil.handlerErrorToNexusError(handlerException, dataConverter));
}
storeOutbound(request);
RespondNexusTaskFailedRequest failedRequest = request.build();
grpcRetryer.retry(
() ->
service
.blockingStub()
.withOption(METRICS_TAGS_CALL_OPTIONS_KEY, metricsScope)
.respondNexusTaskFailed(request.build()),
.respondNexusTaskFailed(failedRequest),
replyGrpcRetryerOptions);
} else {
throw new IllegalArgumentException("[BUG] Either response or failure must be set");
}
}
}

private boolean supportsTemporalFailure(PollNexusTaskQueueResponseOrBuilder pollResponse) {
return !forceOldFailureFormat
&& pollResponse.getRequest().getCapabilities().getTemporalFailureResponses();
}

private void sendStorageFailure(
ByteString taskToken, boolean supportTemporalFailure, Scope metricsScope, Throwable e) {
log.warn("External storage failed for a nexus task", e);
metricsScope
.tagged(
Collections.singletonMap(
TASK_FAILURE_TYPE, MetricsTag.TASK_FAILURE_VALUE_HANDLER_ERROR_INTERNAL))
.counter(MetricsType.NEXUS_EXEC_FAILED_COUNTER)
.inc(1);
HandlerException handlerException =
new HandlerException(HandlerException.ErrorType.INTERNAL, "External storage failed", e);
sendReply(

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 think this needs to handle ExternalStorageTaskFailure in case the failure cannot be externally stored and then submit a minimal NexusTaskHandler.Result from a small exception instance.

taskToken,
supportTemporalFailure,
new NexusTaskHandler.Result(handlerException),
metricsScope);
}

private NexusTask retrieveInboundPayloads(NexusTask task) {
ExternalStorageRunner externalStorageRunner = options.getExternalStorageRunner();
PollNexusTaskQueueResponseOrBuilder response = task.getResponse();
PollNexusTaskQueueResponse built =
response instanceof PollNexusTaskQueueResponse
? (PollNexusTaskQueueResponse) response
: ((PollNexusTaskQueueResponse.Builder) response).build();
if (externalStorageRunner == null) {
ExternalStorageRunner.throwIfContainsReference(built);
return task;
}
return new NexusTask(
externalStorageRunner.retrieve(built, storageCancellation.token()),
task.getPermit(),
task.getCompletionCallback());
}

private void storeOutbound(Message.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.

Why does this storeOutbound work differently then this one ? Should we just make one common helper ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Some callers of storeOutbound have already built messages and some have builders. Some also need a target (ActivityWorker) and others also need a visitor (the WorkflowWorker one). We could create a MessageOrBuilder type for this (maybe there already is one) and do some refactoring. Personally, I'd like that to be a follow-up if that is the direction we want to head in.

ExternalStorageRunner externalStorageRunner = options.getExternalStorageRunner();
if (externalStorageRunner == null) {
return;
}
try {
externalStorageRunner.store(builder, null, null, storageCancellation.token());
} catch (Throwable e) {
throw new ExternalStorageTaskFailure("External storage store failed", e);
}
}
}
}
Loading
Loading