Skip to content

Commit 8af7e04

Browse files
committed
fix(telemetry): strip only a hidden-class suffix from Java type names
javaTypeName cut every type name at the first '/', and wrapperName passed callback and invoke error types through it, so an external type such as payments/v1.PaymentError was named 'payments' instead of PaymentError. Only a trailing /0x<hex> hidden-class suffix of Class.getName() (live or as the core checkpointed it) is removed now, and callback and invoke error types, which come from outside the JVM, are reduced as a whole by the Table A rule.
1 parent 396f8a5 commit 8af7e04

4 files changed

Lines changed: 57 additions & 9 deletions

File tree

‎telemetry-exporter/README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,10 @@ How the Java exporter applies it:
285285
`pkg.Foo` (binary name `pkg.Foo$1LocalError`) gives `1LocalError`; an anonymous class gives its
286286
last segment, for example `1`. A string that ends with a separator (`trailing.`, `Outer$`) is
287287
kept as it is. U+00A0, U+FEFF, U+001C and U+2003 are not blank, so a type made of them is kept.
288-
- A hidden class's name ends in `/0x…`, a suffix that changes on every JVM start. The name is cut
289-
at the first `/` before rule 6, live and from the checkpoint, so `pkg.Gen$HiddenErr/0x…` gives
290-
`HiddenErr`.
288+
- A hidden class's name ends in `/0x…`, a suffix that changes on every JVM start. Only a trailing
289+
suffix matching `/0x[0-9a-fA-F]+$` is removed before rule 6, live and from the checkpoint, so
290+
`pkg.Gen$HiddenErr/0x…` gives `HiddenErr`. Callback and invoke error types come from outside the
291+
JVM and are never cut: `payments/v1.PaymentError` gives `PaymentError`.
291292
- The SDK wrappers are an explicit list, matched on the exact runtime class:
292293
`DurableOperationException`, `UnrecoverableDurableExecutionException`, `StepException`,
293294
`CallbackException`, `InvokeException`, `StepFailedException`, `CallbackFailedException`,

