• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

temporalio / sdk-java / #185

28 Aug 2023 02:02PM CUT coverage: 77.642% (-0.04%) from 77.685%
#185

push

github-actions

web-flow
Reconcile typed search attributes with schedules (#1848)

Reconcile typed search attributes with schedules

30 of 30 new or added lines in 6 files covered. (100.0%)

18579 of 23929 relevant lines covered (77.64%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.0
/temporal-sdk/src/main/java/io/temporal/internal/common/SearchAttributePayloadConverter.java
1
/*
2
 * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
3
 *
4
 * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5
 *
6
 * Modifications copyright (C) 2017 Uber Technologies, Inc.
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this material except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *   http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20

21
package io.temporal.internal.common;
22

23
import com.google.common.base.Preconditions;
24
import com.google.common.reflect.TypeParameter;
25
import com.google.common.reflect.TypeToken;
26
import com.google.protobuf.ByteString;
27
import io.temporal.api.common.v1.Payload;
28
import io.temporal.api.enums.v1.IndexedValueType;
29
import io.temporal.common.SearchAttributeKey;
30
import io.temporal.common.SearchAttributes;
31
import io.temporal.common.converter.DataConverterException;
32
import io.temporal.common.converter.DefaultDataConverter;
33
import java.lang.reflect.Type;
34
import java.time.OffsetDateTime;
35
import java.util.*;
36
import java.util.stream.Collectors;
37
import javax.annotation.Nonnull;
38
import javax.annotation.Nullable;
39
import org.slf4j.Logger;
40
import org.slf4j.LoggerFactory;
41

42
final class SearchAttributePayloadConverter {
1✔
43
  private static final Logger log = LoggerFactory.getLogger(SearchAttributePayloadConverter.class);
1✔
44

45
  private static final String METADATA_TYPE_KEY = "type";
46

47
  public static final SearchAttributePayloadConverter INSTANCE =
1✔
48
      new SearchAttributePayloadConverter();
49

50
  public Payload encodeTyped(SearchAttributeKey<?> key, @Nullable Object value) {
51
    if (key.getValueType() == IndexedValueType.INDEXED_VALUE_TYPE_UNSPECIFIED) {
1✔
52
      // If we don't have the type we should just leave the payload as is
53
      return (Payload) value;
×
54
    }
55
    // We can encode as-is because we know it's strictly typed to expected key value. We
56
    // accept a null value because updates for unset can be null.
57
    return DefaultDataConverter.STANDARD_INSTANCE.toPayload(value).get().toBuilder()
1✔
58
        .putMetadata(
1✔
59
            METADATA_TYPE_KEY,
60
            ByteString.copyFromUtf8(indexValueTypeToEncodedValue(key.getValueType())))
1✔
61
        .build();
1✔
62
  }
63

64
  @SuppressWarnings({"rawtypes", "unchecked"})
65
  public void decodeTyped(SearchAttributes.Builder builder, String name, @Nonnull Payload payload) {
66
    // Get key type
67
    SearchAttributeKey key;
68
    IndexedValueType indexType = getIndexType(payload.getMetadataMap().get(METADATA_TYPE_KEY));
1✔
69
    if (indexType == null) {
1✔
70
      // If the server didn't write the type metadata we
71
      // don't know how to decode this search attribute
72
      key = SearchAttributeKey.forUntyped(name);
×
73
      builder.set(key, payload);
×
74
      return;
×
75
    }
76
    switch (indexType) {
1✔
77
      case INDEXED_VALUE_TYPE_TEXT:
78
        key = SearchAttributeKey.forText(name);
1✔
79
        break;
1✔
80
      case INDEXED_VALUE_TYPE_KEYWORD:
81
        key = SearchAttributeKey.forKeyword(name);
1✔
82
        break;
1✔
83
      case INDEXED_VALUE_TYPE_INT:
84
        key = SearchAttributeKey.forLong(name);
1✔
85
        break;
1✔
86
      case INDEXED_VALUE_TYPE_DOUBLE:
87
        key = SearchAttributeKey.forDouble(name);
1✔
88
        break;
1✔
89
      case INDEXED_VALUE_TYPE_BOOL:
90
        key = SearchAttributeKey.forBoolean(name);
1✔
91
        break;
1✔
92
      case INDEXED_VALUE_TYPE_DATETIME:
93
        key = SearchAttributeKey.forOffsetDateTime(name);
1✔
94
        break;
1✔
95
      case INDEXED_VALUE_TYPE_KEYWORD_LIST:
96
        key = SearchAttributeKey.forKeywordList(name);
1✔
97
        break;
1✔
98
      default:
99
        log.warn(
×
100
            "[BUG] Unrecognized indexed value type {} on search attribute key {}", indexType, name);
101
        return;
×
102
    }
103

104
    // Attempt conversion to key type or if not keyword list, single-value list and extract out
105
    try {
106
      Object value =
1✔
107
          DefaultDataConverter.STANDARD_INSTANCE.fromPayload(
1✔
108
              payload, key.getValueClass(), key.getValueReflectType());
1✔
109
      // Test server can have null value, which means unset so we ignore
110
      if (value != null) {
1✔
111
        builder.set(key, value);
1✔
112
      }
113
    } catch (Exception e) {
×
114
      Exception exception = e;
×
115
      // Since it couldn't be converted using the regular type, try to convert as a single-item list
116
      // for non-keyword-list
117
      if (indexType != IndexedValueType.INDEXED_VALUE_TYPE_KEYWORD_LIST) {
×
118
        try {
119
          List items =
×
120
              DefaultDataConverter.STANDARD_INSTANCE.fromPayload(
×
121
                  payload, List.class, createListType(key.getValueClass()));
×
122
          // If it's a single-item list, we're ok and can return, but if it's not, replace outer
123
          // exception with better exception explaining situation
124
          if (items.size() == 1) {
×
125
            builder.set(key, items.get(0));
×
126
            return;
×
127
          }
128
          exception =
×
129
              new IllegalArgumentException("Unexpected list of " + items.size() + " length");
×
130
        } catch (Exception eIgnore) {
×
131
          // Just ignore the error and fall through to throw after if
132
        }
×
133
      }
134
      throw new IllegalArgumentException(
×
135
          "Search attribute " + name + " can't be deserialized", exception);
136
    }
1✔
137
  }
1✔
138

139
  /**
140
   * Convert Search Attribute object into payload with metadata. Ideally, we don't want to send the
141
   * type metadata to the server, because starting with v1.10.0 Temporal doesn't look at the type
142
   * metadata that the SDK sends. When the attribute is registered with the service, so is its
143
   * intended type which is used to validate the data. However, we do include type metadata in
144
   * payload here for compatibility with older versions of the server. Earlier version of Temporal
145
   * save the type metadata and return exactly the same payload back to the SDK, which will be
146
   * needed to deserialize the attribute into it's initial type.
147
   */
148
  public Payload encode(@Nonnull Object instance) {
149
    if (instance instanceof Collection && ((Collection<?>) instance).size() == 1) {
1✔
150
      // always serialize an empty collection as one value
151
      instance = ((Collection<?>) instance).iterator().next();
1✔
152
    }
153

154
    if (instance instanceof Payload) {
1✔
155
      // if dealing with old style search attributes and old server version we may not be able to
156
      // deserialize them
157
      // and decode will return a Payload. If it gets blindly passed further, we may end up with a
158
      // Payload here.
159
      return (Payload) instance;
×
160
    }
161

162
    Payload payload = DefaultDataConverter.STANDARD_INSTANCE.toPayload(instance).get();
1✔
163

164
    IndexedValueType type = extractIndexValueTypeName(instance);
1✔
165

166
    if (type == null) {
1✔
167
      // null returned from the previous method is a special case for UNSET
168
      return payload;
1✔
169
    }
170

171
    if (IndexedValueType.INDEXED_VALUE_TYPE_UNSPECIFIED.equals(type)) {
1✔
172
      // We can't enforce the specific types because of backwards compatibility. Previously any
173
      // value that was serializable by json (or proto json) into a string could be passed as
174
      // a search attribute
175
      // throw new IllegalArgumentException("Instance " + instance + " of class " +
176
      // instance.getClass() + " is not supported as a search attribute value");
177
      log.warn(
×
178
          "Instance {} of class {}"
179
              + " is not one of the types supported as a search attribute."
180
              + " For backwards compatibility we do the best effort to serialize it,"
181
              + " but it may cause a WorkflowTask failure after server validation.",
182
          instance,
183
          instance.getClass());
×
184
    }
185

186
    return payload.toBuilder()
1✔
187
        .putMetadata(METADATA_TYPE_KEY, ByteString.copyFromUtf8(indexValueTypeToEncodedValue(type)))
1✔
188
        .build();
1✔
189
  }
190

191
  @SuppressWarnings("deprecation")
192
  @Nonnull
193
  public List<?> decode(@Nonnull Payload payload) {
194
    ByteString dataType = payload.getMetadataMap().get(METADATA_TYPE_KEY);
1✔
195

196
    IndexedValueType indexType = getIndexType(dataType);
1✔
197
    if (isIndexTypeUndefined(indexType)) {
1✔
198
      if (isUnset(payload)) {
1✔
199
        return io.temporal.common.SearchAttribute.UNSET_VALUE;
1✔
200
      } else {
201
        log.warn("Absent or unexpected search attribute type metadata in a payload: {}", payload);
×
202
        return Collections.singletonList(payload);
×
203
      }
204
    }
205

206
    return decodeAsType(payload, indexType);
1✔
207
  }
208

209
  @Nonnull
210
  public List<?> decodeAsType(@Nonnull Payload payload, @Nonnull IndexedValueType indexType)
211
      throws DataConverterException {
212
    Preconditions.checkArgument(
1✔
213
        !isIndexTypeUndefined(indexType), "indexType can't be %s", indexType);
1✔
214

215
    ByteString data = payload.getData();
1✔
216
    if (data.isEmpty()) {
1✔
217
      log.warn("No data in payload: {}", payload);
×
218
      return Collections.singletonList(payload);
×
219
    }
220

221
    Class<?> type = indexValueTypeToJavaType(indexType);
1✔
222
    Preconditions.checkArgument(type != null);
1✔
223

224
    try {
225
      // single-value search attribute
226
      return Collections.singletonList(
1✔
227
          DefaultDataConverter.STANDARD_INSTANCE.fromPayload(payload, type, type));
1✔
228
    } catch (Exception e) {
1✔
229
      try {
230
        return DefaultDataConverter.STANDARD_INSTANCE.fromPayload(
1✔
231
            payload, List.class, createListType(type));
1✔
232
      } catch (Exception ex) {
1✔
233
        throw new IllegalArgumentException(
1✔
234
            ("Payload "
235
                + data.toStringUtf8()
1✔
236
                + " can't be deserialized into a single value or a list of "
237
                + type),
238
            ex);
239
      }
240
    }
241
  }
242

243
  private boolean isUnset(@Nonnull Payload payload) {
244
    try {
245
      List<?> o =
1✔
246
          DefaultDataConverter.STANDARD_INSTANCE.fromPayload(payload, List.class, List.class);
1✔
247
      if (o.size() == 0) {
1✔
248
        // this is an "unset" token, we don't need a type for it
249
        return true;
1✔
250
      }
251
    } catch (Exception e) {
×
252
      // ignore the exception, it was an attempt to parse a specific "unset" ('[]') value only
253
    }
×
254
    return false;
×
255
  }
256

257
  private static IndexedValueType getIndexType(ByteString dataType) {
258
    if (dataType != null) {
1✔
259
      String dataTypeString = dataType.toStringUtf8();
1✔
260
      if (dataTypeString.length() != 0) {
1✔
261
        return encodedValueToIndexValueType(dataTypeString);
1✔
262
      }
263
    }
264
    return null;
1✔
265
  }
266

267
  @Nullable
268
  private static IndexedValueType extractIndexValueTypeName(@Nonnull Object instance) {
269
    if (instance instanceof Collection) {
1✔
270
      Collection<?> collection = (Collection<?>) instance;
1✔
271
      if (!collection.isEmpty()) {
1✔
272
        List<IndexedValueType> indexValues =
1✔
273
            collection.stream()
1✔
274
                .map(k -> (javaTypeToIndexValueType(k.getClass())))
1✔
275
                .distinct()
1✔
276
                .collect(Collectors.toList());
1✔
277

278
        if (indexValues.size() == 1) {
1✔
279
          return indexValues.get(0);
1✔
280
        } else {
281
          throw new IllegalArgumentException(
×
282
              instance + " maps into a mix of IndexValueTypes: " + indexValues);
283
        }
284
      } else {
285
        // it's an "unset" value
286
        // has to be null and can't be INDEXED_VALUE_TYPE_UNSPECIFIED
287
        // because there was a bug: https://github.com/temporalio/temporal/issues/2693
288
        return null;
1✔
289
      }
290
    } else {
291
      return javaTypeToIndexValueType(instance.getClass());
1✔
292
    }
293
  }
294

295
  @Nonnull
296
  private static IndexedValueType javaTypeToIndexValueType(@Nonnull Class<?> type) {
297
    if (CharSequence.class.isAssignableFrom(type)) {
1✔
298
      return IndexedValueType.INDEXED_VALUE_TYPE_TEXT;
1✔
299
    } else if (Long.class.equals(type)
1✔
300
        || Integer.class.equals(type)
1✔
301
        || Short.class.equals(type)
1✔
302
        || Byte.class.equals(type)) {
1✔
303
      return IndexedValueType.INDEXED_VALUE_TYPE_INT;
1✔
304
    } else if (Double.class.equals(type) || Float.class.equals(type)) {
1✔
305
      return IndexedValueType.INDEXED_VALUE_TYPE_DOUBLE;
1✔
306
    } else if (Boolean.class.equals(type)) {
1✔
307
      return IndexedValueType.INDEXED_VALUE_TYPE_BOOL;
1✔
308
    } else if (OffsetDateTime.class.equals(type)) {
1✔
309
      return IndexedValueType.INDEXED_VALUE_TYPE_DATETIME;
1✔
310
    }
311
    return IndexedValueType.INDEXED_VALUE_TYPE_UNSPECIFIED;
×
312
  }
313

314
  @Nullable
315
  private static Class<?> indexValueTypeToJavaType(@Nullable IndexedValueType indexedValueType) {
316
    if (indexedValueType == null) {
1✔
317
      return null;
×
318
    }
319
    switch (indexedValueType) {
1✔
320
      case INDEXED_VALUE_TYPE_TEXT:
321
      case INDEXED_VALUE_TYPE_KEYWORD:
322
      case INDEXED_VALUE_TYPE_KEYWORD_LIST:
323
        return String.class;
1✔
324
      case INDEXED_VALUE_TYPE_INT:
325
        return Long.class;
1✔
326
      case INDEXED_VALUE_TYPE_DOUBLE:
327
        return Double.class;
1✔
328
      case INDEXED_VALUE_TYPE_BOOL:
329
        return Boolean.class;
1✔
330
      case INDEXED_VALUE_TYPE_DATETIME:
331
        return OffsetDateTime.class;
1✔
332
      case INDEXED_VALUE_TYPE_UNSPECIFIED:
333
        return null;
×
334
      default:
335
        log.warn(
×
336
            "[BUG] Mapping of IndexedValueType[{}] to Java class is not implemented",
337
            indexedValueType);
338
        return null;
×
339
    }
340
  }
341

342
  private static boolean isIndexTypeUndefined(@Nullable IndexedValueType indexType) {
343
    return indexType == null
1✔
344
        || indexType.equals(IndexedValueType.INDEXED_VALUE_TYPE_UNSPECIFIED)
1✔
345
        || indexType.equals(IndexedValueType.UNRECOGNIZED);
1✔
346
  }
347

348
  private static String indexValueTypeToEncodedValue(@Nonnull IndexedValueType indexedValueType) {
349
    return ProtoEnumNameUtils.uniqueToSimplifiedName(indexedValueType);
1✔
350
  }
351

352
  @Nullable
353
  private static IndexedValueType encodedValueToIndexValueType(String encodedValue) {
354
    try {
355
      return IndexedValueType.valueOf(
1✔
356
          ProtoEnumNameUtils.simplifiedToUniqueName(
1✔
357
              encodedValue, ProtoEnumNameUtils.INDEXED_VALUE_TYPE_PREFIX));
358
    } catch (IllegalArgumentException e) {
×
359
      log.warn("[BUG] No IndexedValueType mapping for {} value exist", encodedValue);
×
360
      return null;
×
361
    }
362
  }
363

364
  private <K> Type createListType(Class<K> elementType) {
365
    return new TypeToken<List<K>>() {}.where(new TypeParameter<K>() {}, elementType).getType();
1✔
366
  }
367
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc