@cumulus/message 10.1.2 → 11.1.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/src/Granules.ts CHANGED
@@ -8,8 +8,23 @@
8
8
  * const Granules = require('@cumulus/message/Granules');
9
9
  */
10
10
 
11
+ import isInteger from 'lodash/isInteger';
12
+ import isNil from 'lodash/isNil';
13
+ import mapValues from 'lodash/mapValues';
14
+ import omitBy from 'lodash/omitBy';
15
+
16
+ import { CumulusMessageError } from '@cumulus/errors';
11
17
  import { Message } from '@cumulus/types';
12
- import { ApiGranule, GranuleStatus } from '@cumulus/types/api/granules';
18
+ import { ExecutionProcessingTimes } from '@cumulus/types/api/executions';
19
+ import {
20
+ ApiGranule,
21
+ GranuleStatus,
22
+ GranuleTemporalInfo,
23
+ MessageGranule,
24
+ } from '@cumulus/types/api/granules';
25
+ import { ApiFile } from '@cumulus/types/api/files';
26
+
27
+ import { CmrUtilsClass } from './types';
13
28
 
14
29
  interface MetaWithGranuleQueryFields extends Message.Meta {
15
30
  granule?: {
@@ -53,14 +68,14 @@ export const messageHasGranules = (
53
68
  * Determine the status of a granule.
54
69
  *
55
70
  * @param {string} workflowStatus - The workflow status
56
- * @param {ApiGranule} granule - A granule record
71
+ * @param {MessageGranule} granule - A granule record conforming to the 'api' schema
57
72
  * @returns {string} The granule status
58
73
  *
59
74
  * @alias module:Granules
60
75
  */
61
76
  export const getGranuleStatus = (
62
77
  workflowStatus: Message.WorkflowStatus,
63
- granule: ApiGranule
78
+ granule: MessageGranule
64
79
  ): Message.WorkflowStatus | GranuleStatus => workflowStatus || granule.status;
65
80
 
66
81
  /**
@@ -74,3 +89,203 @@ export const getGranuleStatus = (
74
89
  export const getGranuleQueryFields = (
75
90
  message: MessageWithGranules
76
91
  ) => message.meta?.granule?.queryFields;
92
+
93
+ /**
94
+ * Calculate granule product volume, which is the sum of the file
95
+ * sizes in bytes
96
+ *
97
+ * @param {Array<Object>} granuleFiles - array of granule file objects that conform to the
98
+ * Cumulus 'api' schema
99
+ * @returns {string} - sum of granule file sizes in bytes as a string
100
+ */
101
+ export const getGranuleProductVolume = (granuleFiles: ApiFile[] = []): string => {
102
+ if (granuleFiles.length === 0) return '0';
103
+ return String(granuleFiles
104
+ .map((f) => f.size ?? 0)
105
+ .filter(isInteger)
106
+ .reduce((x, y) => x + BigInt(y), BigInt(0)));
107
+ };
108
+
109
+ export const getGranuleTimeToPreprocess = ({
110
+ sync_granule_duration = 0,
111
+ } = {}) => sync_granule_duration / 1000;
112
+
113
+ export const getGranuleTimeToArchive = ({
114
+ post_to_cmr_duration = 0,
115
+ } = {}) => post_to_cmr_duration / 1000;
116
+
117
+ /**
118
+ * Convert date string to standard ISO format.
119
+ *
120
+ * @param {string} date - Date string, possibly in multiple formats
121
+ * @returns {string} Standardized ISO date string
122
+ */
123
+ const convertDateToISOString = (date: string) => new Date(date).toISOString();
124
+
125
+ function isProcessingTimeInfo(
126
+ info: ExecutionProcessingTimes | {} = {}
127
+ ): info is ExecutionProcessingTimes {
128
+ return (info as ExecutionProcessingTimes)?.processingStartDateTime !== undefined
129
+ && (info as ExecutionProcessingTimes)?.processingEndDateTime !== undefined;
130
+ }
131
+
132
+ /**
133
+ * Convert granule processing timestamps to a standardized ISO string
134
+ * format for compatibility across database systems.
135
+ *
136
+ * @param {ExecutionProcessingTimes} [processingTimeInfo]
137
+ * Granule processing time info, if any
138
+ * @returns {Promise<ExecutionProcessingTimes | undefined>}
139
+ */
140
+ export const getGranuleProcessingTimeInfo = (
141
+ processingTimeInfo?: ExecutionProcessingTimes
142
+ ): ExecutionProcessingTimes | {} => {
143
+ const updatedProcessingTimeInfo = isProcessingTimeInfo(processingTimeInfo)
144
+ ? { ...processingTimeInfo }
145
+ : {};
146
+ return mapValues(
147
+ updatedProcessingTimeInfo,
148
+ convertDateToISOString
149
+ );
150
+ };
151
+
152
+ function isGranuleTemporalInfo(
153
+ info: GranuleTemporalInfo | {} = {}
154
+ ): info is GranuleTemporalInfo {
155
+ return (info as GranuleTemporalInfo)?.beginningDateTime !== undefined
156
+ && (info as GranuleTemporalInfo)?.endingDateTime !== undefined
157
+ && (info as GranuleTemporalInfo)?.productionDateTime !== undefined
158
+ && (info as GranuleTemporalInfo)?.lastUpdateDateTime !== undefined;
159
+ }
160
+
161
+ /**
162
+ * Get granule temporal information from argument or directly from CMR.
163
+ *
164
+ * Converts temporal information timestamps to a standardized ISO string
165
+ * format for compatibility across database systems.
166
+ *
167
+ * @param {Object} params
168
+ * @param {MessageGranule} params.granule - Granule from workflow message
169
+ * @param {Object} [params.cmrTemporalInfo] - CMR temporal info, if any
170
+ * @param {CmrUtilsClass} params.cmrUtils - CMR utilities object
171
+ * @returns {Promise<GranuleTemporalInfo | undefined>}
172
+ */
173
+ export const getGranuleCmrTemporalInfo = async ({
174
+ granule,
175
+ cmrTemporalInfo,
176
+ cmrUtils,
177
+ }: {
178
+ granule: MessageGranule,
179
+ cmrTemporalInfo?: GranuleTemporalInfo,
180
+ cmrUtils: CmrUtilsClass
181
+ }): Promise<GranuleTemporalInfo | {}> => {
182
+ // Get CMR temporalInfo (beginningDateTime, endingDateTime,
183
+ // productionDateTime, lastUpdateDateTime)
184
+ const temporalInfo = isGranuleTemporalInfo(cmrTemporalInfo)
185
+ ? { ...cmrTemporalInfo }
186
+ : await cmrUtils.getGranuleTemporalInfo(granule);
187
+ return mapValues(
188
+ temporalInfo,
189
+ convertDateToISOString
190
+ );
191
+ };
192
+
193
+ /**
194
+ * Generate an API granule record
195
+ *
196
+ * @param {MessageWithGranules} message - A workflow message
197
+ * @returns {Promise<ApiGranule>} The granule API record
198
+ *
199
+ * @alias module:Granules
200
+ */
201
+ export const generateGranuleApiRecord = async ({
202
+ granule,
203
+ executionUrl,
204
+ collectionId,
205
+ provider,
206
+ workflowStartTime,
207
+ error,
208
+ pdrName,
209
+ status,
210
+ queryFields,
211
+ updatedAt,
212
+ files,
213
+ processingTimeInfo,
214
+ cmrUtils,
215
+ timestamp,
216
+ duration,
217
+ productVolume,
218
+ timeToPreprocess,
219
+ timeToArchive,
220
+ cmrTemporalInfo,
221
+ }: {
222
+ granule: MessageGranule,
223
+ executionUrl?: string,
224
+ collectionId: string,
225
+ provider?: string,
226
+ workflowStartTime: number,
227
+ error?: Object,
228
+ pdrName?: string,
229
+ status: GranuleStatus,
230
+ queryFields?: Object,
231
+ updatedAt: number,
232
+ processingTimeInfo?: ExecutionProcessingTimes,
233
+ files?: ApiFile[],
234
+ timestamp: number,
235
+ cmrUtils: CmrUtilsClass
236
+ cmrTemporalInfo?: GranuleTemporalInfo,
237
+ duration: number,
238
+ productVolume: string,
239
+ timeToPreprocess: number,
240
+ timeToArchive: number,
241
+ }): Promise<ApiGranule> => {
242
+ if (!granule.granuleId) throw new CumulusMessageError(`Could not create granule record, invalid granuleId: ${granule.granuleId}`);
243
+
244
+ if (!collectionId) {
245
+ throw new CumulusMessageError('collectionId required to generate a granule record');
246
+ }
247
+
248
+ const {
249
+ granuleId,
250
+ cmrLink,
251
+ published = false,
252
+ createdAt,
253
+ } = granule;
254
+
255
+ const now = Date.now();
256
+ const recordUpdatedAt = updatedAt ?? now;
257
+ const recordTimestamp = timestamp ?? now;
258
+
259
+ // Get CMR temporalInfo
260
+ const temporalInfo = await getGranuleCmrTemporalInfo({
261
+ granule,
262
+ cmrTemporalInfo,
263
+ cmrUtils,
264
+ });
265
+ const updatedProcessingTimeInfo = getGranuleProcessingTimeInfo(processingTimeInfo);
266
+
267
+ const record = {
268
+ granuleId,
269
+ pdrName,
270
+ collectionId,
271
+ status,
272
+ provider,
273
+ execution: executionUrl,
274
+ cmrLink,
275
+ files,
276
+ error,
277
+ published,
278
+ createdAt: createdAt || workflowStartTime,
279
+ timestamp: recordTimestamp,
280
+ updatedAt: recordUpdatedAt,
281
+ duration,
282
+ productVolume,
283
+ timeToPreprocess,
284
+ timeToArchive,
285
+ ...updatedProcessingTimeInfo,
286
+ ...temporalInfo,
287
+ queryFields,
288
+ };
289
+
290
+ return <ApiGranule>omitBy(record, isNil);
291
+ };
package/src/PDRs.ts CHANGED
@@ -1,4 +1,25 @@
1
+ import {
2
+ CumulusMessageError,
3
+ } from '@cumulus/errors';
4
+ import Logger from '@cumulus/logger';
1
5
  import { Message } from '@cumulus/types';
6
+ import { ApiPdr } from '@cumulus/types/api/pdrs';
7
+
8
+ import { getCollectionIdFromMessage } from './Collections';
9
+ import {
10
+ getMessageExecutionArn,
11
+ getExecutionUrlFromArn,
12
+ } from './Executions';
13
+ import {
14
+ getMessageProviderId,
15
+ } from './Providers';
16
+ import {
17
+ getMetaStatus,
18
+ getMessageWorkflowStartTime,
19
+ getWorkflowDuration,
20
+ } from './workflows';
21
+
22
+ const logger = new Logger({ sender: '@cumulus/message/PDRs' });
2
23
 
3
24
  interface PDR {
4
25
  name: string
@@ -9,12 +30,6 @@ interface PDR {
9
30
  interface MessageWithOptionalPayloadPdr extends Message.CumulusMessage {
10
31
  payload: {
11
32
  pdr?: PDR
12
- }
13
- }
14
-
15
- interface MessageWithOptionalPdrStats extends Message.CumulusMessage {
16
- payload: {
17
- pdr: PDR
18
33
  failed?: unknown[]
19
34
  running?: unknown[]
20
35
  completed?: unknown[]
@@ -91,37 +106,37 @@ export const getMessagePdrName = (
91
106
  /**
92
107
  * Get the number of running executions for a PDR, if any.
93
108
  *
94
- * @param {MessageWithOptionalPdrStats} message - A workflow message
109
+ * @param {MessageWithOptionalPayloadPdr} message - A workflow message
95
110
  * @returns {number} Number of running executions
96
111
  *
97
112
  * @alias module:PDRs
98
113
  */
99
114
  export const getMessagePdrRunningExecutions = (
100
- message: MessageWithOptionalPdrStats
115
+ message: MessageWithOptionalPayloadPdr
101
116
  ): number => (message.payload.running ?? []).length;
102
117
 
103
118
  /**
104
119
  * Get the number of completed executions for a PDR, if any.
105
120
  *
106
- * @param {MessageWithOptionalPdrStats} message - A workflow message
121
+ * @param {MessageWithOptionalPayloadPdr} message - A workflow message
107
122
  * @returns {number} Number of completed executions
108
123
  *
109
124
  * @alias module:PDRs
110
125
  */
111
126
  export const getMessagePdrCompletedExecutions = (
112
- message: MessageWithOptionalPdrStats
127
+ message: MessageWithOptionalPayloadPdr
113
128
  ): number => (message.payload.completed ?? []).length;
114
129
 
115
130
  /**
116
131
  * Get the number of failed executions for a PDR, if any.
117
132
  *
118
- * @param {MessageWithOptionalPdrStats} message - A workflow message
133
+ * @param {MessageWithOptionalPayloadPdr} message - A workflow message
119
134
  * @returns {number} Number of failed executions
120
135
  *
121
136
  * @alias module:PDRs
122
137
  */
123
138
  export const getMessagePdrFailedExecutions = (
124
- message: MessageWithOptionalPdrStats
139
+ message: MessageWithOptionalPayloadPdr
125
140
  ): number => (message.payload.failed ?? []).length;
126
141
 
127
142
  /**
@@ -134,7 +149,7 @@ export const getMessagePdrFailedExecutions = (
134
149
  * @alias module:PDRs
135
150
  */
136
151
  export const getMessagePdrStats = (
137
- message: MessageWithOptionalPdrStats
152
+ message: MessageWithOptionalPayloadPdr
138
153
  ): PdrStats => {
139
154
  const processing = getMessagePdrRunningExecutions(message);
140
155
  const completed = getMessagePdrCompletedExecutions(message);
@@ -167,3 +182,74 @@ export const getPdrPercentCompletion = (
167
182
  }
168
183
  return progress;
169
184
  };
185
+
186
+ /**
187
+ * Generate a PDR record for the API from the message.
188
+ *
189
+ * @param {MessageWithOptionalPayloadPdr} message - A workflow message object
190
+ * @param {string} [updatedAt] - Optional updated timestamp to apply to record
191
+ * @returns {ExecutionRecord} An PDR API record
192
+ *
193
+ * @alias module:Executions
194
+ */
195
+ export const generatePdrApiRecordFromMessage = (
196
+ message: MessageWithOptionalPayloadPdr,
197
+ updatedAt = Date.now()
198
+ ): ApiPdr | undefined => {
199
+ const pdr = getMessagePdr(message);
200
+
201
+ // We got a message with no PDR (OK)
202
+ if (!pdr) {
203
+ logger.info('No PDRs to process on the message');
204
+ return undefined;
205
+ }
206
+
207
+ // We got a message with a PDR but no name to identify it (Not OK)
208
+ if (!pdr.name) {
209
+ throw new CumulusMessageError(`Could not find name on PDR object ${JSON.stringify(pdr)}`);
210
+ }
211
+
212
+ const collectionId = getCollectionIdFromMessage(message);
213
+ if (!collectionId) {
214
+ throw new CumulusMessageError('meta.collection required to generate a PDR record');
215
+ }
216
+
217
+ const providerId = getMessageProviderId(message);
218
+ if (!providerId) {
219
+ throw new CumulusMessageError('meta.provider required to generate a PDR record');
220
+ }
221
+
222
+ const status = getMetaStatus(message);
223
+ if (!status) {
224
+ throw new CumulusMessageError('meta.status required to generate a PDR record');
225
+ }
226
+
227
+ const arn = getMessageExecutionArn(message);
228
+ if (!arn) {
229
+ throw new CumulusMessageError('cumulus_meta.state_machine and cumulus_meta.execution_name required to generate a PDR record');
230
+ }
231
+ const execution = getExecutionUrlFromArn(arn);
232
+
233
+ const stats = getMessagePdrStats(message);
234
+ const progress = getPdrPercentCompletion(stats);
235
+ const now = Date.now();
236
+ const workflowStartTime = getMessageWorkflowStartTime(message);
237
+
238
+ const record = {
239
+ pdrName: pdr.name,
240
+ collectionId,
241
+ status,
242
+ provider: providerId,
243
+ progress,
244
+ execution,
245
+ PANSent: getMessagePdrPANSent(message),
246
+ PANmessage: getMessagePdrPANMessage(message),
247
+ stats,
248
+ createdAt: getMessageWorkflowStartTime(message),
249
+ timestamp: now,
250
+ updatedAt,
251
+ duration: getWorkflowDuration(workflowStartTime, now),
252
+ };
253
+
254
+ return record;
255
+ };
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Message } from '@cumulus/types';
2
+ import { GranuleTemporalInfo, MessageGranule } from '@cumulus/types/api/granules';
2
3
 
3
4
  export interface WorkflowMessageTemplateCumulusMeta {
4
5
  queueExecutionLimits: Message.QueueExecutionLimits
@@ -15,3 +16,7 @@ export interface Workflow {
15
16
  arn: string
16
17
  name: string
17
18
  }
19
+
20
+ export interface CmrUtilsClass {
21
+ getGranuleTemporalInfo(granule: MessageGranule): Promise<GranuleTemporalInfo | {}>
22
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,17 @@
1
+ import isNil from 'lodash/isNil';
2
+ import isObject from 'lodash/isObject';
3
+
4
+ /**
5
+ * Ensures that the exception is returned as an object
6
+ *
7
+ * @param {Object|undefined} exception - the exception
8
+ * @returns {string} an stringified exception
9
+ */
10
+ export const parseException = (exception: Object | undefined) => {
11
+ if (isNil(exception)) return {};
12
+ if (isObject(exception)) return exception;
13
+ return {
14
+ Error: 'Unknown Error',
15
+ Cause: exception,
16
+ };
17
+ };