|
1 | 1 | package io.temporal.internal.common; |
2 | 2 |
|
| 3 | +import com.fasterxml.jackson.core.JsonProcessingException; |
| 4 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 5 | +import com.fasterxml.jackson.databind.ObjectWriter; |
3 | 6 | import com.google.protobuf.ByteString; |
4 | 7 | import com.google.protobuf.InvalidProtocolBufferException; |
5 | 8 | import com.google.protobuf.util.JsonFormat; |
| 9 | +import io.nexusrpc.FailureInfo; |
6 | 10 | import io.nexusrpc.Link; |
| 11 | +import io.nexusrpc.handler.HandlerException; |
| 12 | +import io.temporal.api.common.v1.Payload; |
| 13 | +import io.temporal.api.enums.v1.NexusHandlerErrorRetryBehavior; |
7 | 14 | import io.temporal.api.nexus.v1.Failure; |
| 15 | +import io.temporal.api.nexus.v1.HandlerError; |
8 | 16 | import io.temporal.common.converter.DataConverter; |
9 | 17 | import java.net.URI; |
10 | 18 | import java.net.URISyntaxException; |
|
13 | 21 | import java.util.Map; |
14 | 22 |
|
15 | 23 | public class NexusUtil { |
16 | | - private static final JsonFormat.Printer JSON_PRINTER = |
| 24 | + private static final ObjectWriter JSON_OBJECT_WRITER = new ObjectMapper().writer(); |
| 25 | + private static final JsonFormat.Printer PROTO_JSON_PRINTER = |
17 | 26 | JsonFormat.printer().omittingInsignificantWhitespace(); |
18 | 27 | private static final String TEMPORAL_FAILURE_TYPE_STRING = |
19 | 28 | io.temporal.api.failure.v1.Failure.getDescriptor().getFullName(); |
@@ -47,23 +56,146 @@ public static Link nexusProtoLinkToLink(io.temporal.api.nexus.v1.Link nexusLink) |
47 | 56 | .build(); |
48 | 57 | } |
49 | 58 |
|
50 | | - public static Failure exceptionToNexusFailure(Throwable exception, DataConverter dataConverter) { |
51 | | - io.temporal.api.failure.v1.Failure failure = dataConverter.exceptionToFailure(exception); |
| 59 | + public static Failure temporalFailureToNexusFailure( |
| 60 | + io.temporal.api.failure.v1.Failure temporalFailure) { |
52 | 61 | String details; |
53 | 62 | try { |
54 | | - details = JSON_PRINTER.print(failure.toBuilder().setMessage("").build()); |
| 63 | + details = |
| 64 | + PROTO_JSON_PRINTER.print( |
| 65 | + temporalFailure.toBuilder().setMessage("").setStackTrace("").build()); |
55 | 66 | } catch (InvalidProtocolBufferException e) { |
56 | 67 | return Failure.newBuilder() |
57 | 68 | .setMessage("Failed to serialize failure details") |
58 | 69 | .setDetails(ByteString.copyFromUtf8(e.getMessage())) |
59 | 70 | .build(); |
60 | 71 | } |
61 | | - return Failure.newBuilder() |
62 | | - .setMessage(failure.getMessage()) |
63 | | - .setDetails(ByteString.copyFromUtf8(details)) |
64 | | - .putAllMetadata(NEXUS_FAILURE_METADATA) |
| 72 | + Failure.Builder failureBuilder = |
| 73 | + Failure.newBuilder() |
| 74 | + .setMessage(temporalFailure.getMessage()) |
| 75 | + .setDetails(ByteString.copyFromUtf8(details)) |
| 76 | + .putAllMetadata(NEXUS_FAILURE_METADATA); |
| 77 | + if (!temporalFailure.getStackTrace().isEmpty()) { |
| 78 | + failureBuilder.setStackTrace(temporalFailure.getStackTrace()); |
| 79 | + } |
| 80 | + return failureBuilder.build(); |
| 81 | + } |
| 82 | + |
| 83 | + public static io.temporal.api.failure.v1.Failure nexusFailureToTemporalFailure( |
| 84 | + FailureInfo failureInfo, boolean retryable) { |
| 85 | + io.temporal.api.failure.v1.Failure.Builder apiFailure = |
| 86 | + io.temporal.api.failure.v1.Failure.newBuilder(); |
| 87 | + |
| 88 | + if (failureInfo.getMetadata().containsKey("type") |
| 89 | + && failureInfo.getMetadata().get("type").equals(TEMPORAL_FAILURE_TYPE_STRING)) { |
| 90 | + // Details contains a JSON-serialized Temporal failure |
| 91 | + try { |
| 92 | + JsonFormat.parser().ignoringUnknownFields().merge(failureInfo.getDetailsJson(), apiFailure); |
| 93 | + } catch (InvalidProtocolBufferException e) { |
| 94 | + throw new RuntimeException(e); |
| 95 | + } |
| 96 | + } else { |
| 97 | + // Create an ApplicationFailure with the Nexus failure data |
| 98 | + io.temporal.api.common.v1.Payloads payloads = nexusFailureMetadataToPayloads(failureInfo); |
| 99 | + io.temporal.api.failure.v1.ApplicationFailureInfo.Builder appFailureInfo = |
| 100 | + io.temporal.api.failure.v1.ApplicationFailureInfo.newBuilder() |
| 101 | + .setType("NexusFailure") |
| 102 | + .setNonRetryable(!retryable); |
| 103 | + if (payloads != null) { |
| 104 | + appFailureInfo.setDetails(payloads); |
| 105 | + } |
| 106 | + apiFailure.setApplicationFailureInfo(appFailureInfo.build()); |
| 107 | + } |
| 108 | + |
| 109 | + // Ensure these always get written |
| 110 | + apiFailure.setMessage(failureInfo.getMessage()); |
| 111 | + if (failureInfo.getStackTrace() != null && !failureInfo.getStackTrace().isEmpty()) { |
| 112 | + apiFailure.setStackTrace(failureInfo.getStackTrace()); |
| 113 | + } |
| 114 | + |
| 115 | + return apiFailure.build(); |
| 116 | + } |
| 117 | + |
| 118 | + private static io.temporal.api.common.v1.Payloads nexusFailureMetadataToPayloads( |
| 119 | + FailureInfo failureInfo) { |
| 120 | + if (failureInfo.getMetadata().isEmpty() && failureInfo.getDetailsJson().isEmpty()) { |
| 121 | + return null; |
| 122 | + } |
| 123 | + |
| 124 | + // Create a copy without the message before serializing |
| 125 | + FailureInfo failureCopy = FailureInfo.newBuilder(failureInfo).setMessage("").build(); |
| 126 | + String json = null; |
| 127 | + try { |
| 128 | + json = JSON_OBJECT_WRITER.writeValueAsString(failureCopy); |
| 129 | + } catch (JsonProcessingException e) { |
| 130 | + throw new RuntimeException(e); |
| 131 | + } |
| 132 | + |
| 133 | + return io.temporal.api.common.v1.Payloads.newBuilder() |
| 134 | + .addPayloads( |
| 135 | + Payload.newBuilder() |
| 136 | + .putMetadata("encoding", ByteString.copyFromUtf8("json/plain")) |
| 137 | + .setData(ByteString.copyFromUtf8(json)) |
| 138 | + .build()) |
| 139 | + .build(); |
| 140 | + } |
| 141 | + |
| 142 | + public static FailureInfo temporalFailureToNexusFailureInfo( |
| 143 | + io.temporal.api.failure.v1.Failure temporalFailure) { |
| 144 | + String details; |
| 145 | + try { |
| 146 | + details = |
| 147 | + PROTO_JSON_PRINTER.print( |
| 148 | + temporalFailure.toBuilder().setMessage("").setStackTrace("").build()); |
| 149 | + } catch (InvalidProtocolBufferException e) { |
| 150 | + return FailureInfo.newBuilder() |
| 151 | + .setMessage("Failed to serialize failure details") |
| 152 | + .setDetailsJson(e.getMessage()) |
| 153 | + .build(); |
| 154 | + } |
| 155 | + return FailureInfo.newBuilder() |
| 156 | + .setMessage(temporalFailure.getMessage()) |
| 157 | + .setDetailsJson(details) |
| 158 | + .putMetadata("type", TEMPORAL_FAILURE_TYPE_STRING) |
65 | 159 | .build(); |
66 | 160 | } |
67 | 161 |
|
| 162 | + public static Failure exceptionToNexusFailure(Throwable exception, DataConverter dataConverter) { |
| 163 | + io.temporal.api.failure.v1.Failure failure = dataConverter.exceptionToFailure(exception); |
| 164 | + return temporalFailureToNexusFailure(failure); |
| 165 | + } |
| 166 | + |
| 167 | + /** |
| 168 | + * Convert a HandlerException to the legacy HandlerError format used by Nexus, including |
| 169 | + * converting the cause to a Failure. |
| 170 | + */ |
| 171 | + public static HandlerError handlerErrorToNexusError( |
| 172 | + HandlerException e, DataConverter dataConverter) { |
| 173 | + HandlerError.Builder handlerError = |
| 174 | + HandlerError.newBuilder() |
| 175 | + .setErrorType(e.getErrorType().toString()) |
| 176 | + .setRetryBehavior(mapRetryBehavior(e.getRetryBehavior())); |
| 177 | + if (e.getCause() != null) { |
| 178 | + handlerError.setFailure(exceptionToNexusFailure(e.getCause(), dataConverter)); |
| 179 | + } else if (e.getMessage() != null && !e.getMessage().isEmpty()) { |
| 180 | + // Generate a failure from the message if no cause is provided, to ensure the error is not |
| 181 | + // empty |
| 182 | + handlerError.setFailure( |
| 183 | + exceptionToNexusFailure(new RuntimeException(e.getMessage()), dataConverter)); |
| 184 | + } |
| 185 | + return handlerError.build(); |
| 186 | + } |
| 187 | + |
| 188 | + private static NexusHandlerErrorRetryBehavior mapRetryBehavior( |
| 189 | + HandlerException.RetryBehavior retryBehavior) { |
| 190 | + switch (retryBehavior) { |
| 191 | + case RETRYABLE: |
| 192 | + return NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE; |
| 193 | + case NON_RETRYABLE: |
| 194 | + return NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE; |
| 195 | + default: |
| 196 | + return NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_UNSPECIFIED; |
| 197 | + } |
| 198 | + } |
| 199 | + |
68 | 200 | private NexusUtil() {} |
69 | 201 | } |
0 commit comments