Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 46 additions & 17 deletions src/main/java/io/temporal/common/converter/DataConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

package io.temporal.common.converter;

import com.google.common.base.Defaults;
import io.temporal.api.common.v1.Payload;
import io.temporal.api.common.v1.Payloads;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Optional;

/**
Expand Down Expand Up @@ -51,30 +53,57 @@ static DataConverter getDefaultInstance() {
Optional<Payloads> toPayloads(Object... values) throws DataConverterException;

/**
* Implements conversion of a single value.
* Implements conversion of an array of values of different types. Useful for deserializing
* arguments of function invocations.
*
* @param content Serialized value to convert to a Java object.
* @param index index of the value in the payloads
* @param content serialized value to convert to Java objects.
* @param parameterType type of the parameter stored in the content
* @param genericParameterType generic type of the parameter stored in the content
* @return converted Java object
* @throws DataConverterException if conversion of the data passed as parameter failed for any
* reason.
*/
<T> T fromPayloads(Optional<Payloads> content, Class<T> parameterType, Type genericParameterType)
<T> T fromPayloads(
int index, Optional<Payloads> content, Class<T> parameterType, Type genericParameterType)
throws DataConverterException;

/**
* Implements conversion of an array of values of different types. Useful for deserializing
* arguments of function invocations.
*
* @param content serialized value to convert to Java objects.
* @param parameterTypes types of the parameters stored in the content
* @param genericParameterTypes generic types of the parameters stored in the content
* @return array of converted Java objects
* @throws DataConverterException if conversion of the data passed as parameter failed for any
* reason.
*/
Object[] arrayFromPayloads(
Optional<Payloads> content, Class<?>[] parameterTypes, Type[] genericParameterTypes)
throws DataConverterException;
static Object[] arrayFromPayloads(
DataConverter converter,
Optional<Payloads> content,
Class<?>[] parameterTypes,
Type[] genericParameterTypes)
throws DataConverterException {
if (parameterTypes != null
&& (genericParameterTypes == null
|| parameterTypes.length != genericParameterTypes.length)) {
throw new IllegalArgumentException(
"parameterTypes don't match length of valueTypes: "
+ Arrays.toString(parameterTypes)
+ "<>"
+ Arrays.toString(genericParameterTypes));
}

int length = parameterTypes.length;
Object[] result = new Object[length];
if (!content.isPresent()) {
// Return defaults for all the parameters
for (int i = 0; i < parameterTypes.length; i++) {
result[i] = Defaults.defaultValue((Class<?>) genericParameterTypes[i]);
}
return result;
}
Payloads payloads = content.get();
int count = payloads.getPayloadsCount();
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> pt = parameterTypes[i];
Type gt = genericParameterTypes[i];
if (i >= count) {
Copy link
Member

Choose a reason for hiding this comment

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

what if the count of payloads is much greater than that of the parameters? Basically does the user need to know that they are missing out on some payloads? Or is this well understood?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. The current answer is that it is needed to be able to add and remove arguments of functions.

result[i] = Defaults.defaultValue((Class<?>) gt);
} else {
result[i] = converter.fromPayload(payloads.getPayloads(i), pt, gt);
}
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import io.temporal.api.common.v1.Payloads;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -142,60 +141,17 @@ public Optional<Payloads> toPayloads(Object... values) throws DataConverterExcep
}

@Override
public <T> T fromPayloads(Optional<Payloads> content, Class<T> valueClass, Type valueType)
public <T> T fromPayloads(
int index, Optional<Payloads> content, Class<T> parameterType, Type genericParameterType)
throws DataConverterException {
if (!content.isPresent()) {
return null;
return (T) Defaults.defaultValue((Class<?>) parameterType);
}
Payloads c = content.get();
if (c.getPayloadsCount() == 0) {
return null;
}
if (c.getPayloadsCount() != 1) {
throw new DataConverterException(
"Found multiple payloads while a single one expected", content, valueType);
}
return fromPayload(c.getPayloads(0), valueClass, valueType);
}

