Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ to docs, or any other relevant information.
## [Unreleased]

### Added
- Applications can use the experimental `@TransferTypeConvertible` annotation and
`TransferTypeConverter` interface to convert top-level model values to transfer types. The SDK
applies this conversion to workflow, activity, Nexus, and schedule payloads, including failure
details. `WorkflowClient.getOptions().getDataConverter()` returns the transfer-aware converter
that the client uses.
- Added experimental `ChildWorkflowOptions.Builder.setVersioningOverride` and
`VersioningOverride.OneTimeVersioningOverride` for explicit pinned, auto-upgrade, and one-time
child workflow routing. Invalid child overrides are reported as `InvalidVersioningOverrideFailure`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import com.uber.m3.tally.Scope;
import io.temporal.api.common.v1.Payload;
import io.temporal.common.context.ContextPropagator;
import io.temporal.common.converter.DataConverter;
import io.temporal.common.interceptors.ActivityClientCallsInterceptor;
import io.temporal.common.interceptors.ActivityClientInterceptor;
import io.temporal.common.interceptors.Header;
import io.temporal.internal.client.ActivityClientInternal;
import io.temporal.internal.client.ActivityHandleImpl;
import io.temporal.internal.client.ClientDataConverterFactory;
import io.temporal.internal.client.RootActivityClientInvoker;
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
import io.temporal.internal.client.external.ManualActivityCompletionClientFactory;
Expand Down Expand Up @@ -37,20 +39,25 @@ class ActivityClientImpl implements ActivityClient, ActivityClientInternal {
private final Scope metricsScope;

ActivityClientImpl(WorkflowServiceStubs stubs, ActivityClientOptions options) {
DataConverter internalDataConverter =
ClientDataConverterFactory.forClient(options.getDataConverter());
this.stubs = stubs;
this.options = options;
this.metricsScope =
stubs.getOptions().getMetricsScope().tagged(MetricsTag.defaultTags(options.getNamespace()));
GenericWorkflowClientImpl genericClient = new GenericWorkflowClientImpl(stubs, metricsScope);
this.invoker = initializeClientInvoker(genericClient, options);
this.invoker = initializeClientInvoker(genericClient, options, internalDataConverter);
this.manualActivityCompletionClientFactory =
ManualActivityCompletionClientFactory.newFactory(
stubs, options.getNamespace(), options.getIdentity(), options.getDataConverter());
stubs, options.getNamespace(), options.getIdentity(), internalDataConverter);
}

