@cumulus/message 18.2.2 → 18.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CumulusMessage.d.ts +6 -0
- package/CumulusMessage.d.ts.map +1 -0
- package/CumulusMessage.js +10 -0
- package/CumulusMessage.js.map +1 -0
- package/DeadLetterMessage.d.ts +18 -8
- package/DeadLetterMessage.d.ts.map +1 -1
- package/DeadLetterMessage.js +130 -14
- package/DeadLetterMessage.js.map +1 -1
- package/Providers.d.ts +2 -0
- package/Providers.d.ts.map +1 -1
- package/Providers.js +12 -1
- package/Providers.js.map +1 -1
- package/README.md +50 -13
- package/StepFunctions.d.ts +13 -15
- package/StepFunctions.d.ts.map +1 -1
- package/StepFunctions.js +8 -8
- package/StepFunctions.js.map +1 -1
- package/package.json +12 -10
- package/src/CumulusMessage.ts +9 -0
- package/src/DeadLetterMessage.ts +166 -23
- package/src/Providers.ts +18 -1
- package/src/StepFunctions.ts +17 -17
- package/tsconfig.tsbuildinfo +1 -1
package/src/DeadLetterMessage.ts
CHANGED
|
@@ -1,54 +1,197 @@
|
|
|
1
|
-
|
|
1
|
+
//@ts-check
|
|
2
|
+
import { SQSRecord } from 'aws-lambda';
|
|
3
|
+
import moment from 'moment';
|
|
2
4
|
|
|
3
|
-
import {
|
|
5
|
+
import { uuid } from 'uuidv4';
|
|
6
|
+
import { isEventBridgeEvent, StepFunctionEventBridgeEvent } from '@cumulus/aws-client/Lambda';
|
|
7
|
+
import { parseSQSMessageBody, isSQSRecordLike } from '@cumulus/aws-client/SQS';
|
|
4
8
|
import { CumulusMessage } from '@cumulus/types/message';
|
|
9
|
+
import { DLQRecord, DLARecord } from '@cumulus/types/api/dead_letters';
|
|
5
10
|
import Logger from '@cumulus/logger';
|
|
6
|
-
|
|
11
|
+
import { MessageGranule } from '@cumulus/types';
|
|
12
|
+
import { isMessageWithProvider, getMessageProviderId } from './Providers';
|
|
13
|
+
import { isCumulusMessageLike } from './CumulusMessage';
|
|
7
14
|
import { getCumulusMessageFromExecutionEvent } from './StepFunctions';
|
|
15
|
+
import { constructCollectionId } from './Collections';
|
|
8
16
|
|
|
9
17
|
const log = new Logger({ sender: '@cumulus/DeadLetterMessage' });
|
|
10
18
|
|
|
11
|
-
type
|
|
12
|
-
type UnwrapDeadLetterCumulusMessageReturnType = (
|
|
19
|
+
type UnwrapDeadLetterCumulusMessageInputType = (
|
|
13
20
|
StepFunctionEventBridgeEvent
|
|
14
|
-
|
|
|
15
|
-
| SQSRecord
|
|
21
|
+
| DLQRecord | SQSRecord
|
|
16
22
|
| CumulusMessage
|
|
17
23
|
);
|
|
18
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Bare check for SQS message Shape
|
|
27
|
+
*/
|
|
28
|
+
export const isDLQRecordLike = (message: Object): message is DLQRecord => (
|
|
29
|
+
isSQSRecordLike(message)
|
|
30
|
+
&& 'error' in message
|
|
31
|
+
);
|
|
32
|
+
|
|
19
33
|
/**
|
|
20
34
|
* Unwrap dead letter Cumulus message, which may be wrapped in a
|
|
21
35
|
* States cloudwatch event, which is wrapped in an SQS message.
|
|
22
|
-
*
|
|
23
|
-
* @param {Object} messageBody - received SQS message
|
|
24
|
-
* @returns {Object} the cumulus message or nearest available object
|
|
25
36
|
*/
|
|
26
37
|
export const unwrapDeadLetterCumulusMessage = async (
|
|
27
|
-
messageBody:
|
|
28
|
-
): Promise<
|
|
38
|
+
messageBody: UnwrapDeadLetterCumulusMessageInputType
|
|
39
|
+
): Promise<UnwrapDeadLetterCumulusMessageInputType> => {
|
|
29
40
|
try {
|
|
30
|
-
if (
|
|
31
|
-
return messageBody;
|
|
32
|
-
}
|
|
33
|
-
if ('Body' in messageBody || 'body' in messageBody) {
|
|
41
|
+
if (isSQSRecordLike(messageBody)) {
|
|
34
42
|
// AWS.SQS.Message/SQS.Record case
|
|
35
43
|
const unwrappedMessageBody = parseSQSMessageBody(
|
|
36
44
|
messageBody
|
|
37
|
-
)
|
|
45
|
+
);
|
|
38
46
|
return await unwrapDeadLetterCumulusMessage(unwrappedMessageBody);
|
|
39
47
|
}
|
|
40
|
-
if (
|
|
41
|
-
|
|
48
|
+
if (isEventBridgeEvent(messageBody)) {
|
|
49
|
+
return await getCumulusMessageFromExecutionEvent(
|
|
50
|
+
messageBody
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (isCumulusMessageLike(messageBody)) {
|
|
42
54
|
return messageBody;
|
|
43
55
|
}
|
|
44
|
-
|
|
45
|
-
const unwrappedMessageBody = await getCumulusMessageFromExecutionEvent(messageBody);
|
|
46
|
-
return await unwrapDeadLetterCumulusMessage(unwrappedMessageBody);
|
|
56
|
+
throw new TypeError('DeadLetter CumulusMessage in unrecognized format');
|
|
47
57
|
} catch (error) {
|
|
48
58
|
log.error(
|
|
49
59
|
'Falling back to storing wrapped message after encountering unwrap error',
|
|
50
|
-
error
|
|
60
|
+
error,
|
|
61
|
+
JSON.stringify(messageBody)
|
|
51
62
|
);
|
|
52
63
|
return messageBody;
|
|
53
64
|
}
|
|
54
65
|
};
|
|
66
|
+
|
|
67
|
+
interface PayloadWithGranules {
|
|
68
|
+
granules: Array<MessageGranule>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const payloadHasGranules = (payload: any): payload is PayloadWithGranules => (
|
|
72
|
+
payload instanceof Object
|
|
73
|
+
&& 'granules' in payload
|
|
74
|
+
&& Array.isArray(payload.granules)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const extractCollectionId = (message: CumulusMessage): string | null => {
|
|
78
|
+
const collectionName = message?.meta?.collection?.name || null;
|
|
79
|
+
const collectionVersion = message?.meta?.collection?.version || null;
|
|
80
|
+
if (collectionName && collectionVersion) {
|
|
81
|
+
return constructCollectionId(collectionName, collectionVersion);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const extractGranules = (message: CumulusMessage): Array<string | null> | null => {
|
|
87
|
+
if (payloadHasGranules(message.payload)) {
|
|
88
|
+
return message.payload.granules.map((granule) => granule?.granuleId || null);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type DLQMetadata = Partial<DLQRecord> & { body: undefined };
|
|
94
|
+
/**
|
|
95
|
+
* peel out metadata from an SQS(/DLQ)record
|
|
96
|
+
* @param message DLQ or SQS message
|
|
97
|
+
* @returns the given message without its body
|
|
98
|
+
*/
|
|
99
|
+
const extractSQSMetadata = (message: DLQRecord | SQSRecord): DLQMetadata => {
|
|
100
|
+
const metadata = { ...message } as any;
|
|
101
|
+
delete metadata.body;
|
|
102
|
+
delete metadata.Body;
|
|
103
|
+
return metadata;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Reformat object with key attributes at top level.
|
|
108
|
+
*
|
|
109
|
+
*/
|
|
110
|
+
export const hoistCumulusMessageDetails = async (dlqRecord: SQSRecord): Promise<DLARecord> => {
|
|
111
|
+
let executionArn = null;
|
|
112
|
+
let stateMachineArn = null;
|
|
113
|
+
let status = null;
|
|
114
|
+
let time = null;
|
|
115
|
+
let collectionId = null;
|
|
116
|
+
let granules = null;
|
|
117
|
+
let providerId = null;
|
|
118
|
+
|
|
119
|
+
let messageBody;
|
|
120
|
+
messageBody = dlqRecord;
|
|
121
|
+
let metadata = extractSQSMetadata(messageBody);
|
|
122
|
+
/* de-nest sqs records of unknown depth */
|
|
123
|
+
while (isSQSRecordLike(messageBody)) {
|
|
124
|
+
/* prefer outermost recorded metadata */
|
|
125
|
+
metadata = { ...extractSQSMetadata(messageBody), ...metadata };
|
|
126
|
+
messageBody = parseSQSMessageBody(messageBody);
|
|
127
|
+
}
|
|
128
|
+
const error = 'error' in metadata ? metadata.error : null;
|
|
129
|
+
if (isEventBridgeEvent(messageBody)) {
|
|
130
|
+
executionArn = messageBody?.detail?.executionArn || null;
|
|
131
|
+
stateMachineArn = messageBody?.detail?.stateMachineArn || null;
|
|
132
|
+
status = messageBody?.detail?.status || null;
|
|
133
|
+
time = messageBody?.time || null;
|
|
134
|
+
let cumulusMessage;
|
|
135
|
+
try {
|
|
136
|
+
cumulusMessage = await getCumulusMessageFromExecutionEvent(messageBody);
|
|
137
|
+
} catch (error_) {
|
|
138
|
+
cumulusMessage = undefined;
|
|
139
|
+
log.error(
|
|
140
|
+
'could not parse details from DLQ message body',
|
|
141
|
+
error_,
|
|
142
|
+
messageBody
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (cumulusMessage) {
|
|
146
|
+
collectionId = extractCollectionId(cumulusMessage);
|
|
147
|
+
granules = extractGranules(cumulusMessage);
|
|
148
|
+
if (isMessageWithProvider(cumulusMessage)) {
|
|
149
|
+
providerId = getMessageProviderId(cumulusMessage) || null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
log.error(
|
|
154
|
+
'could not parse details from DLQ message body',
|
|
155
|
+
messageBody,
|
|
156
|
+
'expected EventBridgeEvent'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
...metadata,
|
|
161
|
+
body: JSON.stringify(messageBody),
|
|
162
|
+
collectionId,
|
|
163
|
+
providerId,
|
|
164
|
+
granules,
|
|
165
|
+
executionArn,
|
|
166
|
+
stateMachineArn,
|
|
167
|
+
status,
|
|
168
|
+
time,
|
|
169
|
+
error,
|
|
170
|
+
} as DLARecord; // cast to DLARecord: ts is confused by explicit 'undefined' fields in metadata
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const getDLARootKey = (stackName: string) => (
|
|
174
|
+
`${stackName}/dead-letter-archive/sqs/`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
export const extractDateString = (message: DLARecord): string => (
|
|
178
|
+
message.time && moment.utc(message.time).isValid() ? moment.utc(message.time).format('YYYY-MM-DD') : moment.utc().format('YYYY-MM-DD')
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
export const extractFileName = (message: DLARecord): string => {
|
|
182
|
+
// get token after the last / or :
|
|
183
|
+
const executionName = message.executionArn ? message.executionArn.split(/[/:]/).pop() : 'unknown';
|
|
184
|
+
return `${executionName}-${uuid()}`;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const getDLAKey = (stackName: string, message: DLARecord): string => {
|
|
188
|
+
const dateString = extractDateString(message);
|
|
189
|
+
const fileName = extractFileName(message);
|
|
190
|
+
return `${getDLARootKey(stackName)}${dateString}/${fileName}`;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const getDLAFailureKey = (stackName: string, message: DLARecord): string => {
|
|
194
|
+
const dateString = extractDateString(message);
|
|
195
|
+
const fileName = extractFileName(message);
|
|
196
|
+
return `${stackName}/dead-letter-archive/failed-sqs/${dateString}/${fileName}`;
|
|
197
|
+
};
|
package/src/Providers.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Message } from '@cumulus/types';
|
|
2
|
-
|
|
2
|
+
import { isCumulusMessageLike } from './CumulusMessage';
|
|
3
3
|
type MessageProvider = {
|
|
4
4
|
id: string
|
|
5
5
|
protocol: string
|
|
@@ -7,12 +7,29 @@ type MessageProvider = {
|
|
|
7
7
|
port?: number
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export const isMessageProvider = (
|
|
11
|
+
obj: any
|
|
12
|
+
): obj is MessageProvider => (
|
|
13
|
+
obj instanceof Object
|
|
14
|
+
// in testing instanceof String can return false for string literals
|
|
15
|
+
&& 'id' in obj && (typeof (obj.id) === 'string' || obj.id instanceof String)
|
|
16
|
+
&& 'protocol' in obj && (typeof (obj.protocol) === 'string' || obj.protocol instanceof String)
|
|
17
|
+
&& 'host' in obj && (typeof (obj.host) === 'string' || obj.host instanceof String)
|
|
18
|
+
&& ('port' in obj ? (typeof (obj.port) === 'number' || obj.port instanceof Number) : true)
|
|
19
|
+
);
|
|
20
|
+
|
|
10
21
|
type MessageWithProvider = Message.CumulusMessage & {
|
|
11
22
|
meta: {
|
|
12
23
|
provider?: MessageProvider
|
|
13
24
|
}
|
|
14
25
|
};
|
|
15
26
|
|
|
27
|
+
export const isMessageWithProvider = (
|
|
28
|
+
obj: any
|
|
29
|
+
): obj is MessageWithProvider => (
|
|
30
|
+
isCumulusMessageLike(obj)
|
|
31
|
+
&& isMessageProvider(obj?.meta?.provider)
|
|
32
|
+
);
|
|
16
33
|
/**
|
|
17
34
|
* Get the provider from a workflow message, if any.
|
|
18
35
|
*
|
package/src/StepFunctions.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Utility functions for working with AWS Step Function events/messages
|
|
5
|
-
* @module StepFunctions
|
|
6
5
|
*
|
|
6
|
+
* @module StepFunctions
|
|
7
7
|
* @example
|
|
8
|
-
*
|
|
8
|
+
* const StepFunctions = require('@cumulus/message/StepFunctions');
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { EventBridgeEvent } from 'aws-lambda';
|
|
@@ -13,7 +13,7 @@ import { JSONPath } from 'jsonpath-plus';
|
|
|
13
13
|
import get from 'lodash/get';
|
|
14
14
|
import set from 'lodash/set';
|
|
15
15
|
|
|
16
|
-
import { getExecutionHistory } from '@cumulus/aws-client/StepFunctions';
|
|
16
|
+
import { getExecutionHistory, HistoryEvent } from '@cumulus/aws-client/StepFunctions';
|
|
17
17
|
import { getStepExitedEvent, getTaskExitedEventOutput } from '@cumulus/common/execution-history';
|
|
18
18
|
import { Message } from '@cumulus/types';
|
|
19
19
|
import * as s3Utils from '@cumulus/aws-client/S3';
|
|
@@ -49,9 +49,9 @@ const executionStatusToWorkflowStatus = (
|
|
|
49
49
|
* of S3 remote message
|
|
50
50
|
*
|
|
51
51
|
* @param {Message.CumulusRemoteMessage} event - Source event
|
|
52
|
-
* @
|
|
52
|
+
* @param event.replace - Cumulus message replace config
|
|
53
|
+
* @returns {Promise<object>} Updated event with target path replaced by remote message
|
|
53
54
|
* @throws {Error} if target path cannot be found on source event
|
|
54
|
-
*
|
|
55
55
|
* @async
|
|
56
56
|
* @alias module:StepFunctions
|
|
57
57
|
*/
|
|
@@ -92,8 +92,7 @@ export const pullStepFunctionEvent = async (
|
|
|
92
92
|
*
|
|
93
93
|
* @param {CMAMessage} stepMessage - Message for the step
|
|
94
94
|
* @param {string} stepName - Name of the step
|
|
95
|
-
* @returns {Promise<
|
|
96
|
-
*
|
|
95
|
+
* @returns {Promise<object>} Parsed and updated event with target path replaced by remote message
|
|
97
96
|
* @async
|
|
98
97
|
* @alias module:StepFunctions
|
|
99
98
|
*/
|
|
@@ -123,17 +122,18 @@ export const parseStepMessage = async (
|
|
|
123
122
|
* Searches the Execution step History for the TaskStateEntered pertaining to
|
|
124
123
|
* the failed task Id. HistoryEvent ids are numbered sequentially, starting at
|
|
125
124
|
* one.
|
|
126
|
-
*
|
|
125
|
+
*
|
|
127
126
|
* @param {HistoryEvent[]} events - Step Function events array
|
|
128
|
-
* @param {
|
|
127
|
+
* @param {failedStepEvent} failedStepEvent - Step Function's failed event.
|
|
128
|
+
* @param failedStepEvent.id - number (long), Step Functions failed event id.
|
|
129
129
|
* @returns {string} name of the current stepfunction task or 'UnknownFailedStepName'.
|
|
130
130
|
*/
|
|
131
131
|
export const getFailedStepName = (
|
|
132
|
-
events:
|
|
133
|
-
failedStepEvent:
|
|
132
|
+
events: HistoryEvent[],
|
|
133
|
+
failedStepEvent: HistoryEvent
|
|
134
134
|
) => {
|
|
135
135
|
try {
|
|
136
|
-
const previousEvents = events.slice(0, failedStepEvent.id - 1);
|
|
136
|
+
const previousEvents = events.slice(0, failedStepEvent.id as number - 1);
|
|
137
137
|
const startEvents = previousEvents.filter(
|
|
138
138
|
(e) => e.type === 'TaskStateEntered'
|
|
139
139
|
);
|
|
@@ -150,15 +150,15 @@ export const getFailedStepName = (
|
|
|
150
150
|
/**
|
|
151
151
|
* Finds all failed execution events and returns the last one in the list.
|
|
152
152
|
*
|
|
153
|
-
* @param {
|
|
154
|
-
* @returns {
|
|
153
|
+
* @param {HistoryEvent[]} events - array of AWS Stepfunction execution HistoryEvents
|
|
154
|
+
* @returns {HistoryEvent[] | undefined} - the last lambda or activity that failed in the
|
|
155
155
|
* event array, or an empty array.
|
|
156
156
|
*/
|
|
157
157
|
export const lastFailedEventStep = (
|
|
158
|
-
events:
|
|
159
|
-
):
|
|
158
|
+
events: HistoryEvent[]
|
|
159
|
+
): HistoryEvent | undefined => {
|
|
160
160
|
const failures = events.filter((event) =>
|
|
161
|
-
['LambdaFunctionFailed', 'ActivityFailed'].includes(event.type));
|
|
161
|
+
['LambdaFunctionFailed', 'ActivityFailed'].includes(event.type as string));
|
|
162
162
|
return failures.pop();
|
|
163
163
|
};
|
|
164
164
|
|