-
Notifications
You must be signed in to change notification settings - Fork 243
External Storage Integration: NexusWorker #3018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d201670
9bdc2d8
4841678
fe7fcc4
d877ec7
f3f6f30
1e230e5
1a798a1
be50571
2af26ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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). | ||
|
|
@@ -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. */ | ||
|
|
@@ -63,6 +68,11 @@ public DataConverter getDataConverter() { | |
| return dataConverter; | ||
| } | ||
|
|
||
| @Nullable | ||
| public ExternalStorage getExternalStorage() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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() {} | ||
|
|
||
|
|
@@ -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. */ | ||
|
|
@@ -148,14 +160,21 @@ public NexusClientOptions.Builder setIdentity(String identity) { | |
| return this; | ||
| } | ||
|
|
||
| public NexusClientOptions.Builder setExternalStorage( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
@@ -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(); | ||
|
|
@@ -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) | ||
|
|
@@ -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; | ||
|
|
@@ -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()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
@@ -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; | ||
|
|
@@ -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( | ||
| () -> | ||
|
|
@@ -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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this needs to handle |
||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some callers of |
||
| 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); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this
setExternalStorage(null)?