private static ActivityClientCallsInterceptor initializeClientInvoker(
GenericWorkflowClientImpl genericClient, ActivityClientOptions options) {
ActivityClientCallsInterceptor invoker = new RootActivityClientInvoker(genericClient, options);
GenericWorkflowClientImpl genericClient,
ActivityClientOptions options,
DataConverter dataConverter) {
ActivityClientCallsInterceptor invoker =
new RootActivityClientInvoker(genericClient, options, dataConverter);
for (ActivityClientInterceptor interceptor : options.getInterceptors()) {
invoker = interceptor.activityClientCallsInterceptor(invoker);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public <V> void reportCancellationStandalone(

@Override
public <V> void heartbeat(byte[] taskToken, V details) throws ActivityCompletionException {
factory.getClient(taskToken, metricsScope).recordHeartbeat(details);
factory.getClient(taskToken, metricsScope, serializationContext).recordHeartbeat(details);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
import io.temporal.common.interceptors.NexusClientCallsInterceptor.ListNexusOperationExecutionsOutput;
import io.temporal.common.interceptors.NexusClientInterceptor;
import io.temporal.internal.WorkflowThreadMarker;
import io.temporal.internal.client.ClientDataConverterFactory;
import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs;
import io.temporal.internal.client.NexusClientResolvedOptions;
import io.temporal.internal.client.NexusOperationHandleImpl;
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.ExternalStorageRunner;
import io.temporal.serviceclient.MetricsTag;
import io.temporal.serviceclient.WorkflowServiceStubs;
import java.util.List;
Expand All @@ -40,10 +42,17 @@ public class NexusClientImpl implements NexusClient {
public static NexusClient newInstance(WorkflowServiceStubs service, NexusClientOptions options) {
enforceNonWorkflowThread();
return WorkflowThreadMarker.protectFromWorkflowThread(
new NexusClientImpl(service, options.toResolvedOptions()), NexusClient.class);
new NexusClientImpl(service, options), NexusClient.class);
}

NexusClientImpl(WorkflowServiceStubs workflowServiceStubs, NexusClientResolvedOptions options) {
NexusClientImpl(WorkflowServiceStubs workflowServiceStubs, NexusClientOptions clientOptions) {
NexusClientResolvedOptions options =
clientOptions.toResolvedOptions(
clientOptions.getExternalStorage() == null
? ClientDataConverterFactory.forClient(clientOptions.getDataConverter())
: ClientDataConverterFactory.forClient(
clientOptions.getDataConverter(),
ExternalStorageRunner.create(clientOptions.getExternalStorage())));
workflowServiceStubs =
new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace());
this.workflowServiceStubs = workflowServiceStubs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import io.temporal.common.converter.GlobalDataConverter;
import io.temporal.common.interceptors.NexusClientInterceptor;
import io.temporal.internal.client.NexusClientResolvedOptions;
import io.temporal.internal.payload.storage.ExternalStorageDataConverter;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import io.temporal.payload.storage.ExternalStorage;
import java.lang.management.ManagementFactory;
import java.util.Collections;
Expand Down Expand Up @@ -92,16 +90,11 @@ public String getIdentity() {
* Converts this {@link NexusClientOptions} instance into a {@link NexusClientResolvedOptions}
* instance, which contains the fully resolved runtime settings used by the internal Nexus client.
*
* @param internalDataConverter converter assembled for internal client use
* @return a {@link NexusClientResolvedOptions} instance with the resolved options
*/
NexusClientResolvedOptions toResolvedOptions() {
DataConverter resolvedDataConverter = dataConverter;
if (externalStorage != null) {
resolvedDataConverter =
new ExternalStorageDataConverter(
resolvedDataConverter, ExternalStorageRunner.create(externalStorage));
}
return new NexusClientResolvedOptions(namespace, interceptors, resolvedDataConverter, identity);
NexusClientResolvedOptions toResolvedOptions(DataConverter internalDataConverter) {
return new NexusClientResolvedOptions(namespace, interceptors, internalDataConverter, identity);
}

/** Returns a fresh builder. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
import io.temporal.api.workflowservice.v1.*;
import io.temporal.client.WorkflowInvocationHandler.InvocationType;
import io.temporal.common.WorkflowExecutionHistory;
import io.temporal.common.converter.DataConverter;
import io.temporal.common.interceptors.WorkflowClientCallsInterceptor;
import io.temporal.common.interceptors.WorkflowClientInterceptor;
import io.temporal.internal.WorkflowThreadMarker;
import io.temporal.internal.client.*;
import io.temporal.internal.client.ClientDataConverterFactory;
import io.temporal.internal.client.NexusStartWorkflowResponse;
import io.temporal.internal.client.external.GenericWorkflowClient;
import io.temporal.internal.client.external.GenericWorkflowClientImpl;
Expand Down Expand Up @@ -52,6 +54,7 @@ final class WorkflowClientInternalImpl implements WorkflowClient, WorkflowClient

private final GenericWorkflowClient genericClient;
private final WorkflowClientOptions options;
private final DataConverter internalDataConverter;
private final ManualActivityCompletionClientFactory manualActivityCompletionClientFactory;
private final WorkflowClientCallsInterceptor workflowClientCallsInvoker;
private final WorkflowServiceStubs workflowServiceStubs;
Expand Down Expand Up @@ -103,9 +106,11 @@ public static WorkflowClient newInstance(
// Set merged plugins after configuration, then validate
builder.setPlugins(mergedPlugins);
options = builder.validateAndBuildWithDefaults();
this.internalDataConverter = ClientDataConverterFactory.forClient(options.getDataConverter());
this.options =
WorkflowClientOptions.newBuilder(options).setDataConverter(internalDataConverter).build();
workflowServiceStubs =
new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace());
this.options = options;
this.workflowServiceStubs = workflowServiceStubs;
this.metricsScope =
workflowServiceStubs
Expand All @@ -124,7 +129,7 @@ public static WorkflowClient newInstance(
workflowServiceStubs,
options.getNamespace(),
options.getIdentity(),
options.getDataConverter(),
getInternalDataConverter(),
externalStorageRunner);

java.time.Duration heartbeatInterval = options.getWorkerHeartbeatInterval();
Expand Down Expand Up @@ -160,13 +165,18 @@ public WorkflowClientOptions getOptions() {
return options;
}

@Override
public DataConverter getInternalDataConverter() {
return internalDataConverter;
}

@Override
@SuppressWarnings("unchecked")
public <T> T newWorkflowStub(Class<T> workflowInterface, WorkflowOptions options) {
checkAnnotation(workflowInterface, WorkflowMethod.class);
WorkflowInvocationHandler invocationHandler =
new WorkflowInvocationHandler(
workflowInterface, this.getOptions(), workflowClientCallsInvoker, options);
workflowInterface, this.options, workflowClientCallsInvoker, options);
return (T)
Proxy.newProxyInstance(
workflowInterface.getClassLoader(),
Expand Down Expand Up @@ -243,7 +253,7 @@ public <T> T newWorkflowStub(
WorkflowInvocationHandler invocationHandler =
new WorkflowInvocationHandler(
workflowInterface,
this.getOptions(),
options,
workflowClientCallsInvoker,
execution.build(),
legacyTargeting,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import static io.temporal.internal.WorkflowThreadMarker.enforceNonWorkflowThread;

import com.uber.m3.tally.Scope;
import io.temporal.common.converter.DataConverter;
import io.temporal.common.interceptors.ScheduleClientCallsInterceptor;
import io.temporal.common.interceptors.ScheduleClientInterceptor;
import io.temporal.internal.WorkflowThreadMarker;
import io.temporal.internal.client.ClientDataConverterFactory;
import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs;
import io.temporal.internal.client.RootScheduleClientInvoker;
import io.temporal.internal.client.external.GenericWorkflowClient;
Expand Down Expand Up @@ -71,6 +73,8 @@ public static ScheduleClient newInstance(
// Set merged plugins after configuration, then build
builder.setPlugins(mergedPlugins);
options = builder.build();
DataConverter internalDataConverter =
ClientDataConverterFactory.forClient(options.getDataConverter());

workflowServiceStubs =
new NamespaceInjectWorkflowServiceStubs(workflowServiceStubs, options.getNamespace());
Expand All @@ -83,7 +87,7 @@ public static ScheduleClient newInstance(
.tagged(MetricsTag.defaultTags(options.getNamespace()));
this.genericClient = new GenericWorkflowClientImpl(workflowServiceStubs, metricsScope);
this.interceptors = options.getInterceptors();
this.scheduleClientCallsInvoker = initializeClientInvoker();
this.scheduleClientCallsInvoker = initializeClientInvoker(internalDataConverter);
}

private static ScheduleClientPlugin[] extractScheduleClientPlugins(
Expand All @@ -100,9 +104,9 @@ private static ScheduleClientPlugin[] extractScheduleClientPlugins(
return schedulePlugins.toArray(new ScheduleClientPlugin[0]);
}

private ScheduleClientCallsInterceptor initializeClientInvoker() {
private ScheduleClientCallsInterceptor initializeClientInvoker(DataConverter dataConverter) {
ScheduleClientCallsInterceptor scheduleClientInvoker =
new RootScheduleClientInvoker(genericClient, options);
new RootScheduleClientInvoker(genericClient, options, dataConverter);
for (ScheduleClientInterceptor clientInterceptor : interceptors) {
scheduleClientInvoker =
clientInterceptor.scheduleClientCallsInterceptor(scheduleClientInvoker);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.temporal.api.common.v1.Payloads;
import io.temporal.api.failure.v1.Failure;
import io.temporal.failure.DefaultFailureConverter;
import io.temporal.internal.common.converter.TemporalTransferTypeDataConverter;
import io.temporal.payload.context.SerializationContext;
import java.lang.reflect.Type;
import java.util.*;
Expand Down Expand Up @@ -130,7 +131,7 @@ public RuntimeException failureToException(@Nonnull Failure failure) {
return (serializationContext != null
? failureConverter.withContext(serializationContext)
: failureConverter)
.failureToException(failure, this);
.failureToException(failure, TemporalTransferTypeDataConverter.wrap(this));
}

@Override
Expand All @@ -140,7 +141,7 @@ public Failure exceptionToFailure(@Nonnull Throwable throwable) {
return (serializationContext != null
? failureConverter.withContext(serializationContext)
: failureConverter)
.exceptionToFailure(throwable, this);
.exceptionToFailure(throwable, TemporalTransferTypeDataConverter.wrap(this));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.temporal.common.converter;

import io.temporal.common.Experimental;
import java.lang.reflect.Type;
import javax.annotation.Nullable;

/**
* Converts a model to and from a representation handled by the configured {@link DataConverter}.
*
* <p>Conversion is applied only to top-level values and performs one transfer step. The {@code
* valueType} arguments contain the complete requested model type, including generic arguments.
* Implementations must be stateless and thread-safe because converter instances are cached.
*
* @param <T> annotated model type converted to and from its transfer representation
*/
@Experimental
public interface TransferTypeConverter<T> {
/**
* Returns the type used to serialize a model value.
*
* @param valueType complete declared model type, including generic arguments
* @return non-null transfer type
*/
Type getTransferType(Type valueType);

/** Converts a model value to its transfer representation, which may be null. */
@Nullable
Object toTransferType(T value);

/**
* Reconstructs a model value from its transfer representation.
*
* @param value transfer representation decoded by the configured data converter, which may be
* null
* @param valueType complete requested model type, including generic arguments
*/
T fromTransferType(@Nullable Object value, Type valueType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.temporal.common.converter;

import io.temporal.common.Experimental;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Associates a model class with the converter used to produce its transfer representation.
*
* <p>The annotation is read only from the exact declared class and is not inherited. Conversion is
* applied to top-level values only and performs one transfer step. The configured {@link
* DataConverter} remains responsible for serialization, payload codecs, and wire encoding.
*
* <p>The converter class must be concrete and have a public no-argument constructor. Converter
* instances are cached, so implementations must be stateless and thread-safe.
*/
@Experimental
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TransferTypeConvertible {
/** The converter associated with the annotated model class. */
Class<? extends TransferTypeConverter<?>> value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.temporal.internal.client;

import com.google.common.base.Strings;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.common.converter.DataConverter;
import io.temporal.internal.common.converter.TemporalTransferTypeDataConverter;
import io.temporal.internal.payload.storage.ExternalStorageDataConverter;
import io.temporal.internal.payload.storage.ExternalStorageRunner;
import io.temporal.payload.context.WorkflowSerializationContext;
import io.temporal.payload.storage.StorageDriverWorkflowInfo;
import java.util.Objects;
import javax.annotation.Nullable;

/** Assembles client converters and scopes workflow conversion when workflow identity is known. */
public final class ClientDataConverterFactory {

private final String namespace;
private final ExternalStorageDataConverter workflowConverter;

ClientDataConverterFactory(
WorkflowClientOptions clientOptions, @Nullable ExternalStorageRunner externalStorage) {
this.namespace = clientOptions.getNamespace();
// Workflow reads must reject storage references even when no driver is configured.
this.workflowConverter = withStorage(clientOptions.getDataConverter(), externalStorage);
}

/** Returns a transfer-aware client converter without external storage. */
public static DataConverter forClient(DataConverter configured) {
return transferAware(configured);
}

/** Returns a client converter with external storage outside transfer conversion. */
public static DataConverter forClient(
DataConverter configured, ExternalStorageRunner externalStorage) {
return withStorage(configured, Objects.requireNonNull(externalStorage, "externalStorage"));
}

private static ExternalStorageDataConverter withStorage(
DataConverter configured, @Nullable ExternalStorageRunner externalStorage) {
return new ExternalStorageDataConverter(transferAware(configured), externalStorage);
}

private static DataConverter transferAware(DataConverter configured) {
return TemporalTransferTypeDataConverter.wrap(configured);
}

DataConverter forWorkflow(
String workflowId, @Nullable String runId, @Nullable String workflowType) {
DataConverter converter =
workflowConverter.withContext(new WorkflowSerializationContext(namespace, workflowId));
return ((ExternalStorageDataConverter) converter)
.withStorageTarget(
new StorageDriverWorkflowInfo(
namespace,
Strings.emptyToNull(workflowId),
Strings.emptyToNull(runId),
Strings.emptyToNull(workflowType)));
}
}
Loading
Loading