@terascope/elasticsearch-api 2.24.2 → 3.0.2

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/index.js CHANGED
@@ -19,14 +19,17 @@ const {
19
19
  random,
20
20
  cloneDeep,
21
21
  DataEntity,
22
- isDeepEqual
22
+ isDeepEqual,
23
+ getTypeOf,
24
+ isProd
23
25
  } = require('@terascope/utils');
26
+ const { inspect } = require('util');
24
27
 
25
28
  const DOCUMENT_EXISTS = 409;
26
29
 
27
30
  // Module to manage persistence in Elasticsearch.
28
31
  // All functions in this module return promises that must be resolved to get the final result.
29
- module.exports = function elasticsearchApi(client = {}, logger, _opConfig) {
32
+ module.exports = function elasticsearchApi(client, logger, _opConfig) {
30
33
  const config = _opConfig || {};
31
34
  if (!client) {
32
35
  throw new Error('Elasticsearch API requires client');
@@ -98,7 +101,7 @@ module.exports = function elasticsearchApi(client = {}, logger, _opConfig) {
98
101
 
99
102
  function _runRequest() {
100
103
  clientBase[endpoint](query)
101
- .then((result) => resolve(result))
104
+ .then(resolve)
102
105
  .catch(errHandler);
103
106
  }
104
107
 
@@ -211,7 +214,7 @@ module.exports = function elasticsearchApi(client = {}, logger, _opConfig) {
211
214
  }
212
215
 
213
216
  function version() {
214
- const wildCardRegex = RegExp(/\*/g);
217
+ const wildCardRegex = /\*/g;
215
218
  const isWildCardRegexSearch = config.index.match(wildCardRegex);
216
219
  // We cannot reliable search index queries with wildcards
217
220
  // for existence or max_result_window, it could be
@@ -259,12 +262,26 @@ module.exports = function elasticsearchApi(client = {}, logger, _opConfig) {
259
262
  );
260
263
  }
261
264
 
262
- function _filterResponse(data, results) {
265
+ /**
266
+ * When the bulk request has errors this will find the actions
267
+ * records to retry.
268
+ *
269
+ * @returns {{
270
+ * retry: Record<string, any>[],
271
+ * successful: number,
272
+ * error: boolean,
273
+ * reason?: string
274
+ * }}
275
+ */
276
+ function _filterRetryRecords(actionRecords, result) {
263
277
  const retry = [];
264
- const { items } = results;
278
+ const { items } = result;
279
+
265
280
  let nonRetriableError = false;
266
281
  let reason = '';
267
- for (let i = 0; i < items.length; i += 1) {
282
+ let successful = 0;
283
+
284
+ for (let i = 0; i < items.length; i++) {
268
285
  // key could either be create or delete etc, just want the actual data at the value spot
269
286
  const item = Object.values(items[i])[0];
270
287
  if (item.error) {
@@ -275,11 +292,14 @@ module.exports = function elasticsearchApi(client = {}, logger, _opConfig) {
275
292
  }
276
293
 
277
294
  if (item.error.type === 'es_rejected_execution_exception') {
278
- if (i === 0) {
279
- retry.push(data[0], data[1]);
280
- } else {
281
- retry.push(data[i * 2], data[i * 2 + 1]);
295
+ if (actionRecords[i] == null) {
296
+ // this error should not happen in production,
297
+ // only in tests where the bulk function is mocked
298
+ throw new Error(`Invalid item index (${i}), not found in bulk send records (length: ${actionRecords.length})`);
282
299
  }
300
+ // the index in the item list will match the index in the
301
+ // input records
302
+ retry.push(actionRecords[i]);
283
303
  } else if (
284
304
  item.error.type !== 'document_already_exists_exception'
285
305
  && item.error.type !== 'document_missing_exception'
@@ -288,55 +308,95 @@ module.exports = function elasticsearchApi(client = {}, logger, _opConfig) {
288
308
  reason = `${item.error.type}--${item.error.reason}`;
289
309
  break;
290
310
  }
311
+ } else if (item.status == null || item.status < 400) {
312
+ successful++;
291
313
  }
292
314
  }
293
315
 
294
316
  if (nonRetriableError) {
295
- return { data: [], error: true, reason };
317
+ return {
318
+ retry: [], successful, error: true, reason
319
+ };
296
320
  }
297
321
 
298
- return { data: retry, error: false };
322
+ return { retry, successful, error: false };
299
323
  }
300
324
 
301
- function bulkSend(data) {
302
- return new Promise((resolve, reject) => {
303
- const retry = _retryFn(_sendData, data, reject);
304
-
305
- function _sendData(formattedData) {
306
- return _clientRequest('bulk', { body: formattedData })
307
- .then((results) => {
308
- if (results.errors) {
309
- const response = _filterResponse(formattedData, results);
310
-
311
- if (response.error) {
312
- reject(new TSError(response.reason, {
313
- retryable: false
314
- }));
315
- } else if (response.data.length === 0) {
316
- // may get doc already created error, if so just return
317
- resolve(results);
318
- } else {
319
- warning();
320
- retry(response.data);
321
- }
322
- } else {
323
- resolve(results);
324
- }
325
- })
326
- .catch((err) => {
327
- const error = new TSError(err, {
328
- reason: 'bulk sender error',
329
- context: {
330
- connection,
331
- },
332
- });
325
+ function getFirstKey(obj) {
326
+ return Object.keys(obj)[0];
327
+ }
333
328
 
334
- reject(error);
335
- });
329
+ /**
330
+ * @param data {Array<{ action: data }>}
331
+ * @returns {Promise<number>}
332
+ */
333
+ async function _bulkSend(actionRecords, previousCount = 0, previousRetryDelay = 0) {
334
+ const body = actionRecords.flatMap((record, index) => {
335
+ if (record.action == null) {
336
+ let dbg = '';
337
+ if (!isProd) {
338
+ dbg = `, dbg: ${inspect({ record, index })}`;
339
+ }
340
+ throw new Error(`Bulk send record is missing the action property${dbg}`);
336
341
  }
337
342
 
338
- _sendData(_adjustTypeForEs7(data));
343
+ if (getESVersion() >= 7) {
344
+ const actionKey = getFirstKey(record.action);
345
+ const { _type, ...withoutTypeAction } = record.action[actionKey];
346
+ // if data is specified return both
347
+ return record.data ? [{
348
+ ...record.action,
349
+ [actionKey]: withoutTypeAction
350
+ }, record.data] : [{
351
+ ...record.action,
352
+ [actionKey]: withoutTypeAction
353
+ }];
354
+ }
355
+
356
+ // if data is specified return both
357
+ return record.data ? [record.action, record.data] : [record.action];
339
358
  });
359
+
360
+ const result = await _clientRequest('bulk', { body });
361
+
362
+ if (!result.errors) {
363
+ return result.items.reduce((c, item) => {
364
+ const [value] = Object.values(item);
365
+ // ignore non-successful status codes
366
+ if (value.status != null && value.status >= 400) return c;
367
+ return c + 1;
368
+ }, 0);
369
+ }
370
+
371
+ const {
372
+ retry, successful, error, reason
373
+ } = _filterRetryRecords(actionRecords, result);
374
+
375
+ if (error) {
376
+ throw new Error(`bulk send error: ${reason}`);
377
+ }
378
+
379
+ if (retry.length === 0) {
380
+ return previousCount + successful;
381
+ }
382
+
383
+ warning();
384
+
385
+ const nextRetryDelay = await _awaitRetry(previousRetryDelay);
386
+ return _bulkSend(retry, previousCount + successful, nextRetryDelay);
387
+ }
388
+
389
+ /**
390
+ * The new and improved bulk send with proper retry support
391
+ *
392
+ * @returns {Promise<number>} the number of affected rows
393
+ */
394
+ function bulkSend(data) {
395
+ if (!Array.isArray(data)) {
396
+ throw new Error(`Expected bulkSend to receive an array, got ${data} (${getTypeOf(data)})`);
397
+ }
398
+
399
+ return Promise.resolve(_bulkSend(data));
340
400
  }
341
401
 
342
402
  function _warn(warnLogger, msg) {
@@ -622,6 +682,8 @@ module.exports = function elasticsearchApi(client = {}, logger, _opConfig) {
622
682
  }
623
683
 
624
684
  function _removeTypeFromBulkRequest(query) {
685
+ if (getESVersion() < 7) return query;
686
+
625
687
  return query.map((queryItem) => {
626
688
  if (isSimpleObject(queryItem)) {
627
689
  // get the metadata and ignore the record
@@ -696,17 +758,29 @@ module.exports = function elasticsearchApi(client = {}, logger, _opConfig) {
696
758
  return (_data) => {
697
759
  const args = _data || data;
698
760
 
761
+ _awaitRetry(delay)
762
+ .then((newDelay) => {
763
+ delay = newDelay;
764
+ fn(args);
765
+ })
766
+ .catch(reject);
767
+ };
768
+ }
769
+
770
+ /**
771
+ * @returns {Promise<number>} the delayed time
772
+ */
773
+ async function _awaitRetry(previousDelay = 0) {
774
+ return new Promise((resolve, reject) => {
699
775
  waitForClient((elapsed) => {
700
- delay = getBackoffDelay(delay, 2, retryLimit, retryStart);
776
+ const delay = getBackoffDelay(previousDelay, 2, retryLimit, retryStart);
701
777
 
702
778
  let timeoutMs = delay - elapsed;
703
779
  if (timeoutMs < 1) timeoutMs = 1;
704
780
 
705
- setTimeout(() => {
706
- fn(args);
707
- }, timeoutMs);
781
+ setTimeout(resolve, timeoutMs, delay);
708
782
  }, reject);
709
- };
783
+ });
710
784
  }
711
785
 
712
786
  function _errorHandler(fn, data, reject, fnName = '->unknown()') {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@terascope/elasticsearch-api",
3
3
  "displayName": "Elasticsearch API",
4
- "version": "2.24.2",
4
+ "version": "3.0.2",
5
5
  "description": "Elasticsearch client api used across multiple services, handles retries and exponential backoff",
6
6
  "homepage": "https://github.com/terascope/teraslice/tree/master/packages/elasticsearch-api#readme",
7
7
  "bugs": {
@@ -18,11 +18,11 @@
18
18
  "test:watch": "ts-scripts test --watch . --"
19
19
  },
20
20
  "dependencies": {
21
- "@terascope/utils": "^0.43.2",
21
+ "@terascope/utils": "^0.44.1",
22
22
  "bluebird": "^3.7.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/elasticsearch": "^5.0.37"
25
+ "@types/elasticsearch": "^5.0.40"
26
26
  },
27
27
  "engines": {
28
28
  "node": "^12.22.0 || >=14.17.0",
package/test/api-spec.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const Promise = require('bluebird');
4
- const { debugLogger, cloneDeep, DataEntity } = require('@terascope/utils');
4
+ const {
5
+ debugLogger, cloneDeep, DataEntity, isEmpty
6
+ } = require('@terascope/utils');
5
7
  const esApi = require('..');
6
8
 
7
9
  describe('elasticsearch-api', () => {
@@ -189,15 +191,63 @@ describe('elasticsearch-api', () => {
189
191
 
190
192
  function createBulkResponse(results) {
191
193
  const response = { took: 22, errors: false, items: results };
192
- if (bulkError) {
194
+ if (!isEmpty(bulkError)) {
193
195
  response.errors = true;
194
- response.items = results.body.map((obj, index) => {
195
- Object.entries(obj).forEach(([key, value]) => {
196
- obj[key] = Object.assign(value, {
197
- error: { type: bulkError[index] || 'someType', reason: 'someReason' }
198
- });
199
- });
200
- return obj;
196
+ let i = -1;
197
+ response.items = results.body.flatMap((obj) => {
198
+ if (!obj.index && !obj.update && !obj.create && !obj.delete) {
199
+ // ignore the non-metadata objects
200
+ return [];
201
+ }
202
+ i++;
203
+ const [key, value] = Object.entries(obj)[0];
204
+ return [{
205
+ [key]: {
206
+ _index: value._index,
207
+ _type: value._type,
208
+ _id: String(i),
209
+ _version: 1,
210
+ result: `${key}d`,
211
+ error: { type: bulkError[i] || 'someType', reason: 'someReason' },
212
+ _shards: {
213
+ total: 2,
214
+ successful: 1,
215
+ failed: 0
216
+ },
217
+ status: 400,
218
+ _seq_no: 2,
219
+ _primary_term: 3
220
+ }
221
+ }];
222
+ });
223
+ } else {
224
+ response.errors = false;
225
+ let i = -1;
226
+ response.items = results.body.flatMap((obj) => {
227
+ if (!obj.index && !obj.update && !obj.create && !obj.delete) {
228
+ // ignore the non-metadata objects
229
+ return [];
230
+ }
231
+
232
+ i++;
233
+ const [key, value] = Object.entries(obj)[0];
234
+ return [{
235
+ [key]: {
236
+ _index: value._index,
237
+ _type: value._type,
238
+ _id: String(i),
239
+ _version: 1,
240
+ result: `${key}d`,
241
+ _shards: {
242
+ total: 2,
243
+ successful: 1,
244
+ failed: 0
245
+ },
246
+ status: 200,
247
+ _seq_no: 2,
248
+ _primary_term: 3
249
+ }
250
+ }];
201
251
  });
202
252
  }
203
253
  return response;
@@ -323,45 +373,25 @@ describe('elasticsearch-api', () => {
323
373
  expect(() => {
324
374
  api = esApi(client, logger);
325
375
  }).not.toThrow();
326
- expect(api).toBeDefined();
327
376
  expect(typeof api).toEqual('object');
328
- expect(api.search).toBeDefined();
329
377
  expect(typeof api.search).toEqual('function');
330
- expect(api.count).toBeDefined();
331
378
  expect(typeof api.count).toEqual('function');
332
- expect(api.get).toBeDefined();
333
379
  expect(typeof api.get).toEqual('function');
334
- expect(api.index).toBeDefined();
335
380
  expect(typeof api.index).toEqual('function');
336
- expect(api.indexWithId).toBeDefined();
337
381
  expect(typeof api.indexWithId).toEqual('function');
338
- expect(api.create).toBeDefined();
339
382
  expect(typeof api.create).toEqual('function');
340
- expect(api.update).toBeDefined();
341
383
  expect(typeof api.update).toEqual('function');
342
- expect(api.remove).toBeDefined();
343
384
  expect(typeof api.remove).toEqual('function');
344
- expect(api.version).toBeDefined();
345
385
  expect(typeof api.version).toEqual('function');
346
- expect(api.putTemplate).toBeDefined();
347
386
  expect(typeof api.putTemplate).toEqual('function');
348
- expect(api.bulkSend).toBeDefined();
349
387
  expect(typeof api.bulkSend).toEqual('function');
350
- expect(api.nodeInfo).toBeDefined();
351
388
  expect(typeof api.nodeInfo).toEqual('function');
352
- expect(api.nodeStats).toBeDefined();
353
389
  expect(typeof api.nodeStats).toEqual('function');
354
- expect(api.buildQuery).toBeDefined();
355
390
  expect(typeof api.buildQuery).toEqual('function');
356
- expect(api.indexExists).toBeDefined();
357
391
  expect(typeof api.indexExists).toEqual('function');
358
- expect(api.indexCreate).toBeDefined();
359
392
  expect(typeof api.indexCreate).toEqual('function');
360
- expect(api.indexRefresh).toBeDefined();
361
393
  expect(typeof api.indexRefresh).toEqual('function');
362
- expect(api.indexRecovery).toBeDefined();
363
394
  expect(typeof api.indexRecovery).toEqual('function');
364
- expect(api.indexSetup).toBeDefined();
365
395
  expect(typeof api.indexSetup).toEqual('function');
366
396
  });
367
397
 
@@ -717,30 +747,48 @@ describe('elasticsearch-api', () => {
717
747
 
718
748
  it('can call bulkSend', async () => {
719
749
  const api = esApi(client, logger);
720
- const myBulkData = [
721
- { index: { _index: 'some_index', _type: 'events', _id: 1 } },
722
- { title: 'foo' },
723
- { delete: { _index: 'some_index', _type: 'events', _id: 5 } }
724
- ];
725
750
 
726
- const results = await api.bulkSend(myBulkData);
727
- return expect(results).toBeTruthy();
751
+ const result = await api.bulkSend([
752
+ {
753
+ action: {
754
+ index: { _index: 'some_index', _type: 'events', _id: 1 }
755
+ },
756
+ data: { title: 'foo' }
757
+ },
758
+ {
759
+ action: {
760
+ delete: { _index: 'some_index', _type: 'events', _id: 5 }
761
+ }
762
+ }
763
+ ]);
764
+ expect(bulkData).toEqual({
765
+ body: [
766
+ { index: { _index: 'some_index', _type: 'events', _id: 1 } },
767
+ { title: 'foo' },
768
+ { delete: { _index: 'some_index', _type: 'events', _id: 5 } }
769
+ ]
770
+ });
771
+ return expect(result).toBe(2);
728
772
  });
729
773
 
730
- it('can remove type from bulk send', async () => {
774
+ it('can remove type from bulkSend', async () => {
731
775
  const es7client = cloneDeep(client);
732
776
 
733
777
  es7client.transport._config = { apiVersion: '7.0' };
734
778
 
735
779
  const api = esApi(es7client, logger);
736
780
 
737
- const myBulkData = [
738
- { index: { _index: 'some_index', _type: 'events', _id: 1 } },
739
- { title: 'foo' },
740
- { delete: { _index: 'some_index', _type: 'events', _id: 5 } }
741
- ];
742
-
743
- await api.bulkSend(myBulkData);
781
+ await api.bulkSend([{
782
+ action: {
783
+ index: { _index: 'some_index', _type: 'events', _id: 1 }
784
+ },
785
+ data: { title: 'foo' }
786
+ },
787
+ {
788
+ action: {
789
+ delete: { _index: 'some_index', _type: 'events', _id: 5 }
790
+ }
791
+ }]);
744
792
  expect(bulkData).toEqual({
745
793
  body: [
746
794
  { index: { _index: 'some_index', _id: 1 } },
@@ -750,20 +798,24 @@ describe('elasticsearch-api', () => {
750
798
  });
751
799
  });
752
800
 
753
- it('will not remove _type from record in a bulk send', async () => {
801
+ it('will not remove _type from record in a bulkSend', async () => {
754
802
  const es7client = cloneDeep(client);
755
803
 
756
804
  es7client.transport._config = { apiVersion: '7.0' };
757
805
 
758
806
  const api = esApi(es7client, logger);
759
807
 
760
- const myBulkData = [
761
- { delete: { _index: 'some_index', _type: 'events', _id: 5 } },
762
- { index: { _index: 'some_index', _type: 'events', _id: 1 } },
763
- { title: 'foo', _type: 'doc', name: 'joe' }
764
- ];
765
-
766
- await api.bulkSend(myBulkData);
808
+ await api.bulkSend([{
809
+ action: {
810
+ delete: { _index: 'some_index', _type: 'events', _id: 5 }
811
+ },
812
+ },
813
+ {
814
+ action: {
815
+ index: { _index: 'some_index', _type: 'events', _id: 1 }
816
+ },
817
+ data: { title: 'foo', _type: 'doc', name: 'joe' }
818
+ }]);
767
819
  expect(bulkData).toEqual({
768
820
  body: [
769
821
  { delete: { _index: 'some_index', _id: 5 } },
@@ -773,20 +825,25 @@ describe('elasticsearch-api', () => {
773
825
  });
774
826
  });
775
827
 
776
- it('will not err if no _type in es7 bulk request metadata', async () => {
828
+ it('will not err if no _type in es7 bulkSend request metadata', async () => {
777
829
  const es7client = cloneDeep(client);
778
830
 
779
831
  es7client.transport._config = { apiVersion: '7.0' };
780
832
 
781
833
  const api = esApi(es7client, logger);
782
834
 
783
- const myBulkData = [
784
- { delete: { _index: 'some_index', _id: 5 } },
785
- { index: { _index: 'some_index', _id: 1 } },
786
- { title: 'foo', _type: 'doc', name: 'joe' }
787
- ];
835
+ await api.bulkSend([{
836
+ action: {
837
+ delete: { _index: 'some_index', _type: 'events', _id: 5 }
838
+ },
839
+ },
840
+ {
841
+ action: {
842
+ index: { _index: 'some_index', _type: 'events', _id: 1 }
843
+ },
844
+ data: { title: 'foo', _type: 'doc', name: 'joe' }
845
+ }]);
788
846
 
789
- await api.bulkSend(myBulkData);
790
847
  expect(bulkData).toEqual({
791
848
  body: [
792
849
  { delete: { _index: 'some_index', _id: 5 } },
@@ -798,36 +855,35 @@ describe('elasticsearch-api', () => {
798
855
 
799
856
  it('can call bulkSend with errors', async () => {
800
857
  const api = esApi(client, logger);
801
- const myBulkData = [
802
- { index: { _index: 'some_index', _type: 'events', _id: 1 } },
803
- { title: 'foo' },
804
- { delete: { _index: 'some_index', _type: 'events', _id: 5 } }
805
- ];
858
+ const myBulkData = [{
859
+ action: {
860
+ index: { _index: 'some_index', _type: 'events', _id: 1 }
861
+ },
862
+ data: { title: 'foo' }
863
+ },
864
+ {
865
+ action: {
866
+ delete: { _index: 'some_index', _type: 'events', _id: 5 }
867
+ }
868
+ }];
806
869
 
807
870
  bulkError = [
808
871
  'es_rejected_execution_exception',
809
872
  'es_rejected_execution_exception',
810
- 'es_rejected_execution_exception'
811
873
  ];
812
874
 
813
- const [results] = await Promise.all([
814
- api.bulkSend(myBulkData),
815
- waitFor(20, () => {
816
- bulkError = false;
817
- })
818
- ]);
875
+ waitFor(20, () => {
876
+ bulkError = false;
877
+ });
878
+ const result = await api.bulkSend(myBulkData);
819
879
 
820
- expect(results).toBeTruthy();
821
- bulkError = ['some_thing_else', 'some_thing_else', 'some_thing_else'];
880
+ expect(result).toBe(2);
822
881
 
823
- return expect(
824
- Promise.all([
825
- api.bulkSend(myBulkData),
826
- waitFor(20, () => {
827
- bulkError = false;
828
- })
829
- ])
830
- ).rejects.toThrow(/some_thing_else--someReason/);
882
+ bulkError = ['some_thing_else', 'some_thing_else'];
883
+
884
+ await expect(
885
+ api.bulkSend(myBulkData)
886
+ ).rejects.toThrow('bulk send error: some_thing_else--someReason');
831
887
  });
832
888
 
833
889
  it('can call buildQuery for geo queries', () => {
package/types/index.d.ts CHANGED
@@ -28,7 +28,12 @@ declare namespace elasticsearchAPI {
28
28
  remove: (query: es.DeleteDocumentParams) => Promise<es.DeleteDocumentResponse>;
29
29
  version: () => Promise<boolean>;
30
30
  putTemplate: (template: any, name: string) => Promise<any>;
31
- bulkSend: (data: any[]) => Promise<any>;
31
+ /**
32
+ * The new and improved bulk send with proper retry support
33
+ *
34
+ * @returns the number of affected rows
35
+ */
36
+ bulkSend: (data: BulkRecord[]) => Promise<number>;
32
37
  nodeInfo: (query: any) => Promise<any>;
33
38
  nodeStats: (query: any) => Promise<any>;
34
39
  buildQuery: (opConfig: Config, msg: any) => es.SearchParams;
@@ -41,4 +46,32 @@ declare namespace elasticsearchAPI {
41
46
  validateGeoParameters: (opConfig: any) => any;
42
47
  getESVersion: () => number;
43
48
  }
49
+
50
+ /**
51
+ * This is used for improved bulk sending function
52
+ */
53
+ export interface BulkRecord {
54
+ action: AnyBulkAction;
55
+ data?: UpdateConfig | DataEntity;
56
+ }
57
+
58
+ /**
59
+ * This is used for improved bulk sending function
60
+ */
61
+ export interface AnyBulkAction {
62
+ update?: Partial<BulkActionMetadata>;
63
+ index?: Partial<BulkActionMetadata>;
64
+ create?: Partial<BulkActionMetadata>;
65
+ delete?: Partial<BulkActionMetadata>;
66
+ }
67
+
68
+ /**
69
+ * This is used for improved bulk sending function
70
+ */
71
+ export interface BulkActionMetadata {
72
+ _index: string;
73
+ _type: string;
74
+ _id: string | number;
75
+ retry_on_conflict?: number;
76
+ }
44
77
  }