Skip to content

Commit cc61a4c

Browse files
authored
fix(testing-sdk): fix duplicate start history event for retry updates (#356)
*Issue #, if available:* *Description of changes:* Populate execution history with correct event for step retries. There should be a StepFailed event on each retry update if there is an `Error` object, and `StepSucceeded` if it's not an `Error` object, not a StepStarted event. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent fafb936 commit cc61a4c

File tree

4 files changed

+203
-43
lines changed

4 files changed

+203
-43
lines changed

packages/aws-durable-execution-sdk-js-testing/src/checkpoint-server/storage/__tests__/event-processor.test.ts

Lines changed: 130 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ describe("EventProcessor", () => {
1616
const mockGetHistoryEventDetail = jest.mocked(
1717
historyEventDetails.getHistoryEventDetail,
1818
);
19+
const mockGetRetryHistoryEventDetail = jest.mocked(
20+
historyEventDetails.getRetryHistoryEventDetail,
21+
);
1922

2023
beforeEach(() => {
2124
eventProcessor = new EventProcessor();
@@ -345,6 +348,133 @@ describe("EventProcessor", () => {
345348
OperationType.EXECUTION,
346349
);
347350
});
351+
352+
it("should handle RETRY action with error using getRetryHistoryEventDetail", () => {
353+
const mockRetryHistoryDetails = {
354+
eventType: EventType.StepFailed,
355+
detailPlace: "StepFailedDetails" as const,
356+
getDetails: jest.fn().mockReturnValue({
357+
Error: {
358+
Payload: { ErrorType: "TestError", ErrorMessage: "Test error" },
359+
},
360+
RetryDetails: {
361+
NextAttemptDelaySeconds: 5,
362+
CurrentAttempt: 2,
363+
},
364+
}),
365+
};
366+
367+
mockGetRetryHistoryEventDetail.mockReturnValue(mockRetryHistoryDetails);
368+
369+
const update: OperationUpdate = {
370+
Id: "update-id",
371+
Name: "update-name",
372+
Action: OperationAction.RETRY,
373+
SubType: "update-subtype",
374+
Error: { ErrorType: "TestError", ErrorMessage: "Test error" },
375+
Type: undefined,
376+
};
377+
378+
const operation: Operation = {
379+
Id: "operation-id",
380+
Name: "operation-name",
381+
Type: OperationType.STEP,
382+
SubType: "operation-subtype",
383+
StartTimestamp: undefined,
384+
Status: undefined,
385+
};
386+
387+
const result = eventProcessor.processUpdate(update, operation);
388+
389+
expect(mockGetRetryHistoryEventDetail).toHaveBeenCalledWith(true);
390+
expect(mockGetHistoryEventDetail).not.toHaveBeenCalled();
391+
expect(result.EventType).toBe(EventType.StepFailed);
392+
expect(result.EventId).toBe(1);
393+
});
394+
395+
it("should handle RETRY action without error using getRetryHistoryEventDetail", () => {
396+
const mockSuccessRetryDetails = {
397+
eventType: EventType.StepSucceeded,
398+
detailPlace: "StepSucceededDetails" as const,
399+
getDetails: jest.fn().mockReturnValue({
400+
Result: { Payload: "test-result" },
401+
RetryDetails: {
402+
NextAttemptDelaySeconds: 5,
403+
CurrentAttempt: 2,
404+
},
405+
}),
406+
};
407+
408+
mockGetRetryHistoryEventDetail.mockReturnValue(mockSuccessRetryDetails);
409+
410+
const update: OperationUpdate = {
411+
Id: "update-id",
412+
Name: "update-name",
413+
Action: OperationAction.RETRY,
414+
SubType: "update-subtype",
415+
Payload: "test-result",
416+
Error: undefined,
417+
Type: undefined,
418+
};
419+
420+
const operation: Operation = {
421+
Id: "operation-id",
422+
Name: "operation-name",
423+
Type: OperationType.STEP,
424+
SubType: "operation-subtype",
425+
StartTimestamp: undefined,
426+
Status: undefined,
427+
};
428+
429+
const result = eventProcessor.processUpdate(update, operation);
430+
431+
expect(mockGetRetryHistoryEventDetail).toHaveBeenCalledWith(false);
432+
expect(mockGetHistoryEventDetail).not.toHaveBeenCalled();
433+
expect(result.EventType).toBe(EventType.StepSucceeded);
434+
expect(result.EventId).toBe(1);
435+
});
436+
437+
it("should handle RETRY action without payload and without error using getRetryHistoryEventDetail", () => {
438+
const mockSuccessRetryDetails = {
439+
eventType: EventType.StepSucceeded,
440+
detailPlace: "StepSucceededDetails" as const,
441+
getDetails: jest.fn().mockReturnValue({
442+
Result: { Payload: "test-result" },
443+
RetryDetails: {
444+
NextAttemptDelaySeconds: 5,
445+
CurrentAttempt: 2,
446+
},
447+
}),
448+
};
449+
450+
mockGetRetryHistoryEventDetail.mockReturnValue(mockSuccessRetryDetails);
451+
452+
const update: OperationUpdate = {
453+
Id: "update-id",
454+
Name: "update-name",
455+
Action: OperationAction.RETRY,
456+
SubType: "update-subtype",
457+
Payload: undefined,
458+
Error: undefined,
459+
Type: undefined,
460+
};
461+
462+
const operation: Operation = {
463+
Id: "operation-id",
464+
Name: "operation-name",
465+
Type: OperationType.STEP,
466+
SubType: "operation-subtype",
467+
StartTimestamp: undefined,
468+
Status: undefined,
469+
};
470+
471+
const result = eventProcessor.processUpdate(update, operation);
472+
473+
expect(mockGetRetryHistoryEventDetail).toHaveBeenCalledWith(false);
474+
expect(mockGetHistoryEventDetail).not.toHaveBeenCalled();
475+
expect(result.EventType).toBe(EventType.StepSucceeded);
476+
expect(result.EventId).toBe(1);
477+
});
348478
});
349479

350480
describe("getHistoryDetailsFromUpdate", () => {
@@ -407,18 +537,6 @@ describe("EventProcessor", () => {
407537
OperationType.CONTEXT,
408538
);
409539
});
410-
411-
it("should handle RETRY action with STEP type", () => {
412-
EventProcessor.getHistoryDetailsFromUpdate(
413-
OperationAction.RETRY,
414-
OperationType.STEP,
415-
);
416-
417-
expect(mockGetHistoryEventDetail).toHaveBeenCalledWith(
418-
OperationAction.RETRY,
419-
OperationType.STEP,
420-
);
421-
});
422540
});
423541

424542
describe("event ID management", () => {

packages/aws-durable-execution-sdk-js-testing/src/checkpoint-server/storage/event-processor.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
} from "@aws-sdk/client-lambda";
99
import {
1010
getHistoryEventDetail,
11+
getRetryHistoryEventDetail,
1112
HistoryEventDetail,
13+
OperationActionWithoutRetry,
1214
} from "../utils/history-event-details";
1315

1416
/**
@@ -102,10 +104,13 @@ export class EventProcessor {
102104
);
103105
}
104106

105-
const historyDetails = EventProcessor.getHistoryDetailsFromUpdate(
106-
update.Action,
107-
operation.Type,
108-
);
107+
const historyDetails =
108+
update.Action === OperationAction.RETRY
109+
? getRetryHistoryEventDetail(!!update.Error)
110+
: EventProcessor.getHistoryDetailsFromUpdate(
111+
update.Action,
112+
operation.Type,
113+
);
109114

110115
return {
111116
EventType: historyDetails.eventType,
@@ -131,7 +136,7 @@ export class EventProcessor {
131136
* This static method provides a way to validate that a specific operation action and type
132137
* combination is supported and to retrieve the corresponding event detail handler.
133138
*
134-
* @template Action - The operation action (START, FAIL, SUCCEED, RETRY)
139+
* @template Action - The operation action (START, FAIL, SUCCEED)
135140
* @template Type - The operation type (EXECUTION, CALLBACK, CONTEXT, INVOKE, STEP, WAIT)
136141
* @param action - The action being performed on the operation
137142
* @param type - The type of operation being performed
@@ -149,7 +154,7 @@ export class EventProcessor {
149154
* ```
150155
*/
151156
static getHistoryDetailsFromUpdate<
152-
Action extends OperationAction,
157+
Action extends OperationActionWithoutRetry,
153158
Type extends OperationType,
154159
>(action: Action, type: Type): HistoryEventDetail<Action, Type> {
155160
const historyDetails = getHistoryEventDetail(action, type);

packages/aws-durable-execution-sdk-js-testing/src/checkpoint-server/utils/__tests__/history-event-details.test.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
Operation,
77
ErrorObject,
88
} from "@aws-sdk/client-lambda";
9-
import { getHistoryEventDetail } from "../history-event-details";
9+
import {
10+
getHistoryEventDetail,
11+
getRetryHistoryEventDetail,
12+
OperationActionWithoutRetry,
13+
} from "../history-event-details";
1014

1115
describe("history-event-details", () => {
1216
const mockMetadata = {
@@ -63,7 +67,7 @@ describe("history-event-details", () => {
6367
describe("getHistoryEventDetail", () => {
6468
it("should return undefined for unknown action-type combinations", () => {
6569
const result = getHistoryEventDetail(
66-
"UNKNOWN" as OperationAction,
70+
"UNKNOWN" as OperationActionWithoutRetry,
6771
"UNKNOWN" as OperationType,
6872
);
6973
expect(result).toBeUndefined();
@@ -312,20 +316,12 @@ describe("history-event-details", () => {
312316
expect(details).toEqual({});
313317
});
314318

315-
it("should return correct details for RETRY-STEP", () => {
319+
it("should return undefined for RETRY-STEP", () => {
316320
const detail = getHistoryEventDetail(
317-
OperationAction.RETRY,
321+
OperationAction.RETRY as OperationActionWithoutRetry,
318322
OperationType.STEP,
319323
);
320-
expect(detail).toBeDefined();
321-
expect(detail!.eventType).toBe(EventType.StepStarted);
322-
expect(detail!.detailPlace).toBe("StepStartedDetails");
323-
324-
const update = createMockUpdate();
325-
const operation = createMockOperation();
326-
const details = detail!.getDetails(update, operation, mockMetadata);
327-
328-
expect(details).toEqual({});
324+
expect(detail).toBeUndefined();
329325
});
330326

331327
it("should return correct details for FAIL-STEP", () => {
@@ -367,7 +363,7 @@ describe("history-event-details", () => {
367363
},
368364
RetryDetails: {
369365
NextAttemptDelaySeconds: 5,
370-
CurrentAttempt: undefined,
366+
CurrentAttempt: 1,
371367
},
372368
});
373369
});
@@ -529,6 +525,28 @@ describe("history-event-details", () => {
529525
});
530526
});
531527

528+
describe("getRetryHistoryEventDetail", () => {
529+
it("should return FAIL-STEP handler when isError is true", () => {
530+
const retryFailDetail = getRetryHistoryEventDetail(true);
531+
const directFailDetail = getHistoryEventDetail(
532+
OperationAction.FAIL,
533+
OperationType.STEP,
534+
);
535+
536+
expect(retryFailDetail).toBe(directFailDetail);
537+
});
538+
539+
it("should return SUCCEED-STEP handler when isError is false", () => {
540+
const retrySucceedDetail = getRetryHistoryEventDetail(false);
541+
const directSucceedDetail = getHistoryEventDetail(
542+
OperationAction.SUCCEED,
543+
OperationType.STEP,
544+
);
545+
546+
expect(retrySucceedDetail).toBe(directSucceedDetail);
547+
});
548+
});
549+
532550
describe("edge cases", () => {
533551
it("should handle empty metadata", () => {
534552
const detail = getHistoryEventDetail(

packages/aws-durable-execution-sdk-js-testing/src/checkpoint-server/utils/history-event-details.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import {
77
Operation,
88
} from "@aws-sdk/client-lambda";
99

10+
export type OperationActionWithoutRetry = Exclude<
11+
OperationAction,
12+
typeof OperationAction.RETRY
13+
>;
14+
1015
/**
1116
* Metadata required for generating event details.
1217
*/
@@ -57,11 +62,11 @@ function createEventDetails<T extends keyof Event>(
5762
/**
5863
* Type alias for retrieving the correct HistoryEventDetails type based on operation action and type.
5964
*
60-
* @template Action - The operation action (START, FAIL, SUCCEED, RETRY)
65+
* @template Action - The operation action (START, FAIL, SUCCEED)
6166
* @template Type - The operation type (EXECUTION, CALLBACK, CONTEXT, INVOKE, STEP, WAIT)
6267
*/
6368
export type HistoryEventDetail<
64-
Action extends OperationAction,
69+
Action extends OperationActionWithoutRetry,
6570
Type extends OperationType,
6671
> = (typeof historyEventDetailMap)[`${Action}-${Type}`];
6772

@@ -76,7 +81,7 @@ export type HistoryEventDetail<
7681
* - Callback: START
7782
* - Context: START, FAIL, SUCCEED
7883
* - Invoke: START
79-
* - Step: START, RETRY, FAIL, SUCCEED
84+
* - Step: START, FAIL, SUCCEED
8085
* - Wait: START
8186
*/
8287
const historyEventDetailMap = {
@@ -170,11 +175,6 @@ const historyEventDetailMap = {
170175
"StepStartedDetails",
171176
() => ({}),
172177
),
173-
[`${OperationAction.RETRY}-${OperationType.STEP}`]: createEventDetails(
174-
EventType.StepStarted,
175-
"StepStartedDetails",
176-
() => ({}),
177-
),
178178
[`${OperationAction.FAIL}-${OperationType.STEP}`]: createEventDetails(
179179
EventType.StepFailed,
180180
"StepFailedDetails",
@@ -184,7 +184,7 @@ const historyEventDetailMap = {
184184
},
185185
RetryDetails: {
186186
NextAttemptDelaySeconds: update.StepOptions?.NextAttemptDelaySeconds,
187-
CurrentAttempt: operation.StepDetails?.Attempt,
187+
CurrentAttempt: operation.StepDetails?.Attempt ?? 1,
188188
},
189189
}),
190190
),
@@ -223,7 +223,7 @@ const historyEventDetailMap = {
223223
/**
224224
* Retrieves the appropriate history event detail handler for a given operation action and type.
225225
*
226-
* @template Action - The operation action (START, FAIL, SUCCEED, RETRY)
226+
* @template Action - The operation action (START, FAIL, SUCCEED)
227227
* @template Type - The operation type (EXECUTION, CALLBACK, CONTEXT, INVOKE, STEP, WAIT)
228228
* @param action - The action being performed on the operation
229229
* @param type - The type of operation being performed
@@ -238,8 +238,27 @@ const historyEventDetailMap = {
238238
* ```
239239
*/
240240
export function getHistoryEventDetail<
241-
Action extends OperationAction,
241+
Action extends OperationActionWithoutRetry,
242242
Type extends OperationType,
243243
>(action: Action, type: Type): HistoryEventDetail<Action, Type> | undefined {
244244
return historyEventDetailMap[`${action}-${type}`];
245245
}
246+
247+
/**
248+
* Retry updates can create either a FAIL or SUCCEED event depending on the outcome of the retry.
249+
* This function returns the appropriate HistoryEventDetail handler based on the payload of the retry.
250+
*
251+
* @param isError
252+
* @returns
253+
*/
254+
export function getRetryHistoryEventDetail(isError: boolean) {
255+
if (isError) {
256+
return historyEventDetailMap[
257+
`${OperationAction.FAIL}-${OperationType.STEP}`
258+
];
259+
}
260+
261+
return historyEventDetailMap[
262+
`${OperationAction.SUCCEED}-${OperationType.STEP}`
263+
];
264+
}

0 commit comments

Comments
 (0)