@Override
public Object[] arrayFromPayloads(
Optional<Payloads> content, Class<?>[] parameterTypes, Type[] valueTypes)
throws DataConverterException {
try {
if (parameterTypes != null
&& (valueTypes == null || parameterTypes.length != valueTypes.length)) {
throw new IllegalArgumentException(
"parameterTypes don't match length of valueTypes: "
+ Arrays.toString(parameterTypes)
+ "<>"
+ Arrays.toString(valueTypes));
}
if (!content.isPresent()) {
if (valueTypes.length == 0) {
return EMPTY_OBJECT_ARRAY;
} else {
throw new DataConverterException("Empty content", content, valueTypes);
}
}
Payloads c = content.get();
int count = c.getPayloadsCount();
int length = valueTypes.length;
Object[] result = new Object[length];
for (int i = 0; i < length; i++) {
Type vt = valueTypes[i];
Class<?> pt = parameterTypes[i];
if (i >= count) {
result[i] = Defaults.defaultValue((Class<?>) vt);
} else {
result[i] = fromPayload(c.getPayloads(i), pt, vt);
}
}
return result;
} catch (DataConverterException e) {
throw e;
} catch (Throwable e) {
throw new DataConverterException(e);
int count = content.get().getPayloadsCount();
// To make adding arguments a backwards compatible change
if (index >= count) {
return (T) Defaults.defaultValue((Class<?>) parameterType);
}
return fromPayload(content.get().getPayloads(index), parameterType, genericParameterType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,30 @@
import java.util.Objects;
import java.util.Optional;

public final class EncodedValue implements Value {
public final class EncodedValues implements Values {
private Optional<Payloads> payloads;
private DataConverter converter;
private final Optional<Object> value;
private Object[] values;

public EncodedValue(Optional<Payloads> payloads, DataConverter converter) {
public EncodedValues(Optional<Payloads> payloads, DataConverter converter) {
this.payloads = Objects.requireNonNull(payloads);
this.converter = converter;
this.value = null;
this.values = null;
}

public <T> EncodedValue(T value) {
this.value = Optional.ofNullable(value);
public EncodedValues(Object... values) {
this.values = values;
this.payloads = null;
}

public Optional<Payloads> toPayloads() {
if (payloads == null) {
if (!value.isPresent()) {
if (values == null || values.length == 0) {
payloads = Optional.empty();
} else if (converter == null) {
throw new IllegalStateException("converter not set");
} else {
payloads = converter.toPayloads(value.get());
payloads = converter.toPayloads(values);
}
}
return payloads;
Expand All @@ -58,22 +58,35 @@ public void setDataConverter(DataConverter converter) {
}

@Override
public <T> T get(Class<T> parameterType) throws DataConverterException {
if (value != null) {
@SuppressWarnings("unchecked")
T result = (T) value.orElse(null);
return result;
public int getSize() {
if (values != null) {
return values.length;
} else {
if (converter == null) {
throw new IllegalStateException("converter not set");
if (payloads.isPresent()) {
Copy link
Member

Choose a reason for hiding this comment

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

we are guaranteed that at least one of values / payloads is not null right? Basically, values and payloads cannot both be null because (Object... values) guarantees values is at least of length 0 if that constructor is used?

Copy link
Member Author

Choose a reason for hiding this comment

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

In a lot of cases, Payloads are not set at all if there are no any values. But we probably need to audit for this.

return payloads.get().getPayloadsCount();
} else {
return 0;
}
return converter.fromPayloads(payloads, parameterType, parameterType);
}
}

@Override
public <T> T get(Class<T> parameterType, Type genericParameterType)
public <T> T get(int index, Class<T> parameterType) throws DataConverterException {
return get(index, parameterType, parameterType);
}

@Override
public <T> T get(int index, Class<T> parameterType, Type genericParameterType)
throws DataConverterException {
return converter.fromPayloads(payloads, parameterType, genericParameterType);
if (values != null) {
@SuppressWarnings("unchecked")
T result = (T) values[index];
Copy link
Member

Choose a reason for hiding this comment

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

if the user exceeds the bounds of the values/payload, no issues right?

return result;
} else {
if (converter == null) {
throw new IllegalStateException("converter not set");
}
return converter.fromPayloads(index, payloads, parameterType, genericParameterType);
Copy link
Member

Choose a reason for hiding this comment

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

No need to initialize / cache the converted object into the values array right?

Copy link
Member Author

Choose a reason for hiding this comment

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

The idea of Values is that it can be created with objects or Payloads. I'm not sure if caching is really going to help here as every value accessed only once I believe.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,31 @@
import java.lang.reflect.Type;

/** Value that can be extracted to an appropriate type. */
public interface Value {
public interface Values {

int getSize();

/** The same as {@link #get(int, Class)} with 0 index. */
default <T> T get(Class<T> parameterType) throws DataConverterException {
return get(0, parameterType);
}

/**
* Get value of the specified type.
*
* @param index index of the value in the list of values.
* @param parameterType class of the value to get
* @param <T> type of the value to get
* @return value or null
* @throws DataConverterException if value cannot be extracted to the given type
*/
<T> T get(Class<T> parameterType) throws DataConverterException;
<T> T get(int index, Class<T> parameterType) throws DataConverterException;

/** The same as {@link #get(int, Class, Type)} with 0 index. */
default <T> T get(Class<T> parameterType, Type genericParameterType)
throws DataConverterException {
return get(0, parameterType, genericParameterType);
}

/**
* Get value of the specified generic type. For example if value is of type List<MyClass> use the
Expand All @@ -43,11 +57,13 @@ public interface Value {
* List&lt;MyClass&gt; result = value.get(List.class, typeToken.getType());
* </code></pre>
*
* @param index index of the value in the list of values.
* @param parameterType class of the value to get
* @param genericParameterType the type of the value to get
* @param <T> type of the value to get
* @return value or null
* @throws DataConverterException if value cannot be extracted to the given type
*/
<T> T get(Class<T> parameterType, Type genericParameterType) throws DataConverterException;
<T> T get(int index, Class<T> parameterType, Type genericParameterType)
throws DataConverterException;
}
52 changes: 25 additions & 27 deletions src/main/java/io/temporal/failure/ApplicationFailure.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

import com.google.common.base.Strings;
import io.temporal.common.converter.DataConverter;
import io.temporal.common.converter.EncodedValue;
import io.temporal.common.converter.Value;
import io.temporal.common.converter.EncodedValues;
import io.temporal.common.converter.Values;

/**
* Application failure is used to communicate application specific failures between workflows and
Expand Down Expand Up @@ -51,49 +51,47 @@
*/
public final class ApplicationFailure extends TemporalFailure {
private final String type;
private final Value details;
private final Values details;
private boolean nonRetryable;

/**
* New ApplicationFailure with {@link #isNonRetryable()} flag set to false. Note that this
Copy link
Member

@mastermanu mastermanu Jul 31, 2020

Choose a reason for hiding this comment

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

didn't quite follow the 2nd sentence of this comment. did you mean to say if the type is NOT included

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch.

* exception still can be not retried by the service if its type is included into doNotRetry
* property of the correspondent retry policy.
*
* @param message optional error message
* @param type optional error type that is used by {@link
* io.temporal.common.RetryOptions#addDoNotRetry(String...)}.
* @param details optional details about the failure. They are serialized using the same approach
* as arguments and results and can be accessed through {@link #getDetails()}
* @param cause failure cause. Each element of the cause chain is converted to ApplicationFailure
* if it doesn't extend {@link TemporalFailure}.
* as arguments and results.
*/
public ApplicationFailure(String message, String type, Object details, Exception cause) {
this(message, type, new EncodedValue(details), false, cause);
public static ApplicationFailure newFailure(String message, String type, Object... details) {
Copy link
Member

Choose a reason for hiding this comment

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

why not just call it newRetryableFailure?

Copy link
Member Author

Choose a reason for hiding this comment

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

I called it initially. But then I found that it is very confusing that it can be not retried if the policy says so.

return new ApplicationFailure(message, type, false, new EncodedValues(details), null);
}

/**
* New ApplicationFailure with {@link #isNonRetryable()} flag set to true.
*
* <p>It means that this exception is not going to be retried even if it is not included into
* retry policy doNotRetry list.
*
* @param message optional error message
* @param type optional error type that is used by {@link
* io.temporal.common.RetryOptions#addDoNotRetry(String...)}.
* @param type optional error type
* @param details optional details about the failure. They are serialized using the same approach
* as arguments and results.
*/
public ApplicationFailure(String message, String type, Object details) {
this(message, type, new EncodedValue(details), false, null);
}

/**
* @param message optional error message
* @param type optional error type that is used by {@link
* io.temporal.common.RetryOptions#addDoNotRetry(String...)}.
*/
public ApplicationFailure(String message, String type) {
this(message, type, new EncodedValue(null), false, null);
public static ApplicationFailure newNonRetryableFailure(
String message, String type, Object... details) {
return new ApplicationFailure(message, type, true, new EncodedValues(details), null);
}

/** * @param message optional error message */
public ApplicationFailure(String message) {
this(message, null);
static ApplicationFailure newFromValues(
String message, String type, boolean nonRetryable, Values details, Throwable cause) {
Copy link
Member

Choose a reason for hiding this comment

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

[Nit], having boolean names that are negative (nonRetryable vs retryable) can be kind of confusing

Copy link
Member Author

Choose a reason for hiding this comment

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

I almost always follow the pattern that the default value should be the default value for the type. So the default of the boolean type is false. So the field is called nonRetryable as its default is false. But I agree that it looks weird in the parameter name.

return new ApplicationFailure(message, type, nonRetryable, details, cause);
}

ApplicationFailure(
String message, String type, Value details, boolean nonRetryable, Exception cause) {
String message, String type, boolean nonRetryable, Values details, Throwable cause) {
super(getMessage(message, type, nonRetryable), message, cause);
this.type = type;
this.details = details;
Expand All @@ -104,7 +102,7 @@ public String getType() {
return type;
}

public Value getDetails() {
public Values getDetails() {
return details;
}

Expand All @@ -118,7 +116,7 @@ public boolean isNonRetryable() {

@Override
public void setDataConverter(DataConverter converter) {
((EncodedValue) details).setDataConverter(converter);
((EncodedValues) details).setDataConverter(converter);
}

private static String getMessage(String message, String type, boolean nonRetryable) {
Expand Down
Loading