@google-cloud/nodejs-common 0.9.3-beta → 0.9.4-beta3

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.
@@ -15,7 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  # Cloud Functions Runtime Environment.
18
- CF_RUNTIME="${CF_RUNTIME:=nodejs10}"
18
+ CF_RUNTIME="${CF_RUNTIME:=nodejs14}"
19
19
 
20
20
  # Counter for steps.
21
21
  STEP=0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/nodejs-common",
3
- "version": "0.9.3-beta",
3
+ "version": "0.9.4-beta3",
4
4
  "description": "A NodeJs common library for solutions based on Cloud Functions",
5
5
  "author": "Google Inc.",
6
6
  "license": "Apache-2.0",
@@ -23,7 +23,7 @@ const stream = require('stream');
23
23
  const {google} = require('googleapis');
24
24
  const {Schema$Upload} = google.analytics;
25
25
  const AuthClient = require('./auth_client.js');
26
- const {wait, getLogger} = require('../components/utils.js');
26
+ const {wait, getLogger, BatchResult} = require('../components/utils.js');
27
27
 
28
28
  const API_SCOPES = Object.freeze([
29
29
  'https://www.googleapis.com/auth/analytics',
@@ -81,8 +81,7 @@ class Analytics {
81
81
  * @param {string|!stream.Readable} data A string or a stream to be uploaded.
82
82
  * @param {!DataImportConfig} config GA data import configuration.
83
83
  * @param {string=} batchId A tag for log.
84
- * @return {!Promise<boolean>} Promise returning whether data import
85
- * succeeded.
84
+ * @return {!BatchResult}
86
85
  */
87
86
  async uploadData(data, config, batchId = 'unnamed') {
88
87
  const uploadConfig = Object.assign(
@@ -100,29 +99,37 @@ class Analytics {
100
99
  this.logger.debug('Response: ', response);
101
100
  const job = /** @type {Schema$Upload} */ response.data;
102
101
  const uploadId = (/** @type {Schema$Upload} */job).id;
103
- console.log(`Task [${batchId}] creates GA Data import job: ${uploadId}`);
102
+ this.logger.info(
103
+ `Task [${batchId}] creates GA Data import job: ${uploadId}`);
104
104
  const jobConfig = Object.assign({uploadId}, config);
105
105
  const result = await Promise.race([
106
106
  this.checkJobStatus(jobConfig),
107
107
  wait(8 * 60 * 1000, job), // wait up to 8 minutes here
108
108
  ]);
109
+ /** @type {BatchResult} */ const batchResult = {};
109
110
  switch ((/** @type {Schema$Upload} */ result).status) {
110
111
  case 'FAILED':
111
112
  this.logger.error('GA Data Import failed', result);
112
- const errors = result.errors || [`Unknown reason. ID: ${uploadId}`];
113
- throw new Error(errors.join('\n'));
113
+ batchResult.result = false;
114
+ batchResult.errors = result.errors
115
+ || [`Unknown reason. ID: ${uploadId}`];
116
+ break;
114
117
  case 'COMPLETED':
115
118
  this.logger.info(`GA Data Import job[${uploadId}] completed.`);
116
119
  this.logger.debug('Response: ', result);
117
- return true;
120
+ batchResult.result = true;
121
+ break;
118
122
  case 'PENDING':
119
123
  this.logger.info('GA Data Import pending.', result);
120
124
  this.logger.info('Still will return true here.');
121
- return true;
125
+ batchResult.result = true;
126
+ break;
122
127
  default:
123
128
  this.logger.error('Unknown results of GA Data Import: ', result);
124
- throw new Error(`Unknown status. ID: ${uploadId}`);
129
+ batchResult.result = false;
130
+ batchResult.errors = [`Unknown status. ID: ${uploadId}`];
125
131
  }
132
+ return batchResult;
126
133
  }
127
134
 
128
135
  /**
package/src/apis/index.js CHANGED
@@ -110,13 +110,3 @@ exports.adsdatahub = require('./ads_data_hub.js');
110
110
  * }}
111
111
  */
112
112
  exports.measurementprotocolga4 = require('./measurement_protocol_ga4.js');
113
-
114
- /**
115
- * APIs integration class for YouTube.
116
- * @const {{
117
- * YouTube:!YouTube,
118
- * ListChannelsConfig: !ListChannelsConfig,
119
- * ListVideosConfig: !ListVideosConfig,
120
- * }}
121
- */
122
- exports.youtube = require('./youtube.js');
@@ -20,7 +20,11 @@
20
20
  'use strict';
21
21
 
22
22
  const {request} = require('gaxios');
23
- const {getLogger, SendSingleBatch} = require('../components/utils.js');
23
+ const {
24
+ getLogger,
25
+ SendSingleBatch,
26
+ BatchResult,
27
+ } = require('../components/utils.js');
24
28
  /** Base URL for Google Analytics service. */
25
29
  const BASE_URL = 'https://www.google-analytics.com';
26
30
 
@@ -62,9 +66,9 @@ class MeasurementProtocol {
62
66
  * @param {!Array<string>} lines Data for single request. It should be
63
67
  * guaranteed that it doesn't exceed quota limitation.
64
68
  * @param {string} batchId The tag for log.
65
- * @return {!Promise<boolean>}
69
+ * @return {!Promise<BatchResult>}
66
70
  */
67
- return (lines, batchId) => {
71
+ return async (lines, batchId) => {
68
72
  const payload =
69
73
  lines
70
74
  .map((line) => {
@@ -85,30 +89,47 @@ class MeasurementProtocol {
85
89
  body: payload,
86
90
  headers: {'User-Agent': 'Tentacles/MeasurementProtocol-v1'}
87
91
  };
88
- return request(requestOptions).then((response) => {
89
- if (response.status < 200 || response.status >= 300) {
90
- const errorMessages = [
91
- `Measurement Protocol [${batchId}] didn't succeed.`,
92
- `Get response code: ${response.status}`,
93
- `response: ${response.data}`,
94
- ];
95
- console.error(errorMessages.join('\n'));
96
- throw new Error(`Status code not 2XX`);
97
- }
98
- this.logger.debug(`Configuration:`, config);
99
- this.logger.debug(`Input Data: `, lines);
100
- this.logger.debug(`Batch[${batchId}] status: ${response.status}`);
101
- this.logger.debug(response.data);
102
- // There is not enough information from the non-debug mode.
103
- if (!this.debugMode) return true;
104
- return response.data.hitParsingResult.every((result) => result.valid);
105
- });
92
+ const response = await request(requestOptions);
93
+ /** @type {BatchResult} */ const batchResult = {
94
+ numberOfLines: lines.length,
95
+ };
96
+ if (response.status < 200 || response.status >= 300) {
97
+ const errorMessages = [
98
+ `Measurement Protocol [${batchId}] didn't succeed.`,
99
+ `Get response code: ${response.status}`,
100
+ `response: ${response.data}`,
101
+ ];
102
+ this.logger.error(errorMessages.join('\n'));
103
+ batchResult.errors = errorMessages;
104
+ batchResult.result = false;
105
+ return batchResult;
106
+ }
107
+ this.logger.debug(`Configuration:`, config);
108
+ this.logger.debug(`Input Data: `, lines);
109
+ this.logger.debug(`Batch[${batchId}] status: ${response.status}`);
110
+ this.logger.debug(response.data);
111
+ // There is not enough information from the non-debug mode.
112
+ if (!this.debugMode) {
113
+ batchResult.result = true;
114
+ } else {
115
+ const failedHits = [];
116
+ const failedReasons = new Set();
117
+ response.data.hitParsingResult.forEach((result, index) => {
118
+ if (!result.valid) {
119
+ failedHits.push(lines[index]);
120
+ result.parserMessage.forEach(({description}) => {
121
+ failedReasons.add(description);
122
+ });
123
+ }
124
+ });
125
+ batchResult.result = failedHits.length === 0;
126
+ batchResult.failedLines = failedHits;
127
+ batchResult.errors = Array.from(failedReasons);
128
+ }
129
+ return batchResult;
106
130
  };
107
131
  };
108
132
 
109
- static getInstance() {
110
- return new MeasurementProtocol(process.env['DEBUG'] === 'true');
111
- }
112
133
  }
113
134
 
114
135
  module.exports = {MeasurementProtocol};
@@ -20,7 +20,11 @@
20
20
  'use strict';
21
21
 
22
22
  const {request} = require('gaxios');
23
- const {getLogger, SendSingleBatch} = require('../components/utils.js');
23
+ const {
24
+ getLogger,
25
+ SendSingleBatch,
26
+ BatchResult,
27
+ } = require('../components/utils.js');
24
28
  /** Base URL for Google Analytics service. */
25
29
  const BASE_URL = 'https://www.google-analytics.com';
26
30
 
@@ -85,7 +89,7 @@ class MeasurementProtocolGA4 {
85
89
  * @param {!Array<string>} lines Data for single request. It should be
86
90
  * guaranteed that it doesn't exceed quota limitation.
87
91
  * @param {string} batchId The tag for log.
88
- * @return {!Promise<boolean>}
92
+ * @return {!BatchResult}
89
93
  */
90
94
  return async (lines, batchId) => {
91
95
  const line = lines[0]; // Each request contains one record only.
@@ -104,22 +108,36 @@ class MeasurementProtocolGA4 {
104
108
  headers: {'User-Agent': 'Tentacles/MeasurementProtocol-GA4'}
105
109
  };
106
110
  const response = await request(requestOptions);
111
+ /** @type {BatchResult} */ const batchResult = {
112
+ numberOfLines: lines.length,
113
+ };
107
114
  if (response.status < 200 || response.status >= 300) {
108
115
  const errorMessages = [
109
116
  `Measurement Protocol GA4 [${batchId}] didn't succeed.`,
110
117
  `Get response code: ${response.status}`,
111
118
  `response: ${response.data}`,
112
119
  ];
113
- console.error(errorMessages.join('\n'));
114
- throw new Error(`Status code not 2XX`);
120
+ this.logger.error(errorMessages.join('\n'));
121
+ batchResult.errors = errorMessages;
122
+ batchResult.result = false;
123
+ return batchResult;
115
124
  }
116
125
  this.logger.debug('Configuration:', config);
117
126
  this.logger.debug('Input Data: ', lines);
118
127
  this.logger.debug(`Batch[${batchId}] status: ${response.status}`);
119
- this.logger.debug('Response: ', response.data);
128
+ this.logger.debug(response.data);
120
129
  // There is not enough information from the non-debug mode.
121
- if (!this.debugMode) return true;
122
- return response.data.validationMessages.length === 0;
130
+ if (!this.debugMode) {
131
+ batchResult.result = true;
132
+ } else {
133
+ batchResult.result = response.data.validationMessages.length === 0;
134
+ if (!batchResult.result) {
135
+ batchResult.failedLines = lines;
136
+ batchResult.errors = response.data.validationMessages.map(
137
+ ({description}) => description);
138
+ }
139
+ }
140
+ return batchResult;
123
141
  };
124
142
  };
125
143
 
@@ -21,7 +21,7 @@
21
21
  const {google} = require('googleapis');
22
22
  const {Params$Resource$Spreadsheets$Get} = google.sheets;
23
23
  const AuthClient = require('./auth_client.js');
24
- const {getLogger} = require('../components/utils.js');
24
+ const {getLogger, BatchResult} = require('../components/utils.js');
25
25
 
26
26
  const API_SCOPES = Object.freeze([
27
27
  'https://www.googleapis.com/auth/spreadsheets',
@@ -92,16 +92,15 @@ class Spreadsheets {
92
92
  * @param {string} sheetName Name of the Sheet.
93
93
  * @return {!Promise<number>} Sheet Id.
94
94
  */
95
- getSheetId(sheetName) {
95
+ async getSheetId(sheetName) {
96
96
  const request = /** @type{Params$Resource$Spreadsheets$Get} */ {
97
97
  spreadsheetId: this.spreadsheetId,
98
98
  ranges: sheetName,
99
99
  };
100
- return this.instance.spreadsheets.get(request).then((response) => {
101
- const sheet = response.data.sheets[0];
102
- this.logger.debug(`Get sheet[${sheetName}]: `, sheet);
103
- return sheet.properties.sheetId;
104
- });
100
+ const response = await this.instance.spreadsheets.get(request);
101
+ const sheet = response.data.sheets[0];
102
+ this.logger.debug(`Get sheet[${sheetName}]: `, sheet);
103
+ return sheet.properties.sheetId;
105
104
  }
106
105
 
107
106
  /**
@@ -111,21 +110,20 @@ class Spreadsheets {
111
110
  * @param {string} sheetName Name of the Sheet.
112
111
  * @return {!Promise<boolean>} Whether the operation succeeded.
113
112
  */
114
- clearSheet(sheetName) {
113
+ async clearSheet(sheetName) {
115
114
  const request = {
116
115
  spreadsheetId: this.spreadsheetId,
117
116
  range: sheetName,
118
117
  };
119
- return this.instance.spreadsheets.values.clear(request)
120
- .then((response) => {
121
- const data = response.data;
122
- this.logger.debug(`Clear sheet[${sheetName}}]: `, data);
123
- return true;
124
- })
125
- .catch((error) => {
126
- console.error(error);
127
- return false;
128
- });
118
+ try {
119
+ const response = await this.instance.spreadsheets.values.clear(request);
120
+ const data = response.data;
121
+ this.logger.debug(`Clear sheet[${sheetName}}]: `, data);
122
+ return true;
123
+ } catch (error) {
124
+ this.logger.error(error);
125
+ return false;
126
+ }
129
127
  }
130
128
 
131
129
  /**
@@ -147,26 +145,13 @@ class Spreadsheets {
147
145
  * @private
148
146
  */
149
147
  getChangeDimensionRequest_(sheetId, dimension, current, target) {
150
- if (current === target) {
151
- return;
152
- }
148
+ if (current === target) return;
153
149
  if (current < target) { // Appends dimension.
154
- return {
155
- appendDimension: {
156
- sheetId: sheetId,
157
- dimension: dimension,
158
- length: target - current,
159
- }
160
- };
150
+ return {appendDimension: {sheetId, dimension, length: target - current}};
161
151
  } else { // Deletes dimension.
162
152
  return {
163
153
  deleteDimension: {
164
- range: {
165
- sheetId: sheetId,
166
- dimension: dimension,
167
- startIndex: target,
168
- endIndex: current,
169
- }
154
+ range: {sheetId, dimension, startIndex: target, endIndex: current},
170
155
  }
171
156
  };
172
157
  }
@@ -179,49 +164,44 @@ class Spreadsheets {
179
164
  * @param {number} targetColumns Loaded data columns number.
180
165
  * @return {!Promise<boolean>} Whether the operation succeeded.
181
166
  */
182
- reshape(sheetName, targetRows, targetColumns) {
167
+ async reshape(sheetName, targetRows, targetColumns) {
183
168
  const request = /** @type{Params$Resource$Spreadsheets$Get} */ {
184
169
  spreadsheetId: this.spreadsheetId,
185
170
  ranges: sheetName,
186
171
  };
187
- return this.instance.spreadsheets.get(request)
188
- .then((response) => {
189
- const sheet = response.data.sheets[0];
190
- const sheetId = sheet.properties.sheetId;
191
- const rowCount = sheet.properties.gridProperties.rowCount;
192
- const columnCount = sheet.properties.gridProperties.columnCount;
193
- this.logger.debug(`Get sheet[${sheetName}]: `, sheet);
194
- const requests = {
195
- spreadsheetId: this.spreadsheetId,
196
- resource: {requests: []},
197
- };
198
- if (rowCount !== targetRows) {
199
- requests.resource.requests.push(this.getChangeDimensionRequest_(
200
- sheetId, 'ROWS', rowCount, targetRows));
201
- }
202
- if (columnCount !== targetColumns) {
203
- requests.resource.requests.push(this.getChangeDimensionRequest_(
204
- sheetId, 'COLUMNS', columnCount, targetColumns));
205
- }
206
- this.logger.debug(
207
- `Reshape Sheet from [${rowCount}, ${columnCount}] to [${
208
- targetRows}, ${targetColumns}]`,
209
- JSON.stringify(requests.resource.requests));
210
- if (requests.resource.requests.length > 0) {
211
- return this.instance.spreadsheets.batchUpdate(requests).then(
212
- (response) => {
213
- const data = response.data;
214
- console.log(`Reshape Sheet [${sheetName}]: `, data);
215
- return true;
216
- });
217
- }
218
- this.logger.debug('No need to reshape.');
219
- return true;
220
- })
221
- .catch((error) => {
222
- console.error(error);
223
- return false;
224
- });
172
+ try {
173
+ const response = await this.instance.spreadsheets.get(request);
174
+ const sheet = response.data.sheets[0];
175
+ const sheetId = sheet.properties.sheetId;
176
+ const rowCount = sheet.properties.gridProperties.rowCount;
177
+ const columnCount = sheet.properties.gridProperties.columnCount;
178
+ this.logger.debug(`Get sheet[${sheetName}]: `, sheet);
179
+ const requests = {
180
+ spreadsheetId: this.spreadsheetId,
181
+ resource: {requests: []},
182
+ };
183
+ if (rowCount !== targetRows) {
184
+ requests.resource.requests.push(this.getChangeDimensionRequest_(
185
+ sheetId, 'ROWS', rowCount, targetRows));
186
+ }
187
+ if (columnCount !== targetColumns) {
188
+ requests.resource.requests.push(this.getChangeDimensionRequest_(
189
+ sheetId, 'COLUMNS', columnCount, targetColumns));
190
+ }
191
+ this.logger.debug(`Reshape Sheet from [${rowCount}, ${
192
+ columnCount}] to [${targetRows}, ${targetColumns}]`,
193
+ JSON.stringify(requests.resource.requests));
194
+ if (requests.resource.requests.length > 0) {
195
+ const {data} = await this.instance.spreadsheets.batchUpdate(requests);
196
+ this.logger.debug(`Reshape Sheet [${sheetName}]: `, data);
197
+ return true;
198
+ }
199
+ this.logger.debug('No need to reshape.');
200
+ return true;
201
+ } catch (error) {
202
+ console.error(error);
203
+ return false;
204
+ }
225
205
  }
226
206
 
227
207
  /**
@@ -230,25 +210,26 @@ class Spreadsheets {
230
210
  * @param {!ParseDataRequest} config A ParseDataRequest object template. The
231
211
  * data will be put in before it is send out through Sheets batchUpdate.
232
212
  * @param {string=} batchId The tag for log.
233
- * @return {!Promise<boolean>} Promise returning whether the operation
234
- * succeeded.
213
+ * @return {!BatchResult}
235
214
  */
236
- loadData(data, config, batchId = 'unnamed') {
237
- const pasteData = Object.assign({}, config, {data: data});
215
+ async loadData(data, config, batchId = 'unnamed') {
216
+ const pasteData = Object.assign({}, config, {data});
238
217
  const request = {
239
218
  spreadsheetId: this.spreadsheetId,
240
- resource: {requests: [{pasteData: pasteData}]},
219
+ resource: {requests: [{pasteData}]},
241
220
  };
242
- return this.instance.spreadsheets.batchUpdate(request)
243
- .then((response) => {
244
- const data = response.data;
245
- console.log(`Batch[${batchId}] uploaded: `, data);
246
- return true;
247
- })
248
- .catch((error) => {
249
- console.error(error.errors);
250
- return false;
251
- });
221
+ /** @type {BatchResult} */ const batchResult = {};
222
+ try {
223
+ const response = await this.instance.spreadsheets.batchUpdate(request);
224
+ const data = response.data;
225
+ this.logger.debug(`Batch[${batchId}] uploaded: `, data);
226
+ batchResult.result = true;
227
+ } catch (error) {
228
+ this.logger.error(error);
229
+ batchResult.result = false;
230
+ batchResult.errors = [error.toString()];
231
+ }
232
+ return batchResult;
252
233
  }
253
234
  }
254
235
 
@@ -23,12 +23,27 @@ const {inspect} = require('util');
23
23
  const {LoggingWinston} = require('@google-cloud/logging-winston');
24
24
  const {CloudPlatformApis} = require('../apis/cloud_platform_apis.js');
25
25
 
26
+ /**
27
+ * The result of a batch of data sent to target API. The batch here means the
28
+ * data that will be sent out in one single request.
29
+ * Some APIs upload whole file. In this case, there will be not 'numberOfLines'
30
+ * or 'failedLines'.
31
+ * @typedef {{
32
+ * result: boolean,
33
+ * numberOfLines: (number|undefined),
34
+ * failedLines: (Array<string>|undefined),
35
+ * errors: (Array<string>|undefined),
36
+ * output: (Array<string>|undefined),
37
+ * }}
38
+ */
39
+ let BatchResult;
40
+
26
41
  /**
27
42
  * Function which sends a batch of data. Takes two parameters:
28
43
  * {!Array<string>} Data for single request. It should be guaranteed that it
29
44
  * doesn't exceed quota limitation.
30
45
  * {string} The tag for log.
31
- * @typedef {function(!Array<string>,string): !Promise<boolean>}
46
+ * @typedef {function(!Array<string>,string): !BatchResult}
32
47
  */
33
48
  let SendSingleBatch;
34
49
 
@@ -162,6 +177,44 @@ const splitArray = (records, splitSize) => {
162
177
  return results;
163
178
  };
164
179
 
180
+ /**
181
+ * Merges an array of API results (BatchResult) in to a single one.
182
+ *
183
+ * @param {!Array<!BatchResult>} batchResults
184
+ * @param {string} batchPrefix Prefix tag for this batch, used for logging.
185
+ * @return {!BatchResult}
186
+ */
187
+ const mergeBatchResults = (batchResults, batchPrefix) => {
188
+ const logger = getLogger('SPEED_CTL');
189
+ /** @const {!BatchResult} */ const mergedResult = {
190
+ result: true,
191
+ numberOfLines: 0,
192
+ failedLines: [],
193
+ errors: [],
194
+ output: []
195
+ };
196
+ batchResults.forEach((batchResult, index) => {
197
+ const batchId = `${batchPrefix}-${index}`;
198
+ const {result, numberOfLines, failedLines, errors, output} = batchResult;
199
+ if (logger.isDebugEnabled()) {
200
+ logger.debug(
201
+ ` Task [${batchId}] has ${numberOfLines} lines: ${result
202
+ ? 'succeeded' : 'failed'}.`);
203
+ if (!result) {
204
+ logger.debug(` Errors: ${errors.join('\n')}`);
205
+ logger.debug(` Failed lines: ${failedLines.join('\n')}`);
206
+ }
207
+ }
208
+ mergedResult.result = mergedResult.result && result;
209
+ mergedResult.numberOfLines += numberOfLines;
210
+ mergedResult.failedLines = mergedResult.failedLines.concat(
211
+ failedLines || []);
212
+ mergedResult.errors = mergedResult.errors.concat(errors || []);
213
+ mergedResult.output = mergedResult.output.concat(output || []);
214
+ });
215
+ return mergedResult;
216
+ };
217
+
165
218
  /**
166
219
  * Sends a round of data in multiple batches (requests). Number of records in
167
220
  * every batch is defined by 'recordSize'. All these requests will be send out
@@ -175,45 +228,28 @@ const splitArray = (records, splitSize) => {
175
228
  * single request.
176
229
  * @param {number} qps Queries per second.
177
230
  * @param {string} roundId Round ID for log.
178
- * @return {!Promise<boolean>}
231
+ * @return {!Promise<!Array<!BatchResult>>}
179
232
  * @private
180
233
  */
181
- const sendSingleRound = (sendingFn, sliced, recordSize, qps, roundId) => {
234
+ const sendSingleRound = async (sendingFn, sliced, recordSize, qps, roundId) => {
182
235
  const logger = getLogger('SPEED_CTL');
183
236
  const batchArray = splitArray(sliced, recordSize);
184
237
  const securedDefer = Math.ceil(batchArray.length / qps * 1000);
185
- logger.debug(`Task round[${roundId}] has ${
186
- batchArray.length} requests/batches. Start:`);
187
- const deferPromise = wait(securedDefer).then(() => {
238
+ logger.debug(
239
+ `Task round[${roundId}] has ${batchArray.length} requests/batches:`);
240
+ const deferPromise = async () => {
241
+ await wait(securedDefer);
188
242
  logger.debug(`Task round[${roundId}] is secured for ${securedDefer} ms.`);
189
- return true;
190
- });
191
- const batchPromises = [deferPromise];
192
- batchArray.forEach((batch, index) => {
193
- const batchId = `${roundId}-${index}`;
194
- batchPromises.push(sendingFn(batch, batchId).then((batchResult) => {
195
- logger.debug(`Task batch[${batchId}] has ${batch.length} records: ${
196
- batchResult ? 'succeeded' : 'failed'}.`);
197
- return batchResult;
198
- }));
199
- });
200
- return Promise.all(batchPromises).then((results) => {
201
- const roundResult = !results.includes(false);
202
- logger.debug(
203
- `Task round[${roundId}] ${roundResult ? 'succeeded' : 'failed'}.`);
204
- if (!roundResult) {
205
- console.log(
206
- `Task round[${roundId}] has batch(es) failed batches:`, results);
207
- }
208
- return roundResult;
209
- });
243
+ };
244
+ const batchPromises = batchArray.map(
245
+ (batch, index) => sendingFn(batch, `${roundId}-${index}`));
246
+ const [batchResults] = await Promise.all([
247
+ Promise.all(batchPromises),
248
+ deferPromise(),
249
+ ]);
250
+ return batchResults;
210
251
  };
211
- //TODO refactor the returned value here to give more information of results.
212
- // Ideally return every error line's result.
213
- // pseudo code:
214
- // p.then((previousResult)=>{
215
- // return thisPromise().then((newValue) => value_merge_newValue_with_previousResult);
216
- // }
252
+
217
253
  /**
218
254
  * Some APIs will have the limitations for:
219
255
  * 1. The number of conversions for each request;
@@ -225,11 +261,18 @@ const sendSingleRound = (sendingFn, sliced, recordSize, qps, roundId) => {
225
261
  * @param {number=} numberOfThreads The number of requests will be fired
226
262
  * simultaneously.
227
263
  * @param {number=} qps Queries per second.
264
+ * @param {function(!Array<!BatchResult>, string):!BatchResult} mergeFn
265
+ * The function to merge the array of result of each request.
266
+ * @param {number} timeout The time (seconds) that this function can run. This
267
+ * is used to evaluate whether the given data could be processed with the
268
+ * given 'qps' and 'recordSize' in time.
228
269
  * @return {function(!SendSingleBatch,(string|!Array<string>),string=):
229
- * !Promise<boolean>} Speed and content managed sending function.
270
+ * !BatchResult} Speed and content managed sending function.
230
271
  */
231
- const apiSpeedControl = (recordSize = 1, numberOfThreads = 1, qps = 1) => {
272
+ const apiSpeedControl = (recordSize = 1, numberOfThreads = 1, qps = 1,
273
+ mergeFn = mergeBatchResults, timeout = 540) => {
232
274
  const roundSize = recordSize * numberOfThreads;
275
+ const maxRecords = qps * recordSize * timeout;
233
276
  /**
234
277
  * Returns a sending function with speed and content managed.
235
278
  * @param {!SendSingleBatch} sendingFn Function to send out a
@@ -239,27 +282,42 @@ const apiSpeedControl = (recordSize = 1, numberOfThreads = 1, qps = 1) => {
239
282
  * time, the element in Array<string> is expected to be in the format of
240
283
  * JSON object.
241
284
  * @param {string=} taskId Task ID for log.
242
- * @return {!Promise<boolean>}
285
+ * @return {!BatchResult}
243
286
  */
244
- return (sendingFn, data, taskId = 'unnamed') => {
287
+ return async (sendingFn, data, taskId = 'unnamed') => {
288
+ const logger = getLogger('SPEED_CTL');
245
289
  const records = Array.isArray(data) ?
246
290
  data :
247
291
  data.split('\n').filter((line) => line.trim() !== '');
292
+ if (maxRecords < data.length) {
293
+ const error = `Predicted timeout: ${records.length} records with config: `
294
+ + `${recordSize} records/request and ${qps} QPS in ${timeout} sec.`;
295
+ logger.error(error);
296
+ return {
297
+ result: false,
298
+ errors: [error],
299
+ };
300
+ }
248
301
  const roundArray = splitArray(records, roundSize);
249
- console.log(`Task[${taskId}] has ${records.length} records in ${
250
- roundArray.length} rounds.`);
251
- let roundPromise = Promise.resolve(true);
252
- roundArray.forEach((round, index) => {
302
+ logger.debug(
303
+ `Task [${taskId}] has ${records.length} records in ${roundArray.length} rounds.`);
304
+ const reduceFn = async (previous, roundData, index) => {
305
+ const results = await previous;
253
306
  const roundId = `${taskId}-${index}`;
254
- roundPromise = roundPromise.then((previousResult) => {
255
- return sendSingleRound(sendingFn, round, recordSize, qps, roundId)
256
- .then(roundResult => previousResult && roundResult);
257
- });
258
- });
259
- return roundPromise.then((taskResult) => {
260
- console.log(`Task[${taskId}] ${taskResult ? 'succeeded' : 'failed'}.`);
261
- return taskResult;
262
- });
307
+ /** @const {!Array<!BatchResult>} */ const roundResult =
308
+ await sendSingleRound(sendingFn, roundData, recordSize, qps, roundId);
309
+ /** @const {!BatchResult} */
310
+ const mergedRoundResult = mergeFn(roundResult, roundId);
311
+ return results.concat(mergedRoundResult);
312
+ };
313
+ /** @const {!Array<!BatchResult>} */
314
+ const taskResult = await roundArray.reduce(reduceFn, []);
315
+ /** @const {!BatchResult} */
316
+ const mergedTaskResult = mergeBatchResults(taskResult, taskId);
317
+ logger.debug(
318
+ `Task [${taskId}]: ${mergedTaskResult.result ? 'succeeded' : 'failed'}.`
319
+ );
320
+ return mergedTaskResult;
263
321
  };
264
322
  };
265
323
 
@@ -463,7 +521,9 @@ const changeNamingFromSnakeToLowerCamel = (name) => {
463
521
  module.exports = {
464
522
  getLogger,
465
523
  wait,
524
+ BatchResult,
466
525
  SendSingleBatch,
526
+ sendSingleRound,
467
527
  apiSpeedControl,
468
528
  splitArray,
469
529
  getProperValue,
@@ -1,145 +0,0 @@
1
- // Copyright 2021 Google Inc.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- /**
16
- * @fileoverview Youtube API Client Library.
17
- */
18
-
19
- 'use strict';
20
-
21
- const {google} = require('googleapis');
22
- const {Schema$Channel, Schema$Video} = google.youtube;
23
- const AuthClient = require('./auth_client.js');
24
- const {getLogger} = require('../components/utils.js');
25
-
26
- const API_SCOPES = Object.freeze([
27
- 'https://www.googleapis.com/auth/youtube.readonly'
28
- ]);
29
- const API_VERSION = 'v3';
30
-
31
- /**
32
- * Configuration for listing youtube channels.
33
- * @see https://developers.google.com/youtube/v3/docs/channels/list
34
- * @typedef {{
35
- * part: Array<string>,
36
- * categoryId: (string|undefined),
37
- * forUsername: (string|undefined),
38
- * id: (string|undefined),
39
- * managedByMe: (boolean|undefined),
40
- * mine: (boolean|undefined),
41
- * hl: (string|undefined),
42
- * onBehalfOfContentOwner: (string|undefined),
43
- * pageToken: (string|undefined),
44
- * }}
45
- */
46
- let ListChannelsConfig;
47
-
48
- /**
49
- * Configuration for listing youtube videos.
50
- * @see https://developers.google.com/youtube/v3/docs/videos/list
51
- * @typedef {{
52
- * part: Array<string>,
53
- * id: (string|undefined),
54
- * chart: (string|undefined),
55
- * hl: (string|undefined),
56
- * maxHeight: (unsigned integer|undefined),
57
- * maxResults: (unsigned integer|undefined),
58
- * maxWidth: (unsigned integer|undefined),
59
- * onBehalfOfContentOwner: (string|undefined),
60
- * pageToken: (string|undefined),
61
- * regionCode: (string|undefined),
62
- * }}
63
- */
64
- let ListVideosConfig;
65
-
66
- /**
67
- * Youtube API v3 stub.
68
- * See: https://developers.google.com/youtube/v3/docs
69
- * Channel list type definition, see:
70
- * https://developers.google.com/youtube/v3/docs/channels/list
71
- * Video list type definition, see:
72
- * https://developers.google.com/youtube/v3/docs/videos/list
73
- */
74
- class YouTube {
75
- constructor() {
76
- const authClient = new AuthClient(API_SCOPES);
77
- this.auth = authClient.getDefaultAuth();
78
- /** @const {!google.youtube} */
79
- this.instance = google.youtube({
80
- version: API_VERSION,
81
- });
82
- /**
83
- * Logger object from 'log4js' package where this type is not exported.
84
- */
85
- this.logger = getLogger('API.YT');
86
- }
87
-
88
- /**
89
- * Returns a collection of zero or more channel resources that match the
90
- * request criteria.
91
- * @see https://developers.google.com/youtube/v3/docs/channels/list
92
- * @param {!ListChannelsConfig} config List channels configuration.
93
- * @return {!Promise<Array<Schema$Channel>>}
94
- */
95
- async listChannels(config) {
96
- const channelListRequest = Object.assign({
97
- auth: this.auth,
98
- }, config);
99
- channelListRequest.part = channelListRequest.part.join(',')
100
- try {
101
- const response = await this.instance.channels.list(channelListRequest);
102
- this.logger.debug('Response: ', response);
103
- return response.data.items;
104
- } catch (error) {
105
- const errorMsg =
106
- `Fail to list channels: ${JSON.stringify(channelListRequest)}`;
107
- this.logger.error('YouTube list channels failed.', error.message);
108
- this.logger.debug('Errors in response:', error);
109
- throw new Error(errorMsg);
110
- }
111
- }
112
-
113
- /**
114
- * Returns a list of videos that match the API request parameters.
115
- * @see https://developers.google.com/youtube/v3/docs/videos/list
116
- * @param {!ListVideosConfig} config List videos configuration.
117
- * @return {!Promise<Array<Schema$Video>>}
118
- */
119
- async listVideos(config) {
120
- const videoListRequest = Object.assign({
121
- auth: this.auth,
122
- }, config);
123
- videoListRequest.part = videoListRequest.part.join(',')
124
- try {
125
- const response = await this.instance.videos.list(videoListRequest);
126
- this.logger.debug('Response: ', response);
127
- return response.data.items;
128
- } catch (error) {
129
- const errorMsg =
130
- `Fail to list videos: ${JSON.stringify(videoListRequest)}`;
131
- this.logger.error('YouTube list videos failed.', error.message);
132
- this.logger.debug('Errors in response:', error);
133
- throw new Error(errorMsg);
134
- }
135
- }
136
-
137
- }
138
-
139
- module.exports = {
140
- YouTube,
141
- ListChannelsConfig,
142
- ListVideosConfig,
143
- API_VERSION,
144
- API_SCOPES,
145
- };