@naturalcycles/datastore-lib 3.18.0 → 3.19.0

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.
@@ -35,8 +35,12 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
35
35
  const datastoreLib = require('@google-cloud/datastore');
36
36
  const DS = datastoreLib.Datastore;
37
37
  (_a = this.cfg).projectId || (_a.projectId = this.cfg.credentials?.project_id || process.env['GOOGLE_CLOUD_PROJECT']);
38
- (0, js_lib_1._assert)(this.cfg.projectId, '"projectId" is not set for DatastoreDB');
39
- this.cfg.logger.log(`DatastoreDB connected to ${(0, colors_1.boldWhite)(this.cfg.projectId)}`);
38
+ if (this.cfg.projectId) {
39
+ this.cfg.logger.log(`DatastoreDB connected to ${(0, colors_1.boldWhite)(this.cfg.projectId)}`);
40
+ }
41
+ else if (process.env['GOOGLE_APPLICATION_CREDENTIALS']) {
42
+ this.cfg.logger.log(`DatastoreDB connected via GOOGLE_APPLICATION_CREDENTIALS`);
43
+ }
40
44
  if (this.cfg.useLegacyGRPC) {
41
45
  this.cfg.grpc = require('grpc');
42
46
  }
@@ -56,16 +60,31 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
56
60
  return [];
57
61
  const keys = ids.map(id => this.key(table, id));
58
62
  let rows;
