@certik/skynet 0.10.7 → 0.10.10

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/indexer.js CHANGED
@@ -1,595 +1,596 @@
1
- const meow = require("meow");
2
- const { createRecord, getRecordByKey } = require("./dynamodb");
3
- const { getEnvironment } = require("./env");
4
- const { exponentialRetry } = require("./availability");
5
- const { range, fillRange } = require("./util");
6
- const { getSelectorFlags, getSelectorDesc, toSelectorString } = require("./selector");
7
- const { getBinaryName } = require("./cli");
8
-
9
- const STATE_TABLE_NAME = "skynet-" + getEnvironment() + "-indexer-state";
10
-
11
- async function getIndexerState(name, selectorFlags) {
12
- const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
13
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
14
- });
15
-
16
- if (!stateItem) {
17
- return "N/A";
18
- }
19
-
20
- return stateItem.value;
21
- }
22
-
23
- async function getIndexerLatestId(name, selectorFlags) {
24
- const record = await getRecordByKey(STATE_TABLE_NAME, {
25
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
26
- });
27
-
28
- if (record) {
29
- return record.value;
30
- }
31
-
32
- return 0;
33
- }
34
-
35
- async function getIndexerValidatedId(name, selectorFlags) {
36
- const record = await getRecordByKey(STATE_TABLE_NAME, {
37
- name: `${name}Validate(${toSelectorString(selectorFlags)})`,
38
- });
39
-
40
- if (record) {
41
- return record.value;
42
- }
43
-
44
- return 0;
45
- }
46
-
47
- // for those indexers that can have progress tracked by a numeric state.type
48
- // such as block height, or timestamp
49
- // managing state would be helpful to reduce the build time
50
- // and avoid unnecessary computation & storage
51
- function createModeIndexerApp({
52
- binaryName,
53
- name,
54
- selector = {},
55
- build,
56
- // number of items run in a batch, determines the { from, to } to the build function
57
- buildBatchSize = 1,
58
- // number of build functions calling at a time
59
- buildConcurrency = 1,
60
- // commit updates every rolling window = buildBatchSize * buildConcurrency
61
- validate,
62
- // number of items run in a batch, determines the { from, to } to the validate function
63
- validateBatchSize = 1,
64
- // number of validate functions calling at a time
65
- validateConcurrency = 1,
66
- // commit updates every rolling window = validateBatchSize * validateConcurrency
67
- maxRetry = 2,
68
- state,
69
- }) {
70
- const finalState = {
71
- type: "block",
72
- getMinId: async () => 1,
73
- getMaxId: async () => {
74
- throw new Error("must implement getMaxId");
75
- },
76
- ...state,
77
- };
78
-
79
- async function runMode({ mode, from, to, status, verbose, ...selectorFlags }) {
80
- if (status) {
81
- const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
82
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
83
- });
84
-
85
- const fromItem = await getRecordByKey(STATE_TABLE_NAME, {
86
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
87
- });
88
-
89
- const validateItem = await getRecordByKey(STATE_TABLE_NAME, {
90
- name: `${name}Validate(${toSelectorString(selectorFlags)})`,
91
- });
92
-
93
- console.log(`RebuildState=${stateItem?.value} Since=${fromItem?.value} Validated=${validateItem?.value}`);
94
-
95
- process.exit(0);
96
- }
97
-
98
- if (from === 0) {
99
- from = await finalState.getMinId(selectorFlags);
100
- }
101
-
102
- console.log(
103
- `[MODE INDEXER] mode=${mode}, from=${from}, to=${
104
- to > 0 ? to : "undefined"
105
- }, env=${getEnvironment()}, ${toSelectorString(selectorFlags, ", ")}`
106
- );
107
-
108
- if (mode === "reset") {
109
- await runReset(selectorFlags, from);
110
- } else if (mode === "rebuild") {
111
- await runReset(selectorFlags);
112
- await runRebuild(selectorFlags, from, to > 0 ? to : await finalState.getMaxId(selectorFlags), verbose);
113
- } else if (mode === "resume-rebuild") {
114
- const previousRebuildEnds = await getIndexerLatestId(name, selectorFlags);
115
-
116
- await runRebuild(
117
- selectorFlags,
118
- previousRebuildEnds ? previousRebuildEnds + 1 : from,
119
- to > 0 ? to : await finalState.getMaxId(selectorFlags),
120
- verbose
121
- );
122
- } else if (mode === "validate" || mode === "validation") {
123
- const previousRebuildEnds = await getIndexerLatestId(name, selectorFlags);
124
-
125
- const previousValidatedTo = await getIndexerValidatedId(name, selectorFlags);
126
-
127
- const shouldSaveState = to === 0;
128
-
129
- await runValidate(
130
- selectorFlags,
131
- from > 0 ? from : previousValidatedTo,
132
- to > 0 ? to : previousRebuildEnds,
133
- shouldSaveState,
134
- verbose
135
- );
136
- } else if (mode === "one") {
137
- if (to > 0) {
138
- console.log("[MODE INDEXER] one mode ignores --to option. you may want to use range mode instead");
139
- }
140
- await runRange(selectorFlags, from, from, verbose);
141
- } else if (mode === "range") {
142
- await runRange(selectorFlags, from, to, verbose);
143
- } else {
144
- await runDelta(selectorFlags, verbose);
145
- }
146
- }
147
-
148
- async function runRange(selectorFlags, from, to, verbose) {
149
- const startTime = Date.now();
150
-
151
- console.log(
152
- `[MODE INDEXER] building range, from=${from}, to=${to}, ${toSelectorString(
153
- selectorFlags,
154
- ", "
155
- )}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`
156
- );
157
-
158
- const failedIds = await execBuild(selectorFlags, from, to, verbose, false);
159
-
160
- if (failedIds.length > 0) {
161
- console.log(`[MODE INDEXER] built with some failed ${finalState.type}`, failedIds);
162
- process.exit(1);
163
- } else {
164
- console.log(`[MODE INDEXER] built successfully in ${Date.now() - startTime}ms`);
165
- process.exit(0);
166
- }
167
- }
168
-
169
- async function runValidate(selectorFlags, from, to, shouldSaveState, verbose) {
170
- if (!validate) {
171
- console.log(`[MODE INDEXER] the indexer doesn't support validate mode, validate function not implemented`);
172
- process.exit(1);
173
- }
174
-
175
- const startTime = Date.now();
176
-
177
- console.log(
178
- `[MODE INDEXER] validating, from=${from}, to=${to}, ${toSelectorString(
179
- selectorFlags,
180
- ", "
181
- )}, batchSize=${validateBatchSize}, concurrency=${validateConcurrency}`
182
- );
183
-
184
- const windows = range(from, to, validateBatchSize * validateConcurrency);
185
-
186
- console.log(
187
- `[MODE INDEXER] from=${from}, to=${to}, batchSize=${validateBatchSize}, concurrency=${validateConcurrency}`
188
- );
189
-
190
- for (let [windowStart, windowEnd] of windows) {
191
- console.log(`[MODE INDEXER] validating window ${windowStart}~${windowEnd}, concurrency=${validateConcurrency}`);
192
-
193
- const batches = range(windowStart, windowEnd, validateBatchSize);
194
-
195
- // add a retry for errors
196
- await Promise.all(
197
- batches.map(async ([batchStart, batchEnd]) => {
198
- const result = await exponentialRetry(
199
- async () => {
200
- try {
201
- await validate({
202
- ...selectorFlags,
203
- from: batchStart,
204
- to: batchEnd,
205
- verbose,
206
- });
207
-
208
- return true;
209
- } catch (err) {
210
- console.error(`got error in validation`, err);
211
-
212
- return false;
213
- }
214
- },
215
- {
216
- maxRetry,
217
- test: (r) => r,
218
- verbose,
219
- }
220
- );
221
-
222
- if (!result) {
223
- throw new Error(`Terminate validation due to critical errors, from=${batchStart}, to=${batchEnd}`);
224
- }
225
- })
226
- );
227
-
228
- if (shouldSaveState) {
229
- await createRecord(STATE_TABLE_NAME, {
230
- name: `${name}Validate(${toSelectorString(selectorFlags)})`,
231
- value: to,
232
- });
233
-
234
- if (verbose) {
235
- console.log(`[MODE INDEXER] updated processed ${finalState.type} to ${windowEnd}`);
236
- }
237
- }
238
- }
239
-
240
- console.log(
241
- `[MODE INDEXER] validated ${to - from + 1} ${finalState.type} successfully in ${Date.now() - startTime}ms`
242
- );
243
- }
244
-
245
- async function execBuild(selectorFlags, from, to, verbose, shouldSaveState = false) {
246
- let failedIds = [];
247
-
248
- const windows = range(from, to, buildBatchSize * buildConcurrency);
249
-
250
- for (let [windowStart, windowEnd] of windows) {
251
- console.log(`[MODE INDEXER] building window ${windowStart}~${windowEnd}, concurrency = ${buildConcurrency}`);
252
-
253
- const batches = range(windowStart, windowEnd, buildBatchSize);
254
-
255
- // add a retry for errors
256
- const batchResults = await Promise.all(
257
- batches.map(async ([batchStart, batchEnd]) => {
258
- await exponentialRetry(
259
- async () => {
260
- try {
261
- const ids = await build({
262
- ...selectorFlags,
263
- from: batchStart,
264
- to: batchEnd,
265
- verbose,
266
- });
267
-
268
- if (ids && ids.length > 0) {
269
- return ids;
270
- } else {
271
- return false;
272
- }
273
- } catch (err) {
274
- console.error(`[MODE INDEXER] got error in build`, err);
275
-
276
- return fillRange(batchStart, batchEnd);
277
- }
278
- },
279
- {
280
- maxRetry,
281
- test: (r) => !r,
282
- verbose,
283
- }
284
- );
285
- })
286
- );
287
-
288
- if (shouldSaveState) {
289
- await createRecord(STATE_TABLE_NAME, {
290
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
291
- value: windowEnd,
292
- });
293
-
294
- if (verbose) {
295
- console.log(`[MODE INDEXER] updated processed ${finalState.type} to ${windowEnd}`);
296
- }
297
- }
298
-
299
- batchResults.forEach((ids) => {
300
- if (ids) {
301
- failedIds = failedIds.concat(ids);
302
- }
303
- });
304
- }
305
-
306
- failedIds.sort();
307
-
308
- return failedIds;
309
- }
310
-
311
- async function runRebuild(selectorFlags, from, to, verbose) {
312
- const startTime = Date.now();
313
-
314
- console.log(
315
- `[MODE INDEXER] rebuilding, from=${from}, to=${to}, ${toSelectorString(
316
- selectorFlags,
317
- ", "
318
- )}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`
319
- );
320
-
321
- // add a flag to stop delta from running
322
- await createRecord(STATE_TABLE_NAME, {
323
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
324
- value: "running",
325
- });
326
-
327
- const failedIds = await execBuild(selectorFlags, from, to, verbose, true);
328
-
329
- // even if some transactions are failed we should continue to allow delta job running
330
- // because rebuild is usally heavy
331
- await createRecord(STATE_TABLE_NAME, {
332
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
333
- value: to,
334
- });
335
-
336
- await createRecord(STATE_TABLE_NAME, {
337
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
338
- value: "succeed",
339
- });
340
-
341
- if (failedIds.length > 0) {
342
- console.log(
343
- `[MODE INDEXER] built ${to - from + 1} ${finalState.type} with some failed ${finalState.type}`,
344
- failedIds
345
- );
346
- process.exit(1);
347
- } else {
348
- console.log(
349
- `[MODE INDEXER] built ${to - from + 1} ${finalState.type} successfully in ${Date.now() - startTime}ms`
350
- );
351
- process.exit(0);
352
- }
353
- }
354
-
355
- async function runDelta(selectorFlags, verbose) {
356
- const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
357
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
358
- });
359
-
360
- // only build when rebuild succeed
361
- if (stateItem && stateItem.value === "succeed") {
362
- const from = (await getIndexerLatestId(name, selectorFlags)) + 1;
363
-
364
- const startTime = Date.now();
365
- const to = await state.getMaxId(selectorFlags);
366
-
367
- if (to <= from) {
368
- console.log(
369
- `[MODE INDEXER] skip delta because there're no items need to be processed, ${toSelectorString(
370
- selectorFlags,
371
- ", "
372
- )}`
373
- );
374
-
375
- return;
376
- }
377
-
378
- console.log(
379
- `[MODE INDEXER] starting delta, from=${from}, to=${to}, ${toSelectorString(
380
- selectorFlags,
381
- ", "
382
- )}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`
383
- );
384
-
385
- try {
386
- const failedIds = await execBuild(selectorFlags, from, to, verbose, true);
387
-
388
- if (failedIds.length > 0) {
389
- console.log("[MODE INDEXER] built with some failed txs", failedIds);
390
-
391
- await createRecord(STATE_TABLE_NAME, {
392
- name: `${name}DeltaState(${toSelectorString(selectorFlags)})`,
393
- value: "failed",
394
- });
395
-
396
- await createRecord(STATE_TABLE_NAME, {
397
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
398
- value: Math.min(to, failedIds[0]),
399
- });
400
-
401
- process.exit(1);
402
- } else {
403
- await createRecord(STATE_TABLE_NAME, {
404
- name: `${name}DeltaState(${toSelectorString(selectorFlags)})`,
405
- value: "succeed",
406
- });
407
-
408
- await createRecord(STATE_TABLE_NAME, {
409
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
410
- value: to,
411
- });
412
-
413
- console.log(`[MODE INDEXER] built successfully in ${Date.now() - startTime}ms`);
414
- process.exit(0);
415
- }
416
- } catch (err) {
417
- console.error("[MODE INDEXER] delta build failed", from, to, err);
418
-
419
- process.exit(1);
420
- }
421
- } else {
422
- console.log("[MODE INDEXER] skip because rebuild hasn't done yet");
423
- }
424
- }
425
-
426
- async function runReset(selectorFlags) {
427
- const startTime = Date.now();
428
-
429
- console.log(`[MODE INDEXER] starting reset, ${toSelectorString(selectorFlags, ", ")}`);
430
-
431
- console.log("[MODE INDEXER] reset state", STATE_TABLE_NAME);
432
- await createRecord(STATE_TABLE_NAME, {
433
- name: `${name}Since(${toSelectorString(selectorFlags)})`,
434
- value: 0,
435
- });
436
-
437
- await createRecord(STATE_TABLE_NAME, {
438
- name: `${name}Validate(${toSelectorString(selectorFlags)})`,
439
- value: 0,
440
- });
441
-
442
- await createRecord(STATE_TABLE_NAME, {
443
- name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
444
- value: "init",
445
- });
446
-
447
- console.log(`[MODE INDEXER] reset successfully in ${Date.now() - startTime}ms`);
448
- }
449
-
450
- function run() {
451
- if (!binaryName) {
452
- binaryName = getBinaryName();
453
- }
454
-
455
- const cli = meow(
456
- `
457
- Usage
458
-
459
- $ ${binaryName} <options>
460
-
461
- Options
462
- ${getSelectorDesc(selector)}
463
- --mode could be delta/rebuild/resume-rebuild/validate/one/range/reset
464
- --from min ${finalState.type} to build
465
- --to max ${finalState.type} to build
466
- --status print status of indexer and exit
467
- --verbose Output debug messages
468
- `,
469
- {
470
- description: false,
471
- version: false,
472
- flags: {
473
- ...getSelectorFlags(selector),
474
- mode: {
475
- type: "string",
476
- default: "delta",
477
- },
478
- from: {
479
- alias: "since",
480
- type: "number",
481
- default: 0,
482
- },
483
- to: {
484
- alias: "until",
485
- type: "number",
486
- default: 0,
487
- },
488
- status: {
489
- type: "boolean",
490
- default: false,
491
- },
492
- verbose: {
493
- type: "boolean",
494
- default: false,
495
- },
496
- },
497
- }
498
- );
499
-
500
- runMode(cli.flags).catch((err) => {
501
- console.error(err);
502
- process.exit(1);
503
- });
504
- }
505
-
506
- return { run };
507
- }
508
-
509
- // for those indexers that does not rely on a curosr
510
- // e.g. should always rebuild everything from scratch
511
- // or that the state can be easily inferred from existing data
512
- function createIndexerApp({ binaryName, name, selector = {}, build, maxRetry = 2 }) {
513
- function run() {
514
- if (!binaryName) {
515
- binaryName = getBinaryName();
516
- }
517
-
518
- const cli = meow(
519
- `
520
- Usage
521
- $ ${binaryName} <options>
522
-
523
- Options
524
- ${getSelectorDesc(selector)}
525
- --verbose Output debug messages
526
- `,
527
- {
528
- description: false,
529
- version: false,
530
- flags: {
531
- ...getSelectorFlags(selector),
532
- verbose: {
533
- type: "boolean",
534
- default: false,
535
- },
536
- },
537
- }
538
- );
539
-
540
- async function runBuild({ verbose, ...selectorFlags }) {
541
- const startTime = Date.now();
542
-
543
- if (Object.keys(selectorFlags).length > 0) {
544
- console.log(`[INDEXER] starting build, ${toSelectorString(selectorFlags, ", ")}`);
545
- } else {
546
- console.log(`[INDEXER] starting build`);
547
- }
548
-
549
- const result = await exponentialRetry(
550
- async () => {
551
- try {
552
- await build({ verbose, ...selectorFlags });
553
-
554
- return true;
555
- } catch (err) {
556
- console.log(`[INDEXER] got error in build`, err);
557
-
558
- return false;
559
- }
560
- },
561
- {
562
- maxRetry,
563
- test: (r) => r,
564
- verbose,
565
- }
566
- );
567
-
568
- if (!result) {
569
- throw new Error(`[INDEXER] Build failed due to critical errors`);
570
- }
571
-
572
- await createRecord(STATE_TABLE_NAME, {
573
- name: `${name}UpdatedAt(${toSelectorString(selectorFlags)})`,
574
- value: new Date().toISOString(),
575
- });
576
-
577
- console.log(`[INDEXER] build successfully in ${Date.now() - startTime}ms`);
578
- }
579
-
580
- runBuild(cli.flags).catch((err) => {
581
- console.error(err);
582
- process.exit(1);
583
- });
584
- }
585
-
586
- return { run };
587
- }
588
-
589
- module.exports = {
590
- createModeIndexerApp,
591
- createIndexerApp,
592
- getIndexerState,
593
- getIndexerLatestId,
594
- getIndexerValidatedId,
595
- };
1
+ const meow = require("meow");
2
+ const { createRecord, getRecordByKey } = require("./dynamodb");
3
+ const { getEnvironment } = require("./env");
4
+ const { exponentialRetry } = require("./availability");
5
+ const { range, fillRange } = require("./util");
6
+ const { getSelectorFlags, getSelectorDesc, toSelectorString } = require("./selector");
7
+ const { getBinaryName } = require("./cli");
8
+ const { inline } = require("./log");
9
+
10
+ const STATE_TABLE_NAME = "skynet-" + getEnvironment() + "-indexer-state";
11
+
12
+ async function getIndexerState(name, selectorFlags) {
13
+ const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
14
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
15
+ });
16
+
17
+ if (!stateItem) {
18
+ return "N/A";
19
+ }
20
+
21
+ return stateItem.value;
22
+ }
23
+
24
+ async function getIndexerLatestId(name, selectorFlags) {
25
+ const record = await getRecordByKey(STATE_TABLE_NAME, {
26
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
27
+ });
28
+
29
+ if (record) {
30
+ return record.value;
31
+ }
32
+
33
+ return 0;
34
+ }
35
+
36
+ async function getIndexerValidatedId(name, selectorFlags) {
37
+ const record = await getRecordByKey(STATE_TABLE_NAME, {
38
+ name: `${name}Validate(${toSelectorString(selectorFlags)})`,
39
+ });
40
+
41
+ if (record) {
42
+ return record.value;
43
+ }
44
+
45
+ return 0;
46
+ }
47
+
48
+ // for those indexers that can have progress tracked by a numeric state.type
49
+ // such as block height, or timestamp
50
+ // managing state would be helpful to reduce the build time
51
+ // and avoid unnecessary computation & storage
52
+ function createModeIndexerApp({
53
+ binaryName,
54
+ name,
55
+ selector = {},
56
+ build,
57
+ // number of items run in a batch, determines the { from, to } to the build function
58
+ buildBatchSize = 1,
59
+ // number of build functions calling at a time
60
+ buildConcurrency = 1,
61
+ // commit updates every rolling window = buildBatchSize * buildConcurrency
62
+ validate,
63
+ // number of items run in a batch, determines the { from, to } to the validate function
64
+ validateBatchSize = 1,
65
+ // number of validate functions calling at a time
66
+ validateConcurrency = 1,
67
+ // commit updates every rolling window = validateBatchSize * validateConcurrency
68
+ maxRetry = 2,
69
+ state,
70
+ }) {
71
+ const finalState = {
72
+ type: "block",
73
+ getMinId: async () => 1,
74
+ getMaxId: async () => {
75
+ throw new Error("must implement getMaxId");
76
+ },
77
+ ...state,
78
+ };
79
+
80
+ async function runMode({ mode, from, to, status, verbose, ...selectorFlags }) {
81
+ if (status) {
82
+ const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
83
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
84
+ });
85
+
86
+ const fromItem = await getRecordByKey(STATE_TABLE_NAME, {
87
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
88
+ });
89
+
90
+ const validateItem = await getRecordByKey(STATE_TABLE_NAME, {
91
+ name: `${name}Validate(${toSelectorString(selectorFlags)})`,
92
+ });
93
+
94
+ inline.log(`RebuildState=${stateItem?.value} Since=${fromItem?.value} Validated=${validateItem?.value}`);
95
+
96
+ process.exit(0);
97
+ }
98
+
99
+ if (from === 0) {
100
+ from = await finalState.getMinId(selectorFlags);
101
+ }
102
+
103
+ inline.log(
104
+ `[MODE INDEXER] mode=${mode}, from=${from}, to=${
105
+ to > 0 ? to : "undefined"
106
+ }, env=${getEnvironment()}, ${toSelectorString(selectorFlags, ", ")}`
107
+ );
108
+
109
+ if (mode === "reset") {
110
+ await runReset(selectorFlags, from);
111
+ } else if (mode === "rebuild") {
112
+ await runReset(selectorFlags);
113
+ await runRebuild(selectorFlags, from, to > 0 ? to : await finalState.getMaxId(selectorFlags), verbose);
114
+ } else if (mode === "resume-rebuild") {
115
+ const previousRebuildEnds = await getIndexerLatestId(name, selectorFlags);
116
+
117
+ await runRebuild(
118
+ selectorFlags,
119
+ previousRebuildEnds ? previousRebuildEnds + 1 : from,
120
+ to > 0 ? to : await finalState.getMaxId(selectorFlags),
121
+ verbose
122
+ );
123
+ } else if (mode === "validate" || mode === "validation") {
124
+ const previousRebuildEnds = await getIndexerLatestId(name, selectorFlags);
125
+
126
+ const previousValidatedTo = await getIndexerValidatedId(name, selectorFlags);
127
+
128
+ const shouldSaveState = to === 0;
129
+
130
+ await runValidate(
131
+ selectorFlags,
132
+ from > 0 ? from : previousValidatedTo,
133
+ to > 0 ? to : previousRebuildEnds,
134
+ shouldSaveState,
135
+ verbose
136
+ );
137
+ } else if (mode === "one") {
138
+ if (to > 0) {
139
+ inline.log("[MODE INDEXER] one mode ignores --to option. you may want to use range mode instead");
140
+ }
141
+ await runRange(selectorFlags, from, from, verbose);
142
+ } else if (mode === "range") {
143
+ await runRange(selectorFlags, from, to, verbose);
144
+ } else {
145
+ await runDelta(selectorFlags, verbose);
146
+ }
147
+ }
148
+
149
+ async function runRange(selectorFlags, from, to, verbose) {
150
+ const startTime = Date.now();
151
+
152
+ inline.log(
153
+ `[MODE INDEXER] building range, from=${from}, to=${to}, ${toSelectorString(
154
+ selectorFlags,
155
+ ", "
156
+ )}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`
157
+ );
158
+
159
+ const failedIds = await execBuild(selectorFlags, from, to, verbose, false);
160
+
161
+ if (failedIds.length > 0) {
162
+ inline.log(`[MODE INDEXER] built with some failed ${finalState.type}`, failedIds);
163
+ process.exit(1);
164
+ } else {
165
+ inline.log(`[MODE INDEXER] built successfully in ${Date.now() - startTime}ms`);
166
+ process.exit(0);
167
+ }
168
+ }
169
+
170
+ async function runValidate(selectorFlags, from, to, shouldSaveState, verbose) {
171
+ if (!validate) {
172
+ inline.log(`[MODE INDEXER] the indexer doesn't support validate mode, validate function not implemented`);
173
+ process.exit(1);
174
+ }
175
+
176
+ const startTime = Date.now();
177
+
178
+ inline.log(
179
+ `[MODE INDEXER] validating, from=${from}, to=${to}, ${toSelectorString(
180
+ selectorFlags,
181
+ ", "
182
+ )}, batchSize=${validateBatchSize}, concurrency=${validateConcurrency}`
183
+ );
184
+
185
+ const windows = range(from, to, validateBatchSize * validateConcurrency);
186
+
187
+ inline.log(
188
+ `[MODE INDEXER] from=${from}, to=${to}, batchSize=${validateBatchSize}, concurrency=${validateConcurrency}`
189
+ );
190
+
191
+ for (let [windowStart, windowEnd] of windows) {
192
+ inline.log(`[MODE INDEXER] validating window ${windowStart}~${windowEnd}, concurrency=${validateConcurrency}`);
193
+
194
+ const batches = range(windowStart, windowEnd, validateBatchSize);
195
+
196
+ // add a retry for errors
197
+ await Promise.all(
198
+ batches.map(async ([batchStart, batchEnd]) => {
199
+ const result = await exponentialRetry(
200
+ async () => {
201
+ try {
202
+ await validate({
203
+ ...selectorFlags,
204
+ from: batchStart,
205
+ to: batchEnd,
206
+ verbose,
207
+ });
208
+
209
+ return true;
210
+ } catch (err) {
211
+ inline.error(`got error in validation`, err);
212
+
213
+ return false;
214
+ }
215
+ },
216
+ {
217
+ maxRetry,
218
+ test: (r) => r,
219
+ verbose,
220
+ }
221
+ );
222
+
223
+ if (!result) {
224
+ throw new Error(`Terminate validation due to critical errors, from=${batchStart}, to=${batchEnd}`);
225
+ }
226
+ })
227
+ );
228
+
229
+ if (shouldSaveState) {
230
+ await createRecord(STATE_TABLE_NAME, {
231
+ name: `${name}Validate(${toSelectorString(selectorFlags)})`,
232
+ value: to,
233
+ });
234
+
235
+ if (verbose) {
236
+ inline.log(`[MODE INDEXER] updated processed ${finalState.type} to ${windowEnd}`);
237
+ }
238
+ }
239
+ }
240
+
241
+ inline.log(
242
+ `[MODE INDEXER] validated ${to - from + 1} ${finalState.type} successfully in ${Date.now() - startTime}ms`
243
+ );
244
+ }
245
+
246
+ async function execBuild(selectorFlags, from, to, verbose, shouldSaveState = false) {
247
+ let failedIds = [];
248
+
249
+ const windows = range(from, to, buildBatchSize * buildConcurrency);
250
+
251
+ for (let [windowStart, windowEnd] of windows) {
252
+ inline.log(`[MODE INDEXER] building window ${windowStart}~${windowEnd}, concurrency = ${buildConcurrency}`);
253
+
254
+ const batches = range(windowStart, windowEnd, buildBatchSize);
255
+
256
+ // add a retry for errors
257
+ const batchResults = await Promise.all(
258
+ batches.map(async ([batchStart, batchEnd]) => {
259
+ await exponentialRetry(
260
+ async () => {
261
+ try {
262
+ const ids = await build({
263
+ ...selectorFlags,
264
+ from: batchStart,
265
+ to: batchEnd,
266
+ verbose,
267
+ });
268
+
269
+ if (ids && ids.length > 0) {
270
+ return ids;
271
+ } else {
272
+ return false;
273
+ }
274
+ } catch (err) {
275
+ inline.error(`[MODE INDEXER] got error in build`, err);
276
+
277
+ return fillRange(batchStart, batchEnd);
278
+ }
279
+ },
280
+ {
281
+ maxRetry,
282
+ test: (r) => !r,
283
+ verbose,
284
+ }
285
+ );
286
+ })
287
+ );
288
+
289
+ if (shouldSaveState) {
290
+ await createRecord(STATE_TABLE_NAME, {
291
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
292
+ value: windowEnd,
293
+ });
294
+
295
+ if (verbose) {
296
+ inline.log(`[MODE INDEXER] updated processed ${finalState.type} to ${windowEnd}`);
297
+ }
298
+ }
299
+
300
+ batchResults.forEach((ids) => {
301
+ if (ids) {
302
+ failedIds = failedIds.concat(ids);
303
+ }
304
+ });
305
+ }
306
+
307
+ failedIds.sort();
308
+
309
+ return failedIds;
310
+ }
311
+
312
+ async function runRebuild(selectorFlags, from, to, verbose) {
313
+ const startTime = Date.now();
314
+
315
+ inline.log(
316
+ `[MODE INDEXER] rebuilding, from=${from}, to=${to}, ${toSelectorString(
317
+ selectorFlags,
318
+ ", "
319
+ )}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`
320
+ );
321
+
322
+ // add a flag to stop delta from running
323
+ await createRecord(STATE_TABLE_NAME, {
324
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
325
+ value: "running",
326
+ });
327
+
328
+ const failedIds = await execBuild(selectorFlags, from, to, verbose, true);
329
+
330
+ // even if some transactions are failed we should continue to allow delta job running
331
+ // because rebuild is usally heavy
332
+ await createRecord(STATE_TABLE_NAME, {
333
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
334
+ value: to,
335
+ });
336
+
337
+ await createRecord(STATE_TABLE_NAME, {
338
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
339
+ value: "succeed",
340
+ });
341
+
342
+ if (failedIds.length > 0) {
343
+ inline.log(
344
+ `[MODE INDEXER] built ${to - from + 1} ${finalState.type} with some failed ${finalState.type}`,
345
+ failedIds
346
+ );
347
+ process.exit(1);
348
+ } else {
349
+ inline.log(
350
+ `[MODE INDEXER] built ${to - from + 1} ${finalState.type} successfully in ${Date.now() - startTime}ms`
351
+ );
352
+ process.exit(0);
353
+ }
354
+ }
355
+
356
+ async function runDelta(selectorFlags, verbose) {
357
+ const stateItem = await getRecordByKey(STATE_TABLE_NAME, {
358
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
359
+ });
360
+
361
+ // only build when rebuild succeed
362
+ if (stateItem && stateItem.value === "succeed") {
363
+ const from = (await getIndexerLatestId(name, selectorFlags)) + 1;
364
+
365
+ const startTime = Date.now();
366
+ const to = await state.getMaxId(selectorFlags);
367
+
368
+ if (to <= from) {
369
+ inline.log(
370
+ `[MODE INDEXER] skip delta because there're no items need to be processed, ${toSelectorString(
371
+ selectorFlags,
372
+ ", "
373
+ )}`
374
+ );
375
+
376
+ return;
377
+ }
378
+
379
+ inline.log(
380
+ `[MODE INDEXER] starting delta, from=${from}, to=${to}, ${toSelectorString(
381
+ selectorFlags,
382
+ ", "
383
+ )}, batchSize=${buildBatchSize}, concurrency=${buildConcurrency}`
384
+ );
385
+
386
+ try {
387
+ const failedIds = await execBuild(selectorFlags, from, to, verbose, true);
388
+
389
+ if (failedIds.length > 0) {
390
+ inline.log("[MODE INDEXER] built with some failed txs", failedIds);
391
+
392
+ await createRecord(STATE_TABLE_NAME, {
393
+ name: `${name}DeltaState(${toSelectorString(selectorFlags)})`,
394
+ value: "failed",
395
+ });
396
+
397
+ await createRecord(STATE_TABLE_NAME, {
398
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
399
+ value: Math.min(to, failedIds[0]),
400
+ });
401
+
402
+ process.exit(1);
403
+ } else {
404
+ await createRecord(STATE_TABLE_NAME, {
405
+ name: `${name}DeltaState(${toSelectorString(selectorFlags)})`,
406
+ value: "succeed",
407
+ });
408
+
409
+ await createRecord(STATE_TABLE_NAME, {
410
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
411
+ value: to,
412
+ });
413
+
414
+ inline.log(`[MODE INDEXER] built successfully in ${Date.now() - startTime}ms`);
415
+ process.exit(0);
416
+ }
417
+ } catch (err) {
418
+ inline.error("[MODE INDEXER] delta build failed", from, to, err);
419
+
420
+ process.exit(1);
421
+ }
422
+ } else {
423
+ inline.log("[MODE INDEXER] skip because rebuild hasn't done yet");
424
+ }
425
+ }
426
+
427
+ async function runReset(selectorFlags) {
428
+ const startTime = Date.now();
429
+
430
+ inline.log(`[MODE INDEXER] starting reset, ${toSelectorString(selectorFlags, ", ")}`);
431
+
432
+ inline.log("[MODE INDEXER] reset state", STATE_TABLE_NAME);
433
+ await createRecord(STATE_TABLE_NAME, {
434
+ name: `${name}Since(${toSelectorString(selectorFlags)})`,
435
+ value: 0,
436
+ });
437
+
438
+ await createRecord(STATE_TABLE_NAME, {
439
+ name: `${name}Validate(${toSelectorString(selectorFlags)})`,
440
+ value: 0,
441
+ });
442
+
443
+ await createRecord(STATE_TABLE_NAME, {
444
+ name: `${name}RebuildState(${toSelectorString(selectorFlags)})`,
445
+ value: "init",
446
+ });
447
+
448
+ inline.log(`[MODE INDEXER] reset successfully in ${Date.now() - startTime}ms`);
449
+ }
450
+
451
+ function run() {
452
+ if (!binaryName) {
453
+ binaryName = getBinaryName();
454
+ }
455
+
456
+ const cli = meow(
457
+ `
458
+ Usage
459
+
460
+ $ ${binaryName} <options>
461
+
462
+ Options
463
+ ${getSelectorDesc(selector)}
464
+ --mode could be delta/rebuild/resume-rebuild/validate/one/range/reset
465
+ --from min ${finalState.type} to build
466
+ --to max ${finalState.type} to build
467
+ --status print status of indexer and exit
468
+ --verbose Output debug messages
469
+ `,
470
+ {
471
+ description: false,
472
+ version: false,
473
+ flags: {
474
+ ...getSelectorFlags(selector),
475
+ mode: {
476
+ type: "string",
477
+ default: "delta",
478
+ },
479
+ from: {
480
+ alias: "since",
481
+ type: "number",
482
+ default: 0,
483
+ },
484
+ to: {
485
+ alias: "until",
486
+ type: "number",
487
+ default: 0,
488
+ },
489
+ status: {
490
+ type: "boolean",
491
+ default: false,
492
+ },
493
+ verbose: {
494
+ type: "boolean",
495
+ default: false,
496
+ },
497
+ },
498
+ }
499
+ );
500
+
501
+ runMode(cli.flags).catch((err) => {
502
+ inline.error(err);
503
+ process.exit(1);
504
+ });
505
+ }
506
+
507
+ return { run };
508
+ }
509
+
510
+ // for those indexers that does not rely on a curosr
511
+ // e.g. should always rebuild everything from scratch
512
+ // or that the state can be easily inferred from existing data
513
+ function createIndexerApp({ binaryName, name, selector = {}, build, maxRetry = 2 }) {
514
+ function run() {
515
+ if (!binaryName) {
516
+ binaryName = getBinaryName();
517
+ }
518
+
519
+ const cli = meow(
520
+ `
521
+ Usage
522
+ $ ${binaryName} <options>
523
+
524
+ Options
525
+ ${getSelectorDesc(selector)}
526
+ --verbose Output debug messages
527
+ `,
528
+ {
529
+ description: false,
530
+ version: false,
531
+ flags: {
532
+ ...getSelectorFlags(selector),
533
+ verbose: {
534
+ type: "boolean",
535
+ default: false,
536
+ },
537
+ },
538
+ }
539
+ );
540
+
541
+ async function runBuild({ verbose, ...selectorFlags }) {
542
+ const startTime = Date.now();
543
+
544
+ if (Object.keys(selectorFlags).length > 0) {
545
+ inline.log(`[INDEXER] starting build, ${toSelectorString(selectorFlags, ", ")}`);
546
+ } else {
547
+ inline.log(`[INDEXER] starting build`);
548
+ }
549
+
550
+ const result = await exponentialRetry(
551
+ async () => {
552
+ try {
553
+ await build({ verbose, ...selectorFlags });
554
+
555
+ return true;
556
+ } catch (err) {
557
+ inline.log(`[INDEXER] got error in build`, err);
558
+
559
+ return false;
560
+ }
561
+ },
562
+ {
563
+ maxRetry,
564
+ test: (r) => r,
565
+ verbose,
566
+ }
567
+ );
568
+
569
+ if (!result) {
570
+ throw new Error(`[INDEXER] Build failed due to critical errors`);
571
+ }
572
+
573
+ await createRecord(STATE_TABLE_NAME, {
574
+ name: `${name}UpdatedAt(${toSelectorString(selectorFlags)})`,
575
+ value: new Date().toISOString(),
576
+ });
577
+
578
+ inline.log(`[INDEXER] build successfully in ${Date.now() - startTime}ms`);
579
+ }
580
+
581
+ runBuild(cli.flags).catch((err) => {
582
+ inline.error(err);
583
+ process.exit(1);
584
+ });
585
+ }
586
+
587
+ return { run };
588
+ }
589
+
590
+ module.exports = {
591
+ createModeIndexerApp,
592
+ createIndexerApp,
593
+ getIndexerState,
594
+ getIndexerLatestId,
595
+ getIndexerValidatedId,
596
+ };