‎telemetry-exporter/src/main/java/software/amazon/lambda/durable/telemetry/DurableExecutionTelemetry.java‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Collections;
77
import java.util.IdentityHashMap;
88
import java.util.Set;
9+
import java.util.regex.Pattern;
910
import java.util.function.Function;
1011
import java.util.function.UnaryOperator;
1112
import org.slf4j.Logger;
@@ -302,7 +303,8 @@ private static String wrapperName(Throwable wrapper) {
302303
ErrorObject error = extractErrorObject(wrapper);
303304
String type = error == null ? null : error.errorType();
304305
if (!isBlankType(type)) {
305-
return javaTypeName(type);
306+
// A callback or invoke error type is supplied from outside the JVM and is never a Java class name.
307+
return hasExternalErrorType(wrapper) ? simpleTypeName(type) : javaTypeName(type);
306308
}
307309
if (wrapper instanceof DurableOperationException doe && doe.getOperation() != null) {
308310
var op = doe.getOperation();
@@ -317,6 +319,15 @@ private static String wrapperName(Throwable wrapper) {
317319
return DEFAULT_ERROR_NAME;
318320
}
319321

322+
/** {@code true} when the wrapper's checkpointed error type was written by a callback or invoke caller. */
323+
private static boolean hasExternalErrorType(Throwable wrapper) {
324+
if (!(wrapper instanceof DurableOperationException doe) || doe.getOperation() == null) {
325+
return false;
326+
}
327+
var type = doe.getOperation().type();
328+
return type == OperationType.CALLBACK || type == OperationType.CHAINED_INVOKE;
329+
}
330+
320331
/**
321332
* The SDK error (not a wrapper) the core throws for a wrapper's operation, whatever its checkpoint says: a
322333
* timed-out callback, or a timed-out or stopped invoke. {@code null} for any other operation.
@@ -387,17 +398,20 @@ private static Class<?> contextFailure(String subType) {
387398
return null;
388399
}
389400

401+
/** The address suffix of a hidden class's {@link Class#getName()}, which changes on every JVM start. */
402+
private static final Pattern HIDDEN_CLASS_SUFFIX = Pattern.compile("/0x[0-9a-fA-F]+$");
403+
390404
/**
391-
* {@link #simpleTypeName} for a Java class name, live or checkpointed. The name of a hidden class has a
392-
* {@code /0x…} suffix that changes on every JVM start, so the name is cut at the first {@code '/'} first and the
393-
* same failure keeps the same name across cold starts.
405+
* {@link #simpleTypeName} for a Java class name ({@link Class#getName()}), live or as the core checkpointed it.
406+
* Only a hidden-class suffix matching {@code /0x[0-9a-fA-F]+$} is removed first, so the same failure keeps the same
407+
* name across cold starts. No other {@code '/'} is touched, and type strings from outside the JVM (callback and
408+
* invoke error types) never come here.
394409
*/
395410
static String javaTypeName(String className) {
396411
if (className == null) {
397412
return DEFAULT_ERROR_NAME;
398413
}
399-
int slash = className.indexOf('/');
400-
return simpleTypeName(slash < 0 ? className : className.substring(0, slash));
414+
return simpleTypeName(HIDDEN_CLASS_SUFFIX.matcher(className).replaceFirst(""));
401415
}
402416

403417
/**

‎telemetry-exporter/src/test/java/software/amazon/lambda/durable/telemetry/ErrorNameScenarioTest.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,4 +785,32 @@ void invokeTimeoutAndStopAreSdkErrors(Mode mode) {
785785
}
786786
}
787787
}
788+
789+
/**
790+
* Callback and invoke error types come from outside the JVM: a {@code '/'} in them is not a hidden-class suffix and
791+
* the type is reduced as a whole (Table A).
792+
*/
793+
@ParameterizedTest
794+
@EnumSource(Mode.class)
795+
void externalTypeWithSlashIsReducedAsAWhole(Mode mode) {
796+
String external = "payments/v1.PaymentError";
797+
var viaCallback = run(
798+
(in, ctx) -> failure(
799+
mode, ctx, c -> c.createCallback("cb", String.class).get()),
800+
failCallback(external));
801+
var cb = observe(viaCallback, List.of("cb"), mode);
802+
assertEquals("PaymentError", cb.execution().name(), "callback execution");
803+
assertEquals("PaymentError", cb.operations().get("cb").name(), "callback operation");
804+
805+
var viaInvoke = run((in, ctx) -> failure(mode, ctx, c -> c.invoke("callee", "fn", "x", String.class)), r -> {
806+
r.run("in");
807+
r.failChainedInvoke(
808+
"callee",
809+
ErrorObject.builder().errorType(external).errorMessage(MSG).build());
810+
r.runUntilComplete("in");
811+
});
812+
var inv = observe(viaInvoke, List.of("callee"), mode);
813+
assertEquals("PaymentError", inv.execution().name(), "invoke execution");
814+
assertEquals("PaymentError", inv.operations().get("callee").name(), "invoke operation");
815+
}
788816
}

‎telemetry-exporter/src/test/java/software/amazon/lambda/durable/telemetry/ErrorNameTest.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,11 @@ void hiddenClassNameDropsTheAddressSuffix() throws Throwable {
486486
.name());
487487
assertEquals("Lambda", DurableExecutionTelemetry.javaTypeName("pkg.Foo$$Lambda/0x1"));
488488
assertEquals("Error", DurableExecutionTelemetry.javaTypeName("/0x1"));
489+
assertEquals("Lambda", DurableExecutionTelemetry.javaTypeName("pkg.Foo$$Lambda/0x00007f3aBcD"));
490+
// Only a trailing /0x<hex> is a hidden-class suffix; any other '/' is part of the name.
491+
assertEquals("PaymentError", DurableExecutionTelemetry.javaTypeName("payments/v1.PaymentError"));
492+
assertEquals("Foo/0xZZ", DurableExecutionTelemetry.javaTypeName("pkg.Foo/0xZZ"));
493+
assertEquals("b/0x1", DurableExecutionTelemetry.javaTypeName("a.b/0x1/0x2"));
489494
}
490495

491496
/** Rule 6 blank: only the six ASCII whitespace characters; the Table A vectors cover each single character. */

0 commit comments

Comments
 (0)