@subsquid/batch-processor 0.0.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.
package/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # @subsquid/batch-processor
2
+
3
+ Batch processor ingests block data from the data source
4
+ and executes user defined data handler against received sequential batches of blocks.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Database is responsible for providing a persistent storage for data handlers
3
+ * and keeping the processor progress and status.
4
+ */
5
+ export type Database<S> = FinalDatabase<S> | HotDatabase<S>;
6
+ export interface FinalTxInfo {
7
+ prevHead: HashAndHeight;
8
+ nextHead: HashAndHeight;
9
+ isOnTop: boolean;
10
+ }
11
+ export interface FinalDatabase<S> {
12
+ supportsHotBlocks?: false;
13
+ connect(): Promise<HashAndHeight>;
14
+ transact(info: FinalTxInfo, cb: (store: S) => Promise<void>): Promise<void>;
15
+ }
16
+ export interface HotTxInfo {
17
+ finalizedHead: HashAndHeight;
18
+ baseHead: HashAndHeight;
19
+ newBlocks: HashAndHeight[];
20
+ }
21
+ export interface HotDatabase<S> {
22
+ supportsHotBlocks: true;
23
+ connect(): Promise<HotDatabaseState>;
24
+ transact(info: FinalTxInfo, cb: (store: S) => Promise<void>): Promise<void>;
25
+ /**
26
+ * @deprecated
27
+ */
28
+ transactHot(info: HotTxInfo, cb: (store: S, block: HashAndHeight) => Promise<void>): Promise<void>;
29
+ transactHot2?(info: HotTxInfo, cb: (store: S, blockSliceStart: number, blockSliceEnd: number) => Promise<void>): Promise<void>;
30
+ }
31
+ export interface HotDatabaseState extends HashAndHeight {
32
+ top: HashAndHeight[];
33
+ }
34
+ export interface HashAndHeight {
35
+ height: number;
36
+ hash: string;
37
+ }
38
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;AAG3D,MAAM,WAAW,WAAW;IACxB,QAAQ,EAAE,aAAa,CAAA;IACvB,QAAQ,EAAE,aAAa,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;CACnB;AAGD,MAAM,WAAW,aAAa,CAAC,CAAC;IAC5B,iBAAiB,CAAC,EAAE,KAAK,CAAA;IACzB,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAA;IACjC,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9E;AAGD,MAAM,WAAW,SAAS;IACtB,aAAa,EAAE,aAAa,CAAA;IAC5B,QAAQ,EAAE,aAAa,CAAA;IACvB,SAAS,EAAE,aAAa,EAAE,CAAA;CAC7B;AAGD,MAAM,WAAW,WAAW,CAAC,CAAC;IAC1B,iBAAiB,EAAE,IAAI,CAAA;IACvB,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3E;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAElG,YAAY,CAAC,CACT,IAAI,EAAE,SAAS,EACf,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAChF,OAAO,CAAC,IAAI,CAAC,CAAA;CACnB;AAGD,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACnD,GAAG,EAAE,aAAa,EAAE,CAAA;CACvB;AAGD,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACf"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import type { FiniteRange } from '@subsquid/util-internal-range';
2
+ export interface DataSource<B> {
3
+ getFinalizedHeight(): Promise<number>;
4
+ getBlockHash(height: number): Promise<string | undefined>;
5
+ getBlockStream(fromBlock?: number): AsyncIterable<B[]>;
6
+ getBlocksCountInRange?(range: FiniteRange): number;
7
+ }
8
+ //# sourceMappingURL=datasource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datasource.d.ts","sourceRoot":"","sources":["../src/datasource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,+BAA+B,CAAA;AAG9D,MAAM,WAAW,UAAU,CAAC,CAAC;IACzB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IACrC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACzD,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,CAAC,EAAE,CAAC,CAAA;IACtD,qBAAqB,CAAC,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAAA;CACrD"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=datasource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datasource.js","sourceRoot":"","sources":["../src/datasource.ts"],"names":[],"mappings":""}
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './database';
2
+ export * from './datasource';
3
+ export * from './run';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,OAAO,CAAA"}
package/lib/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./database"), exports);
18
+ __exportStar(require("./datasource"), exports);
19
+ __exportStar(require("./run"), exports);
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,6CAA0B;AAC1B,+CAA4B;AAC5B,wCAAqB"}
@@ -0,0 +1,21 @@
1
+ export declare class Metrics {
2
+ private chainHeight;
3
+ private lastBlock;
4
+ private mappingSpeed;
5
+ private mappingItemSpeed;
6
+ private blockProgress;
7
+ setChainHeight(height: number): void;
8
+ setLastProcessedBlock(height: number): void;
9
+ updateProgress(processed: number, left: number, time?: bigint): void;
10
+ registerBatch(batchSize: number, batchItemSize: number, batchMappingStartTime: bigint, batchMappingEndTime: bigint): void;
11
+ getChainHeight(): number;
12
+ getLastProcessedBlock(): number;
13
+ getSyncSpeed(): number;
14
+ getSyncEtaSeconds(): number;
15
+ getSyncRatio(): number;
16
+ getMappingSpeed(): number;
17
+ getMappingItemSpeed(): number;
18
+ getStatusLine(): string;
19
+ install(): void;
20
+ }
21
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAKA,qBAAa,OAAO;IAChB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,aAAa,CAAkD;IAEvE,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIpC,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK3C,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAKpE,aAAa,CACT,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,qBAAqB,EAAE,MAAM,EAC7B,mBAAmB,EAAE,MAAM,GAC5B,IAAI;IAKP,cAAc,IAAI,MAAM;IAIxB,qBAAqB,IAAI,MAAM;IAI/B,YAAY,IAAI,MAAM;IAItB,iBAAiB,IAAI,MAAM;IAI3B,YAAY,IAAI,MAAM;IAItB,eAAe,IAAI,MAAM;IAIzB,mBAAmB,IAAI,MAAM;IAI7B,aAAa,IAAI,MAAM;IAQvB,OAAO,IAAI,IAAI;CA+BlB"}
package/lib/metrics.js ADDED
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Metrics = void 0;
4
+ const util_internal_counters_1 = require("@subsquid/util-internal-counters");
5
+ const prom_client_1 = require("prom-client");
6
+ const util_1 = require("./util");
7
+ class Metrics {
8
+ constructor() {
9
+ this.chainHeight = -1;
10
+ this.lastBlock = -1;
11
+ this.mappingSpeed = new util_internal_counters_1.Speed({ windowSize: 5 });
12
+ this.mappingItemSpeed = new util_internal_counters_1.Speed({ windowSize: 5 });
13
+ this.blockProgress = new util_internal_counters_1.Progress({ initialValue: 0, windowSize: 20 });
14
+ }
15
+ setChainHeight(height) {
16
+ this.chainHeight = Math.max(height, this.lastBlock);
17
+ }
18
+ setLastProcessedBlock(height) {
19
+ this.lastBlock = height;
20
+ this.chainHeight = Math.max(this.chainHeight, this.lastBlock);
21
+ }
22
+ updateProgress(processed, left, time) {
23
+ this.blockProgress.setTargetValue(processed + left);
24
+ this.blockProgress.setCurrentValue(processed, time);
25
+ }
26
+ registerBatch(batchSize, batchItemSize, batchMappingStartTime, batchMappingEndTime) {
27
+ this.mappingSpeed.push(batchSize, batchMappingStartTime, batchMappingEndTime);
28
+ this.mappingItemSpeed.push(batchItemSize || 1, batchMappingStartTime, batchMappingEndTime);
29
+ }
30
+ getChainHeight() {
31
+ return this.chainHeight;
32
+ }
33
+ getLastProcessedBlock() {
34
+ return this.lastBlock;
35
+ }
36
+ getSyncSpeed() {
37
+ return this.blockProgress.speed();
38
+ }
39
+ getSyncEtaSeconds() {
40
+ return this.blockProgress.eta();
41
+ }
42
+ getSyncRatio() {
43
+ return this.blockProgress.ratio();
44
+ }
45
+ getMappingSpeed() {
46
+ return this.mappingSpeed.speed();
47
+ }
48
+ getMappingItemSpeed() {
49
+ return this.mappingItemSpeed.speed();
50
+ }
51
+ getStatusLine() {
52
+ return `${this.lastBlock} / ${this.chainHeight}, ` +
53
+ `rate: ${Math.round(this.getSyncSpeed())} blocks/sec, ` +
54
+ `mapping: ${Math.round(this.getMappingSpeed())} blocks/sec, ` +
55
+ `${Math.round(this.getMappingItemSpeed())} items/sec, ` +
56
+ `eta: ${(0, util_1.timeInterval)(this.getSyncEtaSeconds())}`;
57
+ }
58
+ install() {
59
+ new prom_client_1.Gauge({
60
+ name: 'sqd_processor_chain_height',
61
+ help: 'Chain height of the data source',
62
+ collect: collect(() => this.getChainHeight())
63
+ });
64
+ new prom_client_1.Gauge({
65
+ name: 'sqd_processor_last_block',
66
+ help: 'Last processed block',
67
+ collect: collect(() => this.getLastProcessedBlock())
68
+ });
69
+ new prom_client_1.Gauge({
70
+ name: 'sqd_processor_mapping_blocks_per_second',
71
+ help: 'Mapping performance',
72
+ collect: collect(() => this.getMappingSpeed())
73
+ });
74
+ new prom_client_1.Gauge({
75
+ name: 'sqd_processor_sync_eta_seconds',
76
+ help: 'Estimated time until all required blocks will be processed or until the chain height will be reached',
77
+ collect: collect(() => this.getSyncEtaSeconds())
78
+ });
79
+ new prom_client_1.Gauge({
80
+ name: 'sqd_processor_sync_ratio',
81
+ help: 'Percentage of processed blocks',
82
+ collect: collect(() => this.getSyncRatio())
83
+ });
84
+ }
85
+ }
86
+ exports.Metrics = Metrics;
87
+ function collect(fn) {
88
+ return function () {
89
+ this.set(fn());
90
+ };
91
+ }
92
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
package/lib/run.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { Database, HashAndHeight } from './database';
2
+ import { DataSource } from './datasource';
3
+ export interface DataHandlerContext<Block, Store> {
4
+ /**
5
+ * Storage interface provided by the database
6
+ */
7
+ store: Store;
8
+ /**
9
+ * List of blocks to map and process
10
+ */
11
+ blocks: Block[];
12
+ /**
13
+ * Signals, that the processor is near the head of the chain.
14
+ */
15
+ isHead: boolean;
16
+ }
17
+ interface BlockBase {
18
+ header: HashAndHeight;
19
+ }
20
+ /**
21
+ * Run data processing.
22
+ *
23
+ * This method assumes full control over the current OS process as
24
+ * it terminates the entire program in case of error or
25
+ * at the end of data processing.
26
+ *
27
+ * @param src - data source to ingest data from
28
+ *
29
+ * @param db - database is responsible for providing storage API to data handler
30
+ * and persisting mapping progress and status.
31
+ *
32
+ * @param dataHandler - The data handler, see {@link DataHandlerContext} for an API available to the handler.
33
+ */
34
+ export declare function run<Block extends BlockBase, Store>(src: DataSource<Block>, db: Database<Store>, dataHandler: (ctx: DataHandlerContext<Block, Store>) => Promise<void>): void;
35
+ export {};
36
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAAC,QAAQ,EAAE,aAAa,EAAC,MAAM,YAAY,CAAA;AAClD,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAA;AAQvC,MAAM,WAAW,kBAAkB,CAAC,KAAK,EAAE,KAAK;IAC5C;;OAEG;IACH,KAAK,EAAE,KAAK,CAAA;IACZ;;OAEG;IACH,MAAM,EAAE,KAAK,EAAE,CAAA;IACf;;OAEG;IACH,MAAM,EAAE,OAAO,CAAA;CAClB;AAGD,UAAU,SAAS;IACf,MAAM,EAAE,aAAa,CAAA;CACxB;AAGD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,GAAG,CAAC,KAAK,SAAS,SAAS,EAAE,KAAK,EAC9C,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EACtB,EAAE,EAAE,QAAQ,CAAC,KAAK,CAAC,EACnB,WAAW,EAAE,CAAC,GAAG,EAAE,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GACtE,IAAI,CAMN"}
package/lib/run.js ADDED
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.run = void 0;
27
+ const logger_1 = require("@subsquid/logger");
28
+ const util_internal_1 = require("@subsquid/util-internal");
29
+ const util_internal_prometheus_server_1 = require("@subsquid/util-internal-prometheus-server");
30
+ const prom = __importStar(require("prom-client"));
31
+ const metrics_1 = require("./metrics");
32
+ const util_1 = require("./util");
33
+ const log = (0, logger_1.createLogger)('sqd:batch-processor');
34
+ /**
35
+ * Run data processing.
36
+ *
37
+ * This method assumes full control over the current OS process as
38
+ * it terminates the entire program in case of error or
39
+ * at the end of data processing.
40
+ *
41
+ * @param src - data source to ingest data from
42
+ *
43
+ * @param db - database is responsible for providing storage API to data handler
44
+ * and persisting mapping progress and status.
45
+ *
46
+ * @param dataHandler - The data handler, see {@link DataHandlerContext} for an API available to the handler.
47
+ */
48
+ function run(src, db, dataHandler) {
49
+ (0, util_internal_1.runProgram)(() => {
50
+ return new Processor(src, db, dataHandler).run();
51
+ }, err => {
52
+ log.fatal(err);
53
+ });
54
+ }
55
+ exports.run = run;
56
+ class Processor {
57
+ constructor(src, db, handler) {
58
+ this.src = src;
59
+ this.db = db;
60
+ this.handler = handler;
61
+ this.metrics = new metrics_1.Metrics();
62
+ this.hasStatusNews = false;
63
+ this.chainHeight = new util_internal_1.Throttler(() => this.src.getFinalizedHeight(), 30000);
64
+ }
65
+ async run() {
66
+ let state = await this.db.connect();
67
+ if (state.height >= 0) {
68
+ log.info(`last processed final block was ${state.height}`);
69
+ }
70
+ await this.assertWeAreOnTheSameChain(state);
71
+ await this.initMetrics(state);
72
+ for await (let blocks of this.src.getBlockStream(state.height + 1)) {
73
+ if (blocks.length > 0) {
74
+ state = await this.processBatch(state, blocks);
75
+ }
76
+ }
77
+ this.reportFinalStatus();
78
+ }
79
+ async assertWeAreOnTheSameChain(state) {
80
+ if (state.height < 0)
81
+ return;
82
+ let hash = await this.src.getBlockHash(state.height);
83
+ if (state.hash === hash)
84
+ return;
85
+ throw new Error(`already indexed block ${(0, util_1.formatHead)(state)} was not found on chain`);
86
+ }
87
+ async initMetrics(state) {
88
+ await this.updateProgressMetrics(await this.chainHeight.get(), state);
89
+ let port = process.env.PROCESSOR_PROMETHEUS_PORT || process.env.PROMETHEUS_PORT;
90
+ if (port == null)
91
+ return;
92
+ prom.collectDefaultMetrics();
93
+ this.metrics.install();
94
+ let server = await (0, util_internal_prometheus_server_1.createPrometheusServer)(prom.register, port);
95
+ log.info(`prometheus metrics are served on port ${server.port}`);
96
+ }
97
+ updateProgressMetrics(chainHeight, state, time) {
98
+ this.metrics.setChainHeight(chainHeight);
99
+ this.metrics.setLastProcessedBlock(state.height);
100
+ let left;
101
+ let processed;
102
+ if (this.src.getBlocksCountInRange) {
103
+ left = this.src.getBlocksCountInRange({
104
+ from: this.metrics.getLastProcessedBlock() + 1,
105
+ to: this.metrics.getChainHeight()
106
+ });
107
+ processed = this.src.getBlocksCountInRange({
108
+ from: 0,
109
+ to: this.metrics.getChainHeight()
110
+ }) - left;
111
+ }
112
+ else {
113
+ left = this.metrics.getChainHeight() - this.metrics.getLastProcessedBlock();
114
+ processed = this.metrics.getLastProcessedBlock();
115
+ }
116
+ this.metrics.updateProgress(processed, left, time);
117
+ }
118
+ async processBatch(prevHead, blocks) {
119
+ let chainHeight = await this.chainHeight.get();
120
+ let nextHead = {
121
+ hash: (0, util_internal_1.last)(blocks).header.hash,
122
+ height: (0, util_internal_1.last)(blocks).header.height
123
+ };
124
+ let isOnTop = nextHead.height >= chainHeight;
125
+ let mappingStartTime = process.hrtime.bigint();
126
+ await this.db.transact({
127
+ prevHead,
128
+ nextHead,
129
+ isOnTop
130
+ }, store => {
131
+ return this.handler({
132
+ store,
133
+ blocks,
134
+ isHead: isOnTop
135
+ });
136
+ });
137
+ let mappingEndTime = process.hrtime.bigint();
138
+ this.updateProgressMetrics(chainHeight, nextHead, mappingEndTime);
139
+ this.metrics.registerBatch(blocks.length, (0, util_1.getItemsCount)(blocks), mappingStartTime, mappingEndTime);
140
+ this.reportStatus();
141
+ return nextHead;
142
+ }
143
+ reportStatus() {
144
+ if (this.statusReportTimer == null) {
145
+ log.info(this.metrics.getStatusLine());
146
+ this.statusReportTimer = setTimeout(() => {
147
+ this.statusReportTimer = undefined;
148
+ if (this.hasStatusNews) {
149
+ this.hasStatusNews = false;
150
+ this.reportStatus();
151
+ }
152
+ }, 5000);
153
+ }
154
+ else {
155
+ this.hasStatusNews = true;
156
+ }
157
+ }
158
+ reportFinalStatus() {
159
+ if (this.statusReportTimer != null) {
160
+ clearTimeout(this.statusReportTimer);
161
+ }
162
+ if (this.hasStatusNews) {
163
+ this.hasStatusNews = false;
164
+ log.info(this.metrics.getStatusLine());
165
+ }
166
+ }
167
+ }
168
+ //# sourceMappingURL=run.js.map
package/lib/run.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
package/lib/util.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { HashAndHeight } from './database';
2
+ export declare function timeInterval(seconds: number): string;
3
+ export declare function getItemsCount(blocks: any[]): number;
4
+ export declare function formatHead(head: HashAndHeight): string;
5
+ export declare function shortHash(hash: string): string;
6
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,YAAY,CAAA;AAGxC,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWpD;AAGD,wBAAgB,aAAa,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAWnD;AAGD,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAEtD;AAGD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM9C"}
package/lib/util.js ADDED
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shortHash = exports.formatHead = exports.getItemsCount = exports.timeInterval = void 0;
4
+ function timeInterval(seconds) {
5
+ if (seconds < 60) {
6
+ return Math.round(seconds) + 's';
7
+ }
8
+ let minutes = Math.ceil(seconds / 60);
9
+ if (minutes < 60) {
10
+ return minutes + 'm';
11
+ }
12
+ let hours = Math.floor(minutes / 60);
13
+ minutes = minutes - hours * 60;
14
+ return hours + 'h ' + minutes + 'm';
15
+ }
16
+ exports.timeInterval = timeInterval;
17
+ function getItemsCount(blocks) {
18
+ let count = 0;
19
+ for (let block of blocks) {
20
+ for (let key in block) {
21
+ let val = block[key];
22
+ if (Array.isArray(val)) {
23
+ count += val.length;
24
+ }
25
+ }
26
+ }
27
+ return count;
28
+ }
29
+ exports.getItemsCount = getItemsCount;
30
+ function formatHead(head) {
31
+ return `${head.height}#${shortHash(head.hash)}`;
32
+ }
33
+ exports.formatHead = formatHead;
34
+ function shortHash(hash) {
35
+ if (hash.startsWith('0x')) {
36
+ return hash.slice(2, 7);
37
+ }
38
+ else {
39
+ return hash.slice(0, 5);
40
+ }
41
+ }
42
+ exports.shortHash = shortHash;
43
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;AAGA,SAAgB,YAAY,CAAC,OAAe;IACxC,IAAI,OAAO,GAAG,EAAE,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,CAAA;IACpC,CAAC;IACD,IAAI,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,GAAC,EAAE,CAAC,CAAA;IACnC,IAAI,OAAO,GAAG,EAAE,EAAE,CAAC;QACf,OAAQ,OAAO,GAAC,GAAG,CAAA;IACvB,CAAC;IACD,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAA;IACpC,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,EAAE,CAAA;IAC9B,OAAO,KAAK,GAAG,IAAI,GAAG,OAAO,GAAG,GAAG,CAAA;AACvC,CAAC;AAXD,oCAWC;AAGD,SAAgB,aAAa,CAAC,MAAa;IACvC,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;QACvB,KAAK,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;YACpB,IAAI,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;YACpB,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrB,KAAK,IAAI,GAAG,CAAC,MAAM,CAAA;YACvB,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAA;AAChB,CAAC;AAXD,sCAWC;AAGD,SAAgB,UAAU,CAAC,IAAmB;IAC1C,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;AACnD,CAAC;AAFD,gCAEC;AAGD,SAAgB,SAAS,CAAC,IAAY;IAClC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3B,CAAC;SAAM,CAAC;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3B,CAAC;AACL,CAAC;AAND,8BAMC"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@subsquid/batch-processor",
3
+ "version": "0.0.0",
4
+ "description": "ETL processor",
5
+ "license": "GPL-3.0-or-later",
6
+ "repository": "git@github.com:subsquid/squid.git",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "files": [
12
+ "lib",
13
+ "src"
14
+ ],
15
+ "main": "lib/index.js",
16
+ "dependencies": {
17
+ "@subsquid/logger": "^1.3.3",
18
+ "@subsquid/util-internal": "^3.2.0",
19
+ "@subsquid/util-internal-counters": "^1.3.2",
20
+ "@subsquid/util-internal-prometheus-server": "^1.3.0",
21
+ "@subsquid/util-internal-range": "^0.3.0",
22
+ "prom-client": "^14.2.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^18.18.14",
26
+ "typescript": "~5.3.2"
27
+ },
28
+ "scripts": {
29
+ "build": "rm -rf lib && tsc"
30
+ }
31
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Database is responsible for providing a persistent storage for data handlers
3
+ * and keeping the processor progress and status.
4
+ */
5
+ export type Database<S> = FinalDatabase<S> | HotDatabase<S>
6
+
7
+
8
+ export interface FinalTxInfo {
9
+ prevHead: HashAndHeight
10
+ nextHead: HashAndHeight
11
+ isOnTop: boolean
12
+ }
13
+
14
+
15
+ export interface FinalDatabase<S> {
16
+ supportsHotBlocks?: false
17
+ connect(): Promise<HashAndHeight>
18
+ transact(info: FinalTxInfo, cb: (store: S) => Promise<void>): Promise<void>
19
+ }
20
+
21
+
22
+ export interface HotTxInfo {
23
+ finalizedHead: HashAndHeight
24
+ baseHead: HashAndHeight
25
+ newBlocks: HashAndHeight[]
26
+ }
27
+
28
+
29
+ export interface HotDatabase<S> {
30
+ supportsHotBlocks: true
31
+ connect(): Promise<HotDatabaseState>
32
+ transact(info: FinalTxInfo, cb: (store: S) => Promise<void>): Promise<void>
33
+ /**
34
+ * @deprecated
35
+ */
36
+ transactHot(info: HotTxInfo, cb: (store: S, block: HashAndHeight) => Promise<void>): Promise<void>
37
+
38
+ transactHot2?(
39
+ info: HotTxInfo,
40
+ cb: (store: S, blockSliceStart: number, blockSliceEnd: number) => Promise<void>
41
+ ): Promise<void>
42
+ }
43
+
44
+
45
+ export interface HotDatabaseState extends HashAndHeight {
46
+ top: HashAndHeight[]
47
+ }
48
+
49
+
50
+ export interface HashAndHeight {
51
+ height: number
52
+ hash: string
53
+ }
@@ -0,0 +1,9 @@
1
+ import type {FiniteRange} from '@subsquid/util-internal-range'
2
+
3
+
4
+ export interface DataSource<B> {
5
+ getFinalizedHeight(): Promise<number>
6
+ getBlockHash(height: number): Promise<string | undefined>
7
+ getBlockStream(fromBlock?: number): AsyncIterable<B[]>
8
+ getBlocksCountInRange?(range: FiniteRange): number
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './database'
2
+ export * from './datasource'
3
+ export * from './run'
package/src/metrics.ts ADDED
@@ -0,0 +1,111 @@
1
+ import {Progress, Speed} from '@subsquid/util-internal-counters'
2
+ import {Gauge} from 'prom-client'
3
+ import {timeInterval} from './util'
4
+
5
+
6
+ export class Metrics {
7
+ private chainHeight = -1
8
+ private lastBlock = -1
9
+ private mappingSpeed = new Speed({windowSize: 5})
10
+ private mappingItemSpeed = new Speed({windowSize: 5})
11
+ private blockProgress = new Progress({initialValue: 0, windowSize: 20})
12
+
13
+ setChainHeight(height: number): void {
14
+ this.chainHeight = Math.max(height, this.lastBlock)
15
+ }
16
+
17
+ setLastProcessedBlock(height: number): void {
18
+ this.lastBlock = height
19
+ this.chainHeight = Math.max(this.chainHeight, this.lastBlock)
20
+ }
21
+
22
+ updateProgress(processed: number, left: number, time?: bigint): void {
23
+ this.blockProgress.setTargetValue(processed + left)
24
+ this.blockProgress.setCurrentValue(processed, time)
25
+ }
26
+
27
+ registerBatch(
28
+ batchSize: number,
29
+ batchItemSize: number,
30
+ batchMappingStartTime: bigint,
31
+ batchMappingEndTime: bigint,
32
+ ): void {
33
+ this.mappingSpeed.push(batchSize, batchMappingStartTime, batchMappingEndTime)
34
+ this.mappingItemSpeed.push(batchItemSize || 1, batchMappingStartTime, batchMappingEndTime)
35
+ }
36
+
37
+ getChainHeight(): number {
38
+ return this.chainHeight
39
+ }
40
+
41
+ getLastProcessedBlock(): number {
42
+ return this.lastBlock
43
+ }
44
+
45
+ getSyncSpeed(): number {
46
+ return this.blockProgress.speed()
47
+ }
48
+
49
+ getSyncEtaSeconds(): number {
50
+ return this.blockProgress.eta()
51
+ }
52
+
53
+ getSyncRatio(): number {
54
+ return this.blockProgress.ratio()
55
+ }
56
+
57
+ getMappingSpeed(): number {
58
+ return this.mappingSpeed.speed()
59
+ }
60
+
61
+ getMappingItemSpeed(): number {
62
+ return this.mappingItemSpeed.speed()
63
+ }
64
+
65
+ getStatusLine(): string {
66
+ return `${this.lastBlock} / ${this.chainHeight}, ` +
67
+ `rate: ${Math.round(this.getSyncSpeed())} blocks/sec, ` +
68
+ `mapping: ${Math.round(this.getMappingSpeed())} blocks/sec, ` +
69
+ `${Math.round(this.getMappingItemSpeed())} items/sec, ` +
70
+ `eta: ${timeInterval(this.getSyncEtaSeconds())}`
71
+ }
72
+
73
+ install(): void {
74
+ new Gauge({
75
+ name: 'sqd_processor_chain_height',
76
+ help: 'Chain height of the data source',
77
+ collect: collect(() => this.getChainHeight())
78
+ })
79
+
80
+ new Gauge({
81
+ name: 'sqd_processor_last_block',
82
+ help: 'Last processed block',
83
+ collect: collect(() => this.getLastProcessedBlock())
84
+ })
85
+
86
+ new Gauge({
87
+ name: 'sqd_processor_mapping_blocks_per_second',
88
+ help: 'Mapping performance',
89
+ collect: collect(() => this.getMappingSpeed())
90
+ })
91
+
92
+ new Gauge({
93
+ name: 'sqd_processor_sync_eta_seconds',
94
+ help: 'Estimated time until all required blocks will be processed or until the chain height will be reached',
95
+ collect: collect(() => this.getSyncEtaSeconds())
96
+ })
97
+
98
+ new Gauge({
99
+ name: 'sqd_processor_sync_ratio',
100
+ help: 'Percentage of processed blocks',
101
+ collect: collect(() => this.getSyncRatio())
102
+ })
103
+ }
104
+ }
105
+
106
+
107
+ function collect(fn: () => number) {
108
+ return function(this: Gauge<string>) {
109
+ this.set(fn())
110
+ }
111
+ }
package/src/run.ts ADDED
@@ -0,0 +1,197 @@
1
+ import {createLogger} from '@subsquid/logger'
2
+ import {last, runProgram, Throttler} from '@subsquid/util-internal'
3
+ import {createPrometheusServer} from '@subsquid/util-internal-prometheus-server'
4
+ import * as prom from 'prom-client'
5
+ import {Database, HashAndHeight} from './database'
6
+ import {DataSource} from './datasource'
7
+ import {Metrics} from './metrics'
8
+ import {formatHead, getItemsCount} from './util'
9
+
10
+
11
+ const log = createLogger('sqd:batch-processor')
12
+
13
+
14
+ export interface DataHandlerContext<Block, Store> {
15
+ /**
16
+ * Storage interface provided by the database
17
+ */
18
+ store: Store
19
+ /**
20
+ * List of blocks to map and process
21
+ */
22
+ blocks: Block[]
23
+ /**
24
+ * Signals, that the processor is near the head of the chain.
25
+ */
26
+ isHead: boolean
27
+ }
28
+
29
+
30
+ interface BlockBase {
31
+ header: HashAndHeight
32
+ }
33
+
34
+
35
+ /**
36
+ * Run data processing.
37
+ *
38
+ * This method assumes full control over the current OS process as
39
+ * it terminates the entire program in case of error or
40
+ * at the end of data processing.
41
+ *
42
+ * @param src - data source to ingest data from
43
+ *
44
+ * @param db - database is responsible for providing storage API to data handler
45
+ * and persisting mapping progress and status.
46
+ *
47
+ * @param dataHandler - The data handler, see {@link DataHandlerContext} for an API available to the handler.
48
+ */
49
+ export function run<Block extends BlockBase, Store>(
50
+ src: DataSource<Block>,
51
+ db: Database<Store>,
52
+ dataHandler: (ctx: DataHandlerContext<Block, Store>) => Promise<void>
53
+ ): void {
54
+ runProgram(() => {
55
+ return new Processor(src, db, dataHandler).run()
56
+ }, err => {
57
+ log.fatal(err)
58
+ })
59
+ }
60
+
61
+
62
+ class Processor<B extends BlockBase, S> {
63
+ private metrics = new Metrics()
64
+ private chainHeight: Throttler<number>
65
+ private statusReportTimer?: any
66
+ private hasStatusNews = false
67
+
68
+ constructor(
69
+ private src: DataSource<B>,
70
+ private db: Database<S>,
71
+ private handler: (ctx: DataHandlerContext<B, S>) => Promise<void>
72
+ ) {
73
+ this.chainHeight = new Throttler(() => this.src.getFinalizedHeight(), 30_000)
74
+ }
75
+
76
+ async run(): Promise<void> {
77
+ let state = await this.db.connect()
78
+ if (state.height >= 0) {
79
+ log.info(`last processed final block was ${state.height}`)
80
+ }
81
+
82
+ await this.assertWeAreOnTheSameChain(state)
83
+ await this.initMetrics(state)
84
+
85
+ for await (let blocks of this.src.getBlockStream(state.height + 1)) {
86
+ if (blocks.length > 0) {
87
+ state = await this.processBatch(state, blocks)
88
+ }
89
+ }
90
+
91
+ this.reportFinalStatus()
92
+ }
93
+
94
+ private async assertWeAreOnTheSameChain(state: HashAndHeight): Promise<void> {
95
+ if (state.height < 0) return
96
+ let hash = await this.src.getBlockHash(state.height)
97
+ if (state.hash === hash) return
98
+ throw new Error(
99
+ `already indexed block ${formatHead(state)} was not found on chain`
100
+ )
101
+ }
102
+
103
+ private async initMetrics(state: HashAndHeight): Promise<void> {
104
+ await this.updateProgressMetrics(await this.chainHeight.get(), state)
105
+ let port = process.env.PROCESSOR_PROMETHEUS_PORT || process.env.PROMETHEUS_PORT
106
+ if (port == null) return
107
+ prom.collectDefaultMetrics()
108
+ this.metrics.install()
109
+ let server = await createPrometheusServer(prom.register, port)
110
+ log.info(`prometheus metrics are served on port ${server.port}`)
111
+ }
112
+
113
+ private updateProgressMetrics(chainHeight: number, state: HashAndHeight, time?: bigint): void {
114
+ this.metrics.setChainHeight(chainHeight)
115
+ this.metrics.setLastProcessedBlock(state.height)
116
+ let left: number
117
+ let processed: number
118
+ if (this.src.getBlocksCountInRange) {
119
+ left = this.src.getBlocksCountInRange({
120
+ from: this.metrics.getLastProcessedBlock() + 1,
121
+ to: this.metrics.getChainHeight()
122
+ })
123
+ processed = this.src.getBlocksCountInRange({
124
+ from: 0,
125
+ to: this.metrics.getChainHeight()
126
+ }) - left
127
+ } else {
128
+ left = this.metrics.getChainHeight() - this.metrics.getLastProcessedBlock()
129
+ processed = this.metrics.getLastProcessedBlock()
130
+ }
131
+ this.metrics.updateProgress(processed, left, time)
132
+ }
133
+
134
+ private async processBatch(prevHead: HashAndHeight, blocks: B[]): Promise<HashAndHeight> {
135
+ let chainHeight = await this.chainHeight.get()
136
+
137
+ let nextHead = {
138
+ hash: last(blocks).header.hash,
139
+ height: last(blocks).header.height
140
+ }
141
+
142
+ let isOnTop = nextHead.height >= chainHeight
143
+
144
+ let mappingStartTime = process.hrtime.bigint()
145
+
146
+ await this.db.transact({
147
+ prevHead,
148
+ nextHead,
149
+ isOnTop
150
+ }, store => {
151
+ return this.handler({
152
+ store,
153
+ blocks,
154
+ isHead: isOnTop
155
+ })
156
+ })
157
+
158
+ let mappingEndTime = process.hrtime.bigint()
159
+
160
+ this.updateProgressMetrics(chainHeight, nextHead, mappingEndTime)
161
+ this.metrics.registerBatch(
162
+ blocks.length,
163
+ getItemsCount(blocks),
164
+ mappingStartTime,
165
+ mappingEndTime
166
+ )
167
+
168
+ this.reportStatus()
169
+
170
+ return nextHead
171
+ }
172
+
173
+ private reportStatus(): void {
174
+ if (this.statusReportTimer == null) {
175
+ log.info(this.metrics.getStatusLine())
176
+ this.statusReportTimer = setTimeout(() => {
177
+ this.statusReportTimer = undefined
178
+ if (this.hasStatusNews) {
179
+ this.hasStatusNews = false
180
+ this.reportStatus()
181
+ }
182
+ }, 5000)
183
+ } else {
184
+ this.hasStatusNews = true
185
+ }
186
+ }
187
+
188
+ private reportFinalStatus(): void {
189
+ if (this.statusReportTimer != null) {
190
+ clearTimeout(this.statusReportTimer)
191
+ }
192
+ if (this.hasStatusNews) {
193
+ this.hasStatusNews = false
194
+ log.info(this.metrics.getStatusLine())
195
+ }
196
+ }
197
+ }
package/src/util.ts ADDED
@@ -0,0 +1,43 @@
1
+ import {HashAndHeight} from './database'
2
+
3
+
4
+ export function timeInterval(seconds: number): string {
5
+ if (seconds < 60) {
6
+ return Math.round(seconds) + 's'
7
+ }
8
+ let minutes = Math.ceil(seconds/60)
9
+ if (minutes < 60) {
10
+ return minutes+'m'
11
+ }
12
+ let hours = Math.floor(minutes / 60)
13
+ minutes = minutes - hours * 60
14
+ return hours + 'h ' + minutes + 'm'
15
+ }
16
+
17
+
18
+ export function getItemsCount(blocks: any[]): number {
19
+ let count = 0
20
+ for (let block of blocks) {
21
+ for (let key in block) {
22
+ let val = block[key]
23
+ if (Array.isArray(val)) {
24
+ count += val.length
25
+ }
26
+ }
27
+ }
28
+ return count
29
+ }
30
+
31
+
32
+ export function formatHead(head: HashAndHeight): string {
33
+ return `${head.height}#${shortHash(head.hash)}`
34
+ }
35
+
36
+
37
+ export function shortHash(hash: string): string {
38
+ if (hash.startsWith('0x')) {
39
+ return hash.slice(2, 7)
40
+ } else {
41
+ return hash.slice(0, 5)
42
+ }
43
+ }