Skip to content

Commit f6350db

Browse files
committed
refactor(telemetry): rely on the core's wrapped-error checkpoints (spec v3)
The core now checkpoints the wrapped error of a failed context (fix/checkpoint-wrapped-error), so a failed step or context always has an error type. Drop the exporter fallback that named a typeless step or context after the SDK failure class the core would throw; keep it only for callbacks and invokes, whose callers can leave the type out and whose timeouts carry no error. A stopped or timed-out invoke inside a child context now has one name for the invoke, every enclosing context and the execution, live and replayed; add nested stop/timeout scenarios. Table B v3: B21 asserts the execution names the branch error that propagated; B5, B6 and B20 stay not applicable because Java map does not throw. README states rule 9 and the core requirement as in spec v3.
1 parent 8af7e04 commit f6350db

4 files changed

Lines changed: 93 additions & 63 deletions

File tree

‎telemetry-exporter/README.md‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,17 @@ this rule. The rule applies to the execution-level `error`, every `operations[].
263263
7. A failure gets the same name whether the record is built live or from a replayed checkpoint.
264264
8. When the execution fails because an operation's error propagated, the execution `error.name`
265265
equals that operation's `error.name`.
266-
9. If the SDK cannot tell which of several failures caused a wrapper, it does not guess. It
267-
reports the wrapper's name. Two map items or parallel branches that fail with the same message
268-
but different types are an example.
266+
9. If a checkpoint holds only a wrapper type, which an older SDK can write, the name is that
267+
wrapper's name. The exporter never guesses a cause from other operations.
268+
269+
The core SDK checkpoints the wrapped error, not the wrapper. When a child context, map item or
270+
parallel branch fails because an SDK wrapper error escaped it, the Java SDK writes the original
271+
error's `ErrorType`, `ErrorMessage`, `ErrorData` and `StackTrace` into that context's checkpoint.
272+
An SDK error that names the failure itself (a timed-out callback, a timed-out or stopped invoke) is
273+
written under its own class name. A `waitForCallback` context keeps the callback's own error. So
274+
the exporter names an operation by reducing its own `ErrorType` (rule 6) and names the execution
275+
by unwrapping its error's cause chain (rules 2 to 5). It keeps no matching by message, timestamp or
276+
tree position.
269277

270278
A callback that is failed externally with an explicit `ErrorType` is named after that type, after
271279
the reduction of rule 6. This includes the type `"Error"`, which reports `Error`. Only a missing or
@@ -304,22 +312,27 @@ How the Java exporter applies it:
304312
timeout. The operation and the execution report `CallbackTimeoutException`,
305313
`InvokeTimedOutException` or `InvokeStoppedException`, whatever error type the checkpoint
306314
carries. A stopped invoke is not named after its stop error: a stop with `errorType`
307-
`States.Stopped` or `com.acme.PaymentError` reports `InvokeStoppedException`.
315+
`States.Stopped` or `com.acme.PaymentError` reports `InvokeStoppedException`. Inside a child
316+
context the core checkpoints that SDK error as the context's error, so the invoke, every
317+
enclosing context and the execution report the same name, live and replayed.
308318
- After 16 unwraps, or at a cycle, the reported wrapper's name is its checkpointed `errorType`,
309319
reduced; with a blank type it is the SDK failure class below.
310-
- When an operation failed with no error type (for example a callback failed externally without an
311-
`errorType`), the operation and the execution report the SDK exception the core throws for it,
312-
such as `CallbackFailedException`.
320+
- When a callback or invoke failed with no error type (for example a callback failed externally
321+
without an `errorType`), the operation and the execution report the SDK exception the core throws
322+
for it, such as `CallbackFailedException`. A failed step or context always has a type in its
323+
checkpoint. A context checkpoint written by an older SDK without one reports the wrapper's name
324+
for the execution and no operation error (rule 9).
313325
- When the checkpoint has no error at all, the operation error has the SDK exception's name and a
314326
`null` message, because there is no original message to keep. This is the case for a timed-out
315327
callback and a timed-out invoke in the local test runner, and for a stopped invoke stopped
316328
without an error. Before this change such operations had no `error` object; a reader that tests
317329
`error != null` now sees one with `"message": null`. The execution error of a timed-out callback
318330
has the message `Callback timed out: <callbackId>`, built by `CallbackTimeoutException`; it is not
319331
checkpointed, and the JS and Python exporters report `Callback timed out` without the id.
320-
- Java `map` and `parallel` return item and branch failures as data and do not throw them, so the
321-
SDK never has to choose between several failures (rule 9). The execution error is whatever the
322-
handler throws.
332+
- Java `map` and `parallel` return item and branch failures as data and do not throw them. Each
333+
item and branch is named after its own type. For `parallel` the execution error is the error of
334+
the branch whose future the handler calls `get()` on (Table B, B21); `map` has no `throwIfError`,
335+
so B5, B6 and B20 have no propagated execution error in Java.
323336
- Non-class type strings are reduced too: an external callback failure with `errorType`
324337
`States.Timeout` reports `Timeout`.
325338

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

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import java.util.Collections;
77
import java.util.IdentityHashMap;
88
import java.util.Set;
9-
import java.util.regex.Pattern;
109
import java.util.function.Function;
1110
import java.util.function.UnaryOperator;
11+
import java.util.regex.Pattern;
1212
import org.slf4j.Logger;
1313
import org.slf4j.LoggerFactory;
1414
import software.amazon.awssdk.services.lambda.model.ErrorObject;
@@ -36,7 +36,6 @@
3636
import software.amazon.lambda.durable.exception.StepInterruptedException;
3737
import software.amazon.lambda.durable.exception.UnrecoverableDurableExecutionException;
3838
import software.amazon.lambda.durable.exception.WaitForConditionFailedException;
39-
import software.amazon.lambda.durable.model.OperationSubType;
4039
import software.amazon.lambda.durable.plugin.DurableExecutionPluginFactory;
4140
import software.amazon.lambda.durable.plugin.InvocationEndInfo;
4241
import software.amazon.lambda.durable.plugin.InvocationInfo;
@@ -350,21 +349,16 @@ private static String sdkErrorOfOperation(Throwable wrapper) {
350349
}
351350

352351
/**
353-
* Returns the simple name of the SDK exception the core throws when an operation of this type, sub-type and status
354-
* fails with no reconstructable user error, or {@code null} when there is none. Used when the checkpoint carries no
355-
* error type, so an operation record and the execution error it propagates to get the same name.
352+
* Returns the simple name of the SDK exception the core throws for a callback or invoke that failed, timed out or
353+
* was stopped with no error type: a callback or invoke caller can leave the type out, and a timeout carries no
354+
* error. {@code null} for any other operation. A failed step or context always checkpoints a type, because the core
355+
* checkpoints the wrapped error of a failed context; an older SDK's checkpoint without one reports no name.
356356
*/
357357
static String sdkFailureName(String type, String subType, OperationStatus status) {
358358
if (type == null || status == null) {
359359
return null;
360360
}
361361
Class<?> failure = switch (type) {
362-
case "STEP" ->
363-
status != OperationStatus.FAILED
364-
? null
365-
: OperationSubType.WAIT_FOR_CONDITION.getValue().equals(subType)
366-
? WaitForConditionFailedException.class
367-
: StepFailedException.class;
368362
case "CALLBACK" ->
369363
switch (status) {
370364
case FAILED -> CallbackFailedException.class;
@@ -378,26 +372,11 @@ static String sdkFailureName(String type, String subType, OperationStatus status
378372
case STOPPED -> InvokeStoppedException.class;
379373
default -> null;
380374
};
381-
case "CONTEXT" -> status != OperationStatus.FAILED ? null : contextFailure(subType);
382375
default -> null;
383376
};
384377
return failure == null ? null : failure.getSimpleName();
385378
}
386379

387-
private static Class<?> contextFailure(String subType) {
388-
if (OperationSubType.MAP_ITERATION.getValue().equals(subType)) {
389-
return MapIterationFailedException.class;
390-
}
391-
if (OperationSubType.PARALLEL_BRANCH.getValue().equals(subType)) {
392-
return ParallelBranchFailedException.class;
393-
}
394-
if (OperationSubType.RUN_IN_CHILD_CONTEXT.getValue().equals(subType)
395-
|| OperationSubType.WITH_RETRY.getValue().equals(subType)) {
396-
return ChildContextFailedException.class;
397-
}
398-
return null;
399-
}
400-
401380
/** The address suffix of a hidden class's {@link Class#getName()}, which changes on every JVM start. */
402381
private static final Pattern HIDDEN_CLASS_SUFFIX = Pattern.compile("/0x[0-9a-fA-F]+$");
403382

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

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,8 @@ private static void check(
307307
"B6",
308308
"same as B5: Java map does not throw item failures",
309309
"B20",
310-
"same as B5: Java map does not throw item failures, so the SDK never has to choose between the two items"
311-
+ " (rule 9)",
312-
"B21",
313-
"Java parallel returns a ParallelResult of branch statuses and does not throw; only the branch future the"
314-
+ " handler calls get() on throws, and it throws that branch's own error, so the SDK never has to"
315-
+ " choose between branches (rule 9)");
310+
"same as B5: Java map does not throw item failures and has no throwIfError, so no item's error propagates;"
311+
+ " each item's own type is still asserted");
316312

317313
enum Row {
318314
B1,
@@ -662,7 +658,7 @@ void tableB(Row row) {
662658
MSG,
663659
Map.of("fanout-iteration-0", "PaymentError", "fanout-iteration-1", "FraudError"),
664660
MSG);
665-
case B21 -> // Two parallel branches fail with the same message and different types.
661+
case B21 -> // Two parallel branches fail like B20; the execution names the branch error that propagated.
666662
check(
667663
"B21",
668664
m -> (in, ctx) -> failure(m, ctx, c -> {
@@ -678,7 +674,9 @@ void tableB(Row row) {
678674
}
679675
}),
680676
null,
681-
null, // execution: not applicable in Java, see EXECUTION_NOT_APPLICABLE
677+
// Java parallel does not throw; the error that propagates is the one of the branch whose
678+
// future the handler calls get() on.
679+
"PaymentError",
682680
MSG,
683681
Map.of("pay", "PaymentError", "fraud", "FraudError"),
684682
MSG);
@@ -786,6 +784,58 @@ void invokeTimeoutAndStopAreSdkErrors(Mode mode) {
786784
}
787785
}
788786

787+
/**
788+
* An invoke that is stopped or times out inside a child context: the invoke, the enclosing context and the
789+
* execution report the same SDK error name, live and replayed, whatever error type a stop carries. The core
790+
* checkpoints the SDK error, not the stop error's type, as the context's error. Nested two contexts deep as well.
791+
*/
792+
@ParameterizedTest
793+
@EnumSource(Mode.class)
794+
void stoppedOrTimedOutInvokeInsideAChildHasOneName(Mode mode) {
795+
for (boolean nested : new boolean[] {false, true}) {
796+
for (String outcome : List.of("timeout", "stop-states", "stop-user")) {
797+
String row = (nested ? "nested " : "") + outcome + " " + mode;
798+
Function<DurableContext, String> invokeInChild = c ->
799+
c.runInChildContext("child", String.class, cc -> cc.invoke("callee", "fn", "x", String.class));
800+
var records = run(
801+
(in, ctx) -> failure(
802+
mode,
803+
ctx,
804+
nested
805+
? c -> c.runInChildContext("outer", String.class, invokeInChild::apply)
806+
: invokeInChild),
807+
r -> {
808+
r.run("in");
809+
switch (outcome) {
810+
case "timeout" -> r.timeoutChainedInvoke("callee");
811+
case "stop-states" ->
812+
r.stopChainedInvoke(
813+
"callee",
814+
ErrorObject.builder()
815+
.errorType("States.Stopped")
816+
.errorMessage(MSG)
817+
.build());
818+
default ->
819+
r.stopChainedInvoke(
820+
"callee",
821+
ErrorObject.builder()
822+
.errorType("com.acme.PaymentError")
823+
.errorMessage(MSG)
824+
.build());
825+
}
826+
r.runUntilComplete("in");
827+
});
828+
String expected = "timeout".equals(outcome) ? "InvokeTimedOutException" : "InvokeStoppedException";
829+
var names = nested ? List.of("callee", "child", "outer") : List.of("callee", "child");
830+
var obs = observe(records, names, mode);
831+
assertEquals(expected, obs.execution().name(), row + " execution");
832+
for (String name : names) {
833+
assertEquals(expected, obs.operations().get(name).name(), row + " " + name);
834+
}
835+
}
836+
}
837+
}
838+
789839
/**
790840
* Callback and invoke error types come from outside the JVM: a {@code '/'} in them is not a hidden-class suffix and
791841
* the type is reduced as a whole (Table A).

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

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,8 @@ void sdkErrorsThatAreNotWrappersReportTheirOwnName() {
464464
}
465465

466466
/**
467-
* R2 (Java only): a hidden class's name ends in {@code /0x…}, which changes on every JVM start. The name is cut at
468-
* the first {@code '/'} before the reduction, live and from the checkpoint.
467+
* R2 (Java only): a hidden class's name ends in {@code /0x…}, which changes on every JVM start. Only that trailing
468+
* {@code /0x<hex>} is removed before the reduction, live and from the checkpoint.
469469
*/
470470
@Test
471471
void hiddenClassNameDropsTheAddressSuffix() throws Throwable {
@@ -549,12 +549,6 @@ void blankCheckpointedTypeUsesTheSdkFailureClassOfTheOperation() {
549549

550550
@Test
551551
void sdkFailureNamesMatchTheCoreExceptions() {
552-
assertEquals(
553-
"StepFailedException",
554-
DurableExecutionTelemetry.sdkFailureName("STEP", "Step", OperationStatus.FAILED));
555-
assertEquals(
556-
"WaitForConditionFailedException",
557-
DurableExecutionTelemetry.sdkFailureName("STEP", "WaitForCondition", OperationStatus.FAILED));
558552
assertEquals(
559553
"CallbackFailedException",
560554
DurableExecutionTelemetry.sdkFailureName("CALLBACK", "Callback", OperationStatus.FAILED));
@@ -570,17 +564,11 @@ void sdkFailureNamesMatchTheCoreExceptions() {
570564
assertEquals(
571565
"InvokeStoppedException",
572566
DurableExecutionTelemetry.sdkFailureName("CHAINED_INVOKE", "ChainedInvoke", OperationStatus.STOPPED));
573-
assertEquals(
574-
"MapIterationFailedException",
575-
DurableExecutionTelemetry.sdkFailureName("CONTEXT", "MapIteration", OperationStatus.FAILED));
576-
assertEquals(
577-
"ParallelBranchFailedException",
578-
DurableExecutionTelemetry.sdkFailureName("CONTEXT", "ParallelBranch", OperationStatus.FAILED));
579-
assertEquals(
580-
"ChildContextFailedException",
581-
DurableExecutionTelemetry.sdkFailureName("CONTEXT", "RunInChildContext", OperationStatus.FAILED));
582-
assertNull(DurableExecutionTelemetry.sdkFailureName("CONTEXT", "WaitForCallback", OperationStatus.FAILED));
583-
assertNull(DurableExecutionTelemetry.sdkFailureName("STEP", "Step", OperationStatus.SUCCEEDED));
567+
// A failed step or context always checkpoints its wrapped error's type, so there is no fallback for them.
568+
assertNull(DurableExecutionTelemetry.sdkFailureName("STEP", "Step", OperationStatus.FAILED));
569+
assertNull(DurableExecutionTelemetry.sdkFailureName("CONTEXT", "RunInChildContext", OperationStatus.FAILED));
570+
assertNull(DurableExecutionTelemetry.sdkFailureName("CONTEXT", "MapIteration", OperationStatus.FAILED));
571+
assertNull(DurableExecutionTelemetry.sdkFailureName("CALLBACK", "Callback", OperationStatus.SUCCEEDED));
584572
assertNull(DurableExecutionTelemetry.sdkFailureName("WAIT", "Wait", OperationStatus.FAILED));
585573
assertNull(DurableExecutionTelemetry.sdkFailureName(null, null, null));
586574
}

0 commit comments

Comments
 (0)