59
- try {
60
- rows = (await this.ds().get(keys))[0];
63
+ if (this.cfg.timeout) {
64
+ // First try
65
+ try {
66
+ const r = await (0, js_lib_1.pTimeout)(this.ds().get(keys), {
67
+ timeout: this.cfg.timeout,
68
+ name: `datastore.getByIds(${table})`,
69
+ });
70
+ rows = r[0];
71
+ }
72
+ catch {
73
+ this.cfg.logger.log('datastore recreated on error');
74
+ // This is to debug "GCP Datastore Timeout issue"
75
+ const datastoreLib = require('@google-cloud/datastore');
76
+ const DS = datastoreLib.Datastore;
77
+ this.cachedDatastore = new DS(this.cfg);
78
+ // Second try (will throw)
79
+ const r = await (0, js_lib_1.pTimeout)(this.ds().get(keys), {
80
+ timeout: this.cfg.timeout,
81
+ name: `datastore.getByIds(${table}) second try`,
82
+ });
83
+ rows = r[0];
84
+ }
61
85
  }
62
- catch (err) {
63
- this.cfg.logger.log('datastore recreated on error');
64
- // This is to debug "GCP Datastore Timeout issue"
65
- const datastoreLib = require('@google-cloud/datastore');
66
- const DS = datastoreLib.Datastore;
67
- this.cachedDatastore = new DS(this.cfg);
68
- throw err;
86
+ else {
87
+ rows = (await this.ds().get(keys))[0];
69
88
  }
70
89
  return (rows
71
90
  .map(r => this.mapId(r))
@@ -124,12 +143,12 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
124
143
  */
125
144
  async saveBatch(table, rows, opt = {}) {
126
145
  const entities = rows.map(obj => this.toDatastoreEntity(table, obj, opt.excludeFromIndexes));
127
- const save = (0, js_lib_1.pRetry)(async (batch) => {
146
+ const save = (0, js_lib_1.pRetryFn)(async (batch) => {
128
147
  await (opt.tx || this.ds()).save(batch);
129
148
  }, {
130
149
  // Here we retry the GOAWAY errors that are somewhat common for Datastore
131
150
  // Currently only retrying them here in .saveBatch(), cause probably they're only thrown when saving
132
- predicate: err => RETRY_ON.some(s => err?.message.includes(s)),
151
+ predicate: err => RETRY_ON.some(s => err?.message?.includes(s)),
133
152
  name: `DatastoreLib.saveBatch(${table})`,
134
153
  maxAttempts: 5,
135
154
  delay: 5000,
@@ -140,13 +159,19 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
140
159
  logger: this.cfg.logger,
141
160
  });
142
161
  try {
143
- await (0, js_lib_1.pMap)((0, js_lib_1._chunk)(entities, MAX_ITEMS), async (batch) => await save(batch));
162
+ const chunks = (0, js_lib_1._chunk)(entities, MAX_ITEMS);
163
+ if (chunks.length === 1) {
164
+ // Not using pMap in hope to preserve stack trace
165
+ await save(chunks[0]);
166
+ }
167
+ else {
168
+ await (0, js_lib_1.pMap)(chunks, async (batch) => await save(batch));
169
+ }
144
170
  }
145
171
  catch (err) {
146
172
  // console.log(`datastore.save ${kind}`, { obj, entity })
147
173
  this.cfg.logger.error(`error in DatastoreLib.saveBatch for ${table} (${rows.length} rows)`, err);
148
- // don't throw, because datastore SDK makes it in separate thread, so exception will be unhandled otherwise
149
- return await Promise.reject(err);
174
+ throw err;
150
175
  }
151
176
  }
152
177
  async deleteByQuery(q, opt) {
@@ -34,6 +34,13 @@ export interface DatastoreDBCfg extends DatastoreOptions {
34
34
  * Default to `console`
35
35
  */
36
36
  logger?: CommonLogger;
37
+ /**
38
+ * Experimental option, currently only applies to `getByIds`.
39
+ * Applies pTimeout to Datastore operation, re-creates Datastore on any error.
40
+ *
41
+ * @experimental
42
+ */
43
+ timeout?: number;
37
44
  }
38
45
  export interface DatastoreCredentials {
39
46
  type?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/datastore-lib",
3
- "version": "3.18.0",
3
+ "version": "3.19.0",
4
4
  "description": "Opinionated library to work with Google Datastore",
5
5
  "scripts": {
6
6
  "prepare": "husky install"
@@ -17,13 +17,14 @@ import {
17
17
  JsonSchemaObject,
18
18
  JsonSchemaString,
19
19
  pMap,
20
- pRetry,
21
20
  _assert,
22
21
  _chunk,
23
22
  _omit,
24
23
  JsonSchemaRootObject,
25
24
  CommonLogger,
26
25
  commonLoggerMinLevel,
26
+ pTimeout,
27
+ pRetryFn,
27
28
  } from '@naturalcycles/js-lib'
28
29
  import { ReadableTyped } from '@naturalcycles/nodejs-lib'
29
30
  import { boldWhite } from '@naturalcycles/nodejs-lib/dist/colors'
@@ -83,9 +84,11 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
83
84
  const DS = datastoreLib.Datastore as typeof Datastore
84
85
  this.cfg.projectId ||= this.cfg.credentials?.project_id || process.env['GOOGLE_CLOUD_PROJECT']
85
86
 
86
- _assert(this.cfg.projectId, '"projectId" is not set for DatastoreDB')
87
-
88
- this.cfg.logger.log(`DatastoreDB connected to ${boldWhite(this.cfg.projectId)}`)
87
+ if (this.cfg.projectId) {
88
+ this.cfg.logger.log(`DatastoreDB connected to ${boldWhite(this.cfg.projectId)}`)
89
+ } else if (process.env['GOOGLE_APPLICATION_CREDENTIALS']) {
90
+ this.cfg.logger.log(`DatastoreDB connected via GOOGLE_APPLICATION_CREDENTIALS`)
91
+ }
89
92
 
90
93
  if (this.cfg.useLegacyGRPC) {
91
94
  this.cfg.grpc = require('grpc')
@@ -115,17 +118,31 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
115
118
  const keys = ids.map(id => this.key(table, id))
116
119
  let rows: any[]
117
120
 
118
- try {
121
+ if (this.cfg.timeout) {
122
+ // First try
123
+ try {
124
+ const r = await pTimeout(this.ds().get(keys), {
125
+ timeout: this.cfg.timeout,
126
+ name: `datastore.getByIds(${table})`,
127
+ })
128
+ rows = r[0]
129
+ } catch {
130
+ this.cfg.logger.log('datastore recreated on error')
131
+
132
+ // This is to debug "GCP Datastore Timeout issue"
133
+ const datastoreLib = require('@google-cloud/datastore')
134
+ const DS = datastoreLib.Datastore as typeof Datastore
135
+ this.cachedDatastore = new DS(this.cfg)
136
+
137
+ // Second try (will throw)
138
+ const r = await pTimeout(this.ds().get(keys), {
139
+ timeout: this.cfg.timeout,
140
+ name: `datastore.getByIds(${table}) second try`,
141
+ })
142
+ rows = r[0]
143
+ }
144
+ } else {
119
145
  rows = (await this.ds().get(keys))[0]
120
- } catch (err) {
121
- this.cfg.logger.log('datastore recreated on error')
122
-
123
- // This is to debug "GCP Datastore Timeout issue"
124
- const datastoreLib = require('@google-cloud/datastore')
125
- const DS = datastoreLib.Datastore as typeof Datastore
126
- this.cachedDatastore = new DS(this.cfg)
127
-
128
- throw err
129
146
  }
130
147
 
131
148
  return (
@@ -226,14 +243,14 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
226
243
  this.toDatastoreEntity(table, obj, opt.excludeFromIndexes as string[]),
227
244
  )
228
245
 
229
- const save = pRetry(
246
+ const save = pRetryFn(
230
247
  async (batch: DatastorePayload<ROW>[]) => {
231
248
  await (opt.tx || this.ds()).save(batch)
232
249
  },
233
250
  {
234
251
  // Here we retry the GOAWAY errors that are somewhat common for Datastore
235
252
  // Currently only retrying them here in .saveBatch(), cause probably they're only thrown when saving
236
- predicate: err => RETRY_ON.some(s => (err as Error)?.message.includes(s)),
253
+ predicate: err => RETRY_ON.some(s => err?.message?.includes(s)),
237
254
  name: `DatastoreLib.saveBatch(${table})`,
238
255
  maxAttempts: 5,
239
256
  delay: 5000,
@@ -246,15 +263,21 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
246
263
  )
247
264
 
248
265
  try {
249
- await pMap(_chunk(entities, MAX_ITEMS), async batch => await save(batch))
266
+ const chunks = _chunk(entities, MAX_ITEMS)
267
+ if (chunks.length === 1) {
268
+ // Not using pMap in hope to preserve stack trace
269
+ await save(chunks[0]!)
270
+ } else {
271
+ await pMap(chunks, async batch => await save(batch))
272
+ }
250
273
  } catch (err) {
251
274
  // console.log(`datastore.save ${kind}`, { obj, entity })
252
275
  this.cfg.logger.error(
253
276
  `error in DatastoreLib.saveBatch for ${table} (${rows.length} rows)`,
254
277
  err,
255
278
  )
256
- // don't throw, because datastore SDK makes it in separate thread, so exception will be unhandled otherwise
257
- return await Promise.reject(err)
279
+
280
+ throw err
258
281
  }
259
282
  }
260
283
 
@@ -41,6 +41,14 @@ export interface DatastoreDBCfg extends DatastoreOptions {
41
41
  * Default to `console`
42
42
  */
43
43
  logger?: CommonLogger
44
+
45
+ /**
46
+ * Experimental option, currently only applies to `getByIds`.
47
+ * Applies pTimeout to Datastore operation, re-creates Datastore on any error.
48
+ *
49
+ * @experimental
50
+ */
51
+ timeout?: number
44
52
  }
45
53
 
46
54
  export interface DatastoreCredentials {