Skip to content

Commit d2d3dd9

Browse files
authored
Merge pull request #69 from nibble-4bits/feature/state-failed-retried-caught-event-logs
Feature/StateFailed, StateRetried, StateCaught event logs
2 parents 8686fcd + 32a55d5 commit d2d3dd9

File tree

8 files changed

+453
-63
lines changed

8 files changed

+453
-63
lines changed

__tests__/EventLogger.test.ts

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import type {
44
ExecutionStartedEvent,
55
ExecutionSucceededEvent,
66
ExecutionTerminatedEvent,
7-
StateEvent,
7+
StateEnteredEvent,
8+
StateExitedEvent,
89
} from '../src/typings/EventLogs';
10+
import { StatesRuntimeError } from '../src/error/predefined/StatesRuntimeError';
911
import { EventLogger } from '../src/stateMachine/EventLogger';
1012
import './_customMatchers';
1113

@@ -27,34 +29,70 @@ describe('Event Logger', () => {
2729

2830
eventLogger.dispatchExecutionStartedEvent(50);
2931
eventLogger.dispatchStateEnteredEvent('SomeState', 'Choice', { a: 1, b: 'string', c: true, d: [1, 2, 3] });
32+
eventLogger.dispatchStateFailedEvent(
33+
'A state name',
34+
'Parallel',
35+
12345,
36+
new StatesRuntimeError('An error happened during runtime')
37+
);
38+
eventLogger.dispatchStateRetriedEvent('A state name', 'Parallel', 12345, { ErrorEquals: ['States.ALL'] }, 2);
39+
eventLogger.dispatchStateCaughtEvent('A state name', 'Parallel', 12345, {
40+
ErrorEquals: ['States.ALL'],
41+
Next: 'CatchState',
42+
});
3043
eventLogger.dispatchStateExitedEvent('AnotherState', 'Task', ['a', 'b', 1, null], 123.456);
3144
eventLogger.dispatchExecutionSucceededEvent('result');
3245

33-
const firstEvent = await generator.next();
34-
const secondEvent = await generator.next();
35-
const thirdEvent = await generator.next();
36-
const fourthEvent = await generator.next();
37-
const endEvent = await generator.next();
38-
39-
expect(firstEvent.value).toEqual({ type: 'ExecutionStarted', timestamp: 1670198400000, input: 50 });
40-
expect(secondEvent.value).toEqual({
46+
const event1 = await generator.next();
47+
const event2 = await generator.next();
48+
const event3 = await generator.next();
49+
const event4 = await generator.next();
50+
const event5 = await generator.next();
51+
const event6 = await generator.next();
52+
const event7 = await generator.next();
53+
const event8 = await generator.next();
54+
55+
expect(event1.value).toEqual({ type: 'ExecutionStarted', timestamp: 1670198400000, input: 50 });
56+
expect(event2.value).toEqual({
4157
type: 'StateEntered',
4258
timestamp: 1670198400000,
4359
state: { name: 'SomeState', type: 'Choice', input: { a: 1, b: 'string', c: true, d: [1, 2, 3] } },
4460
});
45-
expect(thirdEvent.value).toEqual({
61+
expect(event3.value).toEqual({
62+
type: 'StateFailed',
63+
timestamp: 1670198400000,
64+
state: { name: 'A state name', type: 'Parallel', input: 12345 },
65+
Error: 'States.Runtime',
66+
Cause: 'An error happened during runtime',
67+
});
68+
expect(event4.value).toEqual({
69+
type: 'StateRetried',
70+
timestamp: 1670198400000,
71+
state: { name: 'A state name', type: 'Parallel', input: 12345 },
72+
retry: { retrier: { ErrorEquals: ['States.ALL'] }, attempt: 2 },
73+
});
74+
expect(event5.value).toEqual({
75+
type: 'StateCaught',
76+
timestamp: 1670198400000,
77+
state: { name: 'A state name', type: 'Parallel', input: 12345 },
78+
catch: { catcher: { ErrorEquals: ['States.ALL'], Next: 'CatchState' } },
79+
});
80+
expect(event6.value).toEqual({
4681
type: 'StateExited',
4782
timestamp: 1670198400000,
4883
state: { name: 'AnotherState', type: 'Task', input: ['a', 'b', 1, null], output: 123.456 },
4984
});
50-
expect(fourthEvent.value).toEqual({ type: 'ExecutionSucceeded', timestamp: 1670198400000, output: 'result' });
51-
expect(endEvent.value).toBeUndefined();
52-
53-
expect(firstEvent.done).toBe(false);
54-
expect(secondEvent.done).toBe(false);
55-
expect(thirdEvent.done).toBe(false);
56-
expect(fourthEvent.done).toBe(false);
57-
expect(endEvent.done).toBe(true);
85+
expect(event7.value).toEqual({ type: 'ExecutionSucceeded', timestamp: 1670198400000, output: 'result' });
86+
expect(event8.value).toBeUndefined();
87+
88+
expect(event1.done).toBe(false);
89+
expect(event2.done).toBe(false);
90+
expect(event3.done).toBe(false);
91+
expect(event4.done).toBe(false);
92+
expect(event5.done).toBe(false);
93+
expect(event6.done).toBe(false);
94+
expect(event7.done).toBe(false);
95+
expect(event8.done).toBe(true);
5896
});
5997
});
6098

@@ -126,7 +164,7 @@ describe('Event Logger', () => {
126164
test('should forward `StateEntered` event and add index', async () => {
127165
const eventLogger = new EventLogger();
128166
const generator = eventLogger.getEvents();
129-
const event: StateEvent = {
167+
const event: StateEnteredEvent = {
130168
type: 'StateEntered',
131169
timestamp: Date.now(),
132170
state: { name: 'SomeEvent', type: 'Succeed', input: {} },
@@ -148,7 +186,7 @@ describe('Event Logger', () => {
148186
test('should forward `StateExited` event and add index', async () => {
149187
const eventLogger = new EventLogger();
150188
const generator = eventLogger.getEvents();
151-
const event: StateEvent = {
189+
const event: StateExitedEvent = {
152190
type: 'StateExited',
153191
timestamp: Date.now(),
154192
state: { name: 'SomeEvent', type: 'Succeed', input: {}, output: {} },

__tests__/StateMachine.test.ts

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,19 +255,30 @@ describe('State Machine', () => {
255255
PassState1: {
256256
Type: 'Pass',
257257
Result: 1,
258-
Next: 'FailState',
258+
Next: 'TaskState',
259259
},
260-
FailState: {
261-
Type: 'Fail',
262-
Error: 'MachineFailure',
263-
Cause: 'The state machine failed',
260+
TaskState: {
261+
Type: 'Task',
262+
Resource: 'arn:aws:lambda:us-east-1:123456789012:function:AddNumbers',
263+
Retry: [{ ErrorEquals: ['SyntaxError'], MaxAttempts: 1 }],
264+
Catch: [{ ErrorEquals: ['SyntaxError'], Next: 'FailExecution' }],
265+
End: true,
264266
},
267+
FailExecution: { Type: 'Fail', Error: 'MachineFailure', Cause: 'The state machine failed' },
265268
},
266269
};
267270
const input = {};
268271

269272
const stateMachine = new StateMachine(machineDefinition);
270-
const execution = stateMachine.run(input);
273+
const execution = stateMachine.run(input, {
274+
overrides: {
275+
taskResourceLocalHandlers: {
276+
TaskState: () => {
277+
throw new SyntaxError('Unknown token at position 12');
278+
},
279+
},
280+
},
281+
});
271282

272283
const events: EventLog[] = [];
273284
for await (const event of execution.eventLogs) {
@@ -283,7 +294,63 @@ describe('State Machine', () => {
283294
timestamp: 1670198400000,
284295
state: { name: 'PassState1', type: 'Pass', input: {}, output: 1 },
285296
},
286-
{ type: 'StateEntered', timestamp: 1670198400000, state: { name: 'FailState', type: 'Fail', input: 1 } },
297+
{ type: 'StateEntered', timestamp: 1670198400000, state: { name: 'TaskState', type: 'Task', input: 1 } },
298+
{
299+
type: 'StateFailed',
300+
timestamp: 1670198400000,
301+
state: { name: 'TaskState', type: 'Task', input: 1 },
302+
Error: 'SyntaxError',
303+
Cause: 'Unknown token at position 12',
304+
},
305+
{
306+
type: 'StateRetried',
307+
timestamp: 1670198400000,
308+
state: { name: 'TaskState', type: 'Task', input: 1 },
309+
retry: { retrier: { ErrorEquals: ['SyntaxError'], MaxAttempts: 1 }, attempt: 1 },
310+
},
311+
{
312+
type: 'StateFailed',
313+
timestamp: 1670198400000,
314+
state: { name: 'TaskState', type: 'Task', input: 1 },
315+
Error: 'SyntaxError',
316+
Cause: 'Unknown token at position 12',
317+
},
318+
{
319+
type: 'StateCaught',
320+
timestamp: 1670198400000,
321+
state: { name: 'TaskState', type: 'Task', input: 1 },
322+
catch: { catcher: { ErrorEquals: ['SyntaxError'], Next: 'FailExecution' } },
323+
},
324+
{
325+
type: 'StateExited',
326+
timestamp: 1670198400000,
327+
state: {
328+
name: 'TaskState',
329+
type: 'Task',
330+
input: 1,
331+
output: { Error: 'SyntaxError', Cause: 'Unknown token at position 12' },
332+
},
333+
},
334+
{
335+
type: 'StateEntered',
336+
timestamp: 1670198400000,
337+
state: {
338+
name: 'FailExecution',
339+
type: 'Fail',
340+
input: { Error: 'SyntaxError', Cause: 'Unknown token at position 12' },
341+
},
342+
},
343+
{
344+
type: 'StateFailed',
345+
timestamp: 1670198400000,
346+
state: {
347+
name: 'FailExecution',
348+
type: 'Fail',
349+
input: { Error: 'SyntaxError', Cause: 'Unknown token at position 12' },
350+
},
351+
Error: 'MachineFailure',
352+
Cause: 'The state machine failed',
353+
},
287354
{
288355
type: 'ExecutionFailed',
289356
timestamp: 1670198400000,

docs/execution-event-logs.md

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
- [`ParallelBranchFailed` event](#parallelbranchfailed-event)
1818
- [`StateEntered` event](#stateentered-event)
1919
- [`StateExited` event](#stateexited-event)
20+
- [`StateFailed` event](#statefailed-event)
21+
- [`StateRetried` event](#stateretried-event)
22+
- [`StateCaught` event](#statecaught-event)
2023
- [Helper data types](#helper-data-types)
2124
- [`StateData`](#statedata)
2225

@@ -80,7 +83,7 @@ The `ExecutionFailed` event is produced when the execution encounters an error a
8083
- `Error`: Name of the error that caused the failure.
8184
- `Cause`: This field can either be a string or an object:
8285
- `string`: Contains a description explaining why the execution failed.
83-
- `object`: Contains details that provide information as to why the execution failed.
86+
- `object`: Contains details that provide more information as to why the execution failed.
8487

8588
#### Example
8689

@@ -206,7 +209,7 @@ The `MapIterationFailed` event is produced when an iteration in a `Map` encounte
206209
- `Error`: Name of the error that caused the failure.
207210
- `Cause`: This field can either be a string or an object:
208211
- `string`: Contains a description explaining why the iteration failed.
209-
- `object`: Contains details that provide information as to why the iteration failed.
212+
- `object`: Contains details that provide more information as to why the iteration failed.
210213

211214
#### Example
212215

@@ -292,7 +295,7 @@ The `ParallelBranchFailed` event is produced when a branch in a `Parallel` state
292295
- `Error`: Name of the error that caused the failure.
293296
- `Cause`: This field can either be a string or an object:
294297
- `string`: Contains a description explaining why the branch failed.
295-
- `object`: Contains details that provide information as to why the branch failed.
298+
- `object`: Contains details that provide more information as to why the branch failed.
296299

297300
#### Example
298301

@@ -361,6 +364,99 @@ The `StateExited` event is produced when the execution transitions out of a stat
361364
}
362365
```
363366

367+
---
368+
369+
### `StateFailed` event
370+
371+
The `StateFailed` event is produced when the state that is currently being executed encounters an error and fails.
372+
373+
#### Additional fields
374+
375+
- `state`: An object of type [`StateData`](#statedata) containing data associated with the state that failed.
376+
- `Error`: Name of the error that caused the state to fail.
377+
- `Cause`: This field can either be a string or an object:
378+
- `string`: Contains a description explaining why the state failed.
379+
- `object`: Contains details that provide more information as to why the state failed.
380+
- `index?`: The index of the `Map` iteration in which this state failed. This property is only set if this state was executed within a `Map` state.
381+
382+
#### Example
383+
384+
```js
385+
{
386+
type: 'StateFailed',
387+
timestamp: 1234567890123,
388+
state: {
389+
name: 'ReadDataFile',
390+
type: 'Task',
391+
input: { filePath: '/home/user/data.csv' }
392+
},
393+
Error: 'ReadError',
394+
Cause: 'Unable to read file "data.csv". The file is not properly formatted as CSV.'
395+
}
396+
```
397+
398+
---
399+
400+
### `StateRetried` event
401+
402+
The `StateRetried` event is produced when a state fails and it's retried because it matched the error specified by a retrier in the `Retry` field.
403+
404+
#### Additional fields
405+
406+
- `state`: An object of type [`StateData`](#statedata) containing data associated with the state that is being retried.
407+
- `retry`: An object of type [`RetryData`](#retrydata) containing data associated with the retry attempt.
408+
- `index?`: The index of the `Map` iteration in which this state is being retried. This property is only set if this state is being executed within a `Map` state.
409+
410+
#### Example
411+
412+
```js
413+
{
414+
type: 'StateRetried',
415+
timestamp: 1234567890123,
416+
state: {
417+
name: 'ReadDataFile',
418+
type: 'Task',
419+
input: { filePath: '/home/user/data.csv' }
420+
},
421+
retry: {
422+
retrier: { ErrorEquals: ['ReadError'] },
423+
attempt: 2
424+
}
425+
}
426+
```
427+
428+
---
429+
430+
### `StateCaught` event
431+
432+
The `StateCaught` event is produced when a state fails and it's caught because it matched the error specified by a catcher in the `Catch` field.
433+
434+
#### Additional fields
435+
436+
- `state`: An object of type [`StateData`](#statedata) containing data associated with the state that was caught.
437+
- `catch`: An object of type [`CatchData`](#catchdata) containing data associated with the caught error.
438+
- `index?`: The index of the `Map` iteration in which this state was caught. This property is only set if this state was executed within a `Map` state.
439+
440+
#### Example
441+
442+
```js
443+
{
444+
type: 'StateCaught',
445+
timestamp: 1234567890123,
446+
state: {
447+
name: 'ReadDataFile',
448+
type: 'Task',
449+
input: { filePath: '/home/user/data.csv' }
450+
},
451+
catch: {
452+
catcher: {
453+
ErrorEquals: ['ReadError'],
454+
Next: 'RecoveryState'
455+
}
456+
}
457+
}
458+
```
459+
364460
## Helper data types
365461

366462
#### `StateData`
@@ -378,3 +474,38 @@ interface StateData {
378474
- `type`: Type of the state.
379475
- `input`: The input passed to the state.
380476
- `output`: The output produced by the state. Only set when event is of type `StateExited`.
477+
478+
---
479+
480+
#### `RetryData`
481+
482+
```ts
483+
interface RetryData {
484+
retrier: {
485+
ErrorEquals: string[];
486+
IntervalSeconds?: number;
487+
MaxAttempts?: number;
488+
BackoffRate?: number;
489+
};
490+
attempt: number;
491+
}
492+
```
493+
494+
- `retrier`: The retrier object that caused the state to be retried.
495+
- `attempt`: Number of current attempt (`attempt: 1` being the first attempt).
496+
497+
---
498+
499+
#### `CatchData`
500+
501+
```ts
502+
interface CatchData {
503+
catcher: {
504+
ErrorEquals: string[];
505+
Next: string;
506+
ResultPath?: string;
507+
};
508+
}
509+
```
510+
511+
- `catcher`: The catcher object that caused the state to be caught.

0 commit comments

Comments
 (0)