Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
import io.temporal.internal.client.external.ManualActivityCompletionClientFactory;
import io.temporal.internal.common.PluginUtils;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import io.temporal.internal.sync.StubMarker;
import io.temporal.internal.worker.HeartbeatManager;
import io.temporal.payload.storage.ExternalStorage;
import io.temporal.serviceclient.MetricsTag;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.serviceclient.WorkflowServiceStubsPlugin;
Expand Down Expand Up @@ -56,6 +58,7 @@ final class WorkflowClientInternalImpl implements WorkflowClient, WorkflowClient
private final WorkerFactoryRegistry workerFactoryRegistry = new WorkerFactoryRegistry();
private final String workerGroupingKey = java.util.UUID.randomUUID().toString();
private final @Nullable HeartbeatManager heartbeatManager;
private final @Nullable ExternalStorageRunner externalStorageRunner;

/**
* Creates client that connects to an instance of the Temporal Service. Cannot be used from within
Expand Down Expand Up @@ -106,6 +109,9 @@ public static WorkflowClient newInstance(
.getOptions()
.getMetricsScope()
.tagged(MetricsTag.defaultTags(options.getNamespace()));
ExternalStorage externalStorage = options.getExternalStorage();
this.externalStorageRunner =
externalStorage == null ? null : ExternalStorageRunner.create(externalStorage);
this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope);
this.interceptors = options.getInterceptors();
this.workflowClientCallsInvoker = initializeClientInvoker();
Expand Down Expand Up @@ -815,6 +821,12 @@ public HeartbeatManager getHeartbeatManager() {
return heartbeatManager;
}

@Override
@Nullable
public ExternalStorageRunner getExternalStorageRunner() {
return externalStorageRunner;
}

@Override
public NexusStartWorkflowResponse startNexus(
NexusStartWorkflowRequest request, Functions.Proc workflow) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import io.temporal.common.converter.DataConverter;
import io.temporal.common.converter.GlobalDataConverter;
import io.temporal.common.interceptors.WorkflowClientInterceptor;
import io.temporal.payload.storage.ExternalStorage;
import java.lang.management.ManagementFactory;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;

/** Options for WorkflowClient configuration. */
public final class WorkflowClientOptions {
Expand Down Expand Up @@ -52,6 +54,7 @@ public static final class Builder {
private QueryRejectCondition queryRejectCondition;
private WorkflowClientPlugin[] plugins;
private Duration workerHeartbeatInterval;
private ExternalStorage externalStorage;

private Builder() {}

Expand All @@ -68,6 +71,7 @@ private Builder(WorkflowClientOptions options) {
queryRejectCondition = options.queryRejectCondition;
plugins = options.plugins;
workerHeartbeatInterval = options.workerHeartbeatInterval;
externalStorage = options.externalStorage;
}

public Builder setNamespace(String namespace) {
Expand All @@ -86,6 +90,19 @@ public Builder setDataConverter(DataConverter dataConverter) {
return this;
}

/**
* External storage configuration used to store/retrieve large payloads.
*
* <p>n.b. This is currently a no-op. External storage has not been fully integrated yet.
*
* <p>Defaults to null.
*/
@Experimental
public Builder setExternalStorage(@Nullable ExternalStorage externalStorage) {

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.

nit: Is there a way we could possibly mark this hidden from API / doc generation for now and adjust the comment to effectively say this is no-op at this time? I know we have the other PRs as follow ups to enable it, but just in case we need to pause and release Java in between. Obviously remove all of that when it does actually get attached in some usable manner.

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.

AFAIK there isn't a way to hide this but I did add a n.b. note to the doc comment saying it's a no-op until integration is complete.

this.externalStorage = externalStorage;
return this;
}

/**
* Interceptor used to intercept workflow client calls.
*
Expand Down Expand Up @@ -180,7 +197,8 @@ public WorkflowClientOptions build() {
contextPropagators,
queryRejectCondition,
plugins == null ? EMPTY_PLUGINS : plugins,
resolveHeartbeatInterval(workerHeartbeatInterval));
resolveHeartbeatInterval(workerHeartbeatInterval),
externalStorage);
}

/**
Expand All @@ -207,7 +225,8 @@ public WorkflowClientOptions validateAndBuildWithDefaults() {
? QueryRejectCondition.QUERY_REJECT_CONDITION_UNSPECIFIED
: queryRejectCondition,
plugins == null ? EMPTY_PLUGINS : plugins,
resolveHeartbeatInterval(workerHeartbeatInterval));
resolveHeartbeatInterval(workerHeartbeatInterval),
externalStorage);
}

private static Duration resolveHeartbeatInterval(Duration raw) {
Expand Down Expand Up @@ -250,6 +269,8 @@ private static Duration resolveHeartbeatInterval(Duration raw) {

private final Duration workerHeartbeatInterval;

private final @Nullable ExternalStorage externalStorage;

private WorkflowClientOptions(
String namespace,
DataConverter dataConverter,
Expand All @@ -259,7 +280,8 @@ private WorkflowClientOptions(
List<ContextPropagator> contextPropagators,
QueryRejectCondition queryRejectCondition,
WorkflowClientPlugin[] plugins,
Duration workerHeartbeatInterval) {
Duration workerHeartbeatInterval,
@Nullable ExternalStorage externalStorage) {
this.namespace = namespace;
this.dataConverter = dataConverter;
this.interceptors = interceptors;
Expand All @@ -269,6 +291,7 @@ private WorkflowClientOptions(
this.queryRejectCondition = queryRejectCondition;
this.plugins = plugins;
this.workerHeartbeatInterval = workerHeartbeatInterval;
this.externalStorage = externalStorage;
}

/**
Expand All @@ -284,6 +307,13 @@ public DataConverter getDataConverter() {
return dataConverter;
}

/** External storage used to offload large payloads or null when disabled. */
@Experimental
@Nullable
public ExternalStorage getExternalStorage() {
return externalStorage;
}

public WorkflowClientInterceptor[] getInterceptors() {
return interceptors;
}
Expand Down Expand Up @@ -359,6 +389,8 @@ public String toString() {
+ Arrays.toString(plugins)
+ ", workerHeartbeatInterval="
+ workerHeartbeatInterval
+ ", externalStorage="
+ externalStorage
+ '}';
}

Expand All @@ -376,7 +408,8 @@ public boolean equals(Object o) {
&& queryRejectCondition == that.queryRejectCondition
&& Arrays.equals(plugins, that.plugins)
&& com.google.common.base.Objects.equal(
workerHeartbeatInterval, that.workerHeartbeatInterval);
workerHeartbeatInterval, that.workerHeartbeatInterval)
&& com.google.common.base.Objects.equal(externalStorage, that.externalStorage);
}

@Override
Expand All @@ -390,6 +423,7 @@ public int hashCode() {
contextPropagators,
queryRejectCondition,
Arrays.hashCode(plugins),
workerHeartbeatInterval);
workerHeartbeatInterval,
externalStorage);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.temporal.internal.client;

import io.temporal.client.WorkflowClient;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import io.temporal.internal.worker.HeartbeatManager;
import io.temporal.worker.WorkerFactory;
import io.temporal.workflow.Functions;
Expand All @@ -25,4 +26,7 @@ public interface WorkflowClientInternal {

@Nullable
HeartbeatManager getHeartbeatManager();

@Nullable
ExternalStorageRunner getExternalStorageRunner();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package io.temporal.internal.payload.storage;

import io.temporal.api.common.v1.Payload;
import io.temporal.api.common.v1.Payloads;
import io.temporal.api.failure.v1.Failure;
import io.temporal.common.CancellationToken;
import io.temporal.common.converter.DataConverter;
import io.temporal.common.converter.DataConverterException;
import io.temporal.payload.context.SerializationContext;
import io.temporal.payload.storage.StorageDriverTargetInfo;
import java.lang.reflect.Type;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
* A {@link DataConverter} that stores/retrieves payloads to/from external storage.
*
* <p>This is an internal class that is not exposed to users or workflow code. The intent is to use
* this data converter to consolidate extstore usage within the SDK.
*/
public final class ExternalStorageDataConverter implements DataConverter {

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.

Need to implement Object[] fromPayloads(Optional<Payloads> content, Class<?>[] parameterTypes, Type[] genericParameterTypes).


private final DataConverter delegate;
private final ExternalStorageRunner externalStorage;
private final @Nullable StorageDriverTargetInfo storageTarget;

public ExternalStorageDataConverter(
@Nonnull DataConverter delegate, @Nonnull ExternalStorageRunner externalStorage) {
this(delegate, externalStorage, null);
}

private ExternalStorageDataConverter(
@Nonnull DataConverter delegate,
@Nonnull ExternalStorageRunner externalStorage,
@Nullable StorageDriverTargetInfo storageTarget) {
this.delegate = delegate;
this.externalStorage = externalStorage;
this.storageTarget = storageTarget;
}

public ExternalStorageDataConverter withStorageTarget(
@Nullable StorageDriverTargetInfo storageTarget) {
return new ExternalStorageDataConverter(delegate, externalStorage, storageTarget);
}

@Override
public <T> Optional<Payload> toPayload(T value) throws DataConverterException {
Optional<Payload> converted = delegate.toPayload(value);
if (!converted.isPresent()) {
return converted;
}
Payloads stored = store(Payloads.newBuilder().addPayloads(converted.get()).build());
return Optional.of(stored.getPayloads(0));
}

@Override
public Optional<Payloads> toPayloads(Object... values) throws DataConverterException {
Optional<Payloads> converted = delegate.toPayloads(values);
if (!converted.isPresent()) {
return converted;
}
return Optional.of(store(converted.get()));
}

@Override
public <T> T fromPayload(Payload payload, Class<T> valueClass, Type valueType)
throws DataConverterException {
return delegate.fromPayload(retrieve(payload), valueClass, valueType);
}

@Override
public <T> T fromPayloads(
int index, Optional<Payloads> content, Class<T> parameterType, Type genericParameterType)
throws DataConverterException {
if (!content.isPresent() || index >= content.get().getPayloadsCount()) {
return delegate.fromPayloads(index, content, parameterType, genericParameterType);
}
Payload resolved = retrieve(content.get().getPayloads(index));
return delegate.fromPayload(resolved, parameterType, genericParameterType);
}

@Override
public Object[] fromPayloads(
Optional<Payloads> content, Class<?>[] parameterTypes, Type[] genericParameterTypes)
throws DataConverterException {
if (!content.isPresent()) {
return delegate.fromPayloads(content, parameterTypes, genericParameterTypes);
}
return delegate.fromPayloads(
Optional.of(retrieveAll(content.get())), parameterTypes, genericParameterTypes);
}

@Override
@Nonnull
public RuntimeException failureToException(@Nonnull Failure failure) {
return delegate.failureToException(retrieveMessage(failure));
}

@Override
@Nonnull
public Failure exceptionToFailure(@Nonnull Throwable throwable) {
return storeMessage(delegate.exceptionToFailure(throwable));
}

@Override
@Nonnull
public DataConverter withContext(@Nonnull SerializationContext context) {
return new ExternalStorageDataConverter(
delegate.withContext(context), externalStorage, storageTarget);
}

private Payloads retrieveAll(Payloads payloads) {
for (Payload payload : payloads.getPayloadsList()) {
if (ExternalStorageReferences.isReference(payload)) {
return retrieveMessage(payloads);
}
}
return payloads;
}

private Payload retrieve(Payload payload) {
if (!ExternalStorageReferences.isReference(payload)) {
return payload;
}
return retrieveMessage(Payloads.newBuilder().addPayloads(payload).build()).getPayloads(0);
}

private Payloads store(Payloads payloads) {
Payloads.Builder builder = payloads.toBuilder();
externalStorage.store(builder, storageTarget, null, CancellationToken.none());

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.

nit: I think this needs to be lifted to be an instance field so that the owner of the data converter can cancel in flight operations, or maybe provided via something like a thread local so each call site can cancel. Then again, not sure if there is anything on any of the clients that would enable cancellation. Take this as a todo rather than fixing unless others object.

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.

Yea, AFAICT we don't have anything that could actually enable this cancellation right now but eventually, when we do, I think the token will need to be passed in with a withCancellationToken or something.

return builder.build();
}

private <T extends com.google.protobuf.Message> T retrieveMessage(T message) {
return externalStorage.retrieve(message, CancellationToken.none());
}

private Failure storeMessage(Failure failure) {
Failure.Builder builder = failure.toBuilder();
externalStorage.store(builder, storageTarget, null, CancellationToken.none());
return builder.build();
}
}
Loading
Loading