@subsquid/batch-processor 0.2.0-portal-api.493495 → 0.2.0-portal-api.19316d
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/lib/database.d.ts +27 -6
- package/lib/database.d.ts.map +1 -1
- package/lib/run.d.ts +16 -2
- package/lib/run.d.ts.map +1 -1
- package/lib/run.js +134 -97
- package/lib/run.js.map +1 -1
- package/lib/template-registry.d.ts +37 -0
- package/lib/template-registry.d.ts.map +1 -0
- package/lib/template-registry.js +186 -0
- package/lib/template-registry.js.map +1 -0
- package/package.json +3 -2
- package/src/database.ts +36 -6
- package/src/run.ts +195 -96
- package/src/template-registry.test.ts +651 -0
- package/src/template-registry.ts +199 -0
package/src/run.ts
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import {createLogger} from '@subsquid/logger'
|
|
2
2
|
import {last, maybeLast, runProgram, Throttler} from '@subsquid/util-internal'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
Database,
|
|
5
|
+
DatabaseTransactResult,
|
|
6
|
+
FinalDatabaseState,
|
|
7
|
+
HashAndHeight,
|
|
8
|
+
HotDatabaseState,
|
|
9
|
+
TemplateMutation,
|
|
10
|
+
} from './database'
|
|
11
|
+
import {
|
|
12
|
+
DataSource,
|
|
13
|
+
isForkException,
|
|
14
|
+
BlockRef,
|
|
15
|
+
type BlockBatch,
|
|
16
|
+
type TemplateRegistry as ITemplateRegistry,
|
|
17
|
+
type TemplateValue,
|
|
18
|
+
} from '@subsquid/util-internal-data-source'
|
|
19
|
+
import {type TemplateManager, TemplateRegistry} from './template-registry'
|
|
20
|
+
import type {FiniteRange} from '@subsquid/util-internal-range'
|
|
5
21
|
import assert from 'assert'
|
|
6
22
|
import {PrometheusServer, RunnerMetrics} from '@subsquid/util-internal-processor-tools'
|
|
7
23
|
import {formatHead, getItemsCount} from './util'
|
|
8
24
|
|
|
9
|
-
|
|
10
25
|
export {PrometheusServer}
|
|
11
|
-
|
|
26
|
+
export type {TemplateManager} from './template-registry'
|
|
12
27
|
|
|
13
28
|
const log = createLogger('sqd:batch-processor')
|
|
14
29
|
|
|
@@ -25,6 +40,10 @@ export interface DataHandlerContext<Block, Store> {
|
|
|
25
40
|
* Signals, that the processor is near the head of the chain.
|
|
26
41
|
*/
|
|
27
42
|
isHead: boolean
|
|
43
|
+
/**
|
|
44
|
+
* Templates manager to add and remove templates
|
|
45
|
+
*/
|
|
46
|
+
templates: TemplateManager
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
|
|
@@ -33,13 +52,7 @@ export interface BlockBase {
|
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
|
|
36
|
-
interface
|
|
37
|
-
finalizedHead: BlockRef | undefined
|
|
38
|
-
unfinalizedHeads: BlockRef[]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
interface RunOptions {
|
|
55
|
+
export interface RunOptions {
|
|
43
56
|
prometheus?: PrometheusServer
|
|
44
57
|
}
|
|
45
58
|
|
|
@@ -58,6 +71,18 @@ interface RunOptions {
|
|
|
58
71
|
*
|
|
59
72
|
* @param dataHandler - The data handler, see {@link DataHandlerContext} for an API available to the handler.
|
|
60
73
|
*/
|
|
74
|
+
export function run<Block extends BlockBase, Store>(
|
|
75
|
+
src: DataSource<Block>,
|
|
76
|
+
db: Database<Store> & {supportsTemplates: true},
|
|
77
|
+
dataHandler: (ctx: DataHandlerContext<Block, Store>) => Promise<void>,
|
|
78
|
+
opts?: RunOptions
|
|
79
|
+
): void
|
|
80
|
+
export function run<Block extends BlockBase, Store>(
|
|
81
|
+
src: DataSource<Block>,
|
|
82
|
+
db: Database<Store>,
|
|
83
|
+
dataHandler: (ctx: Omit<DataHandlerContext<Block, Store>, 'templates'>) => Promise<void>,
|
|
84
|
+
opts?: RunOptions
|
|
85
|
+
): void
|
|
61
86
|
export function run<Block extends BlockBase, Store>(
|
|
62
87
|
src: DataSource<Block>,
|
|
63
88
|
db: Database<Store>,
|
|
@@ -71,10 +96,72 @@ export function run<Block extends BlockBase, Store>(
|
|
|
71
96
|
})
|
|
72
97
|
}
|
|
73
98
|
|
|
99
|
+
interface ProcessorStateInit {
|
|
100
|
+
finalizedHead?: BlockRef
|
|
101
|
+
unfinalizedHeads?: (BlockRef & {templates?: TemplateMutation[]})[]
|
|
102
|
+
templates?: TemplateMutation[]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ProcessorState implements ITemplateRegistry {
|
|
107
|
+
finalizedHead: BlockRef | undefined = undefined
|
|
108
|
+
unfinalizedHeads: BlockRef[] = []
|
|
109
|
+
private templates = new TemplateRegistry()
|
|
110
|
+
|
|
111
|
+
get head(): BlockRef | undefined {
|
|
112
|
+
return maybeLast(this.unfinalizedHeads) ?? this.finalizedHead
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get(key: string): TemplateValue[] {
|
|
116
|
+
return this.templates.get(key)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
init(state: ProcessorStateInit): void {
|
|
120
|
+
this.finalizedHead = state.finalizedHead
|
|
121
|
+
this.unfinalizedHeads = state.unfinalizedHeads ?? []
|
|
122
|
+
|
|
123
|
+
let hotBlockTemplates = state.unfinalizedHeads
|
|
124
|
+
?.filter((b) => b.templates?.length)
|
|
125
|
+
.map((b) => ({blockNumber: b.number, templates: b.templates!}))
|
|
126
|
+
|
|
127
|
+
if (state.templates?.length || hotBlockTemplates?.length) {
|
|
128
|
+
log.info('loading persisted templates')
|
|
129
|
+
this.templates.init(state.templates ?? [], hotBlockTemplates)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
handleFork(previousBlocks: BlockRef[]): void {
|
|
134
|
+
let chain = this.finalizedHead ? [this.finalizedHead, ...this.unfinalizedHeads] : this.unfinalizedHeads
|
|
135
|
+
let rollbackIndex = findRollbackIndex(chain, previousBlocks)
|
|
136
|
+
if (rollbackIndex === -1) {
|
|
137
|
+
if (this.finalizedHead != null) throw new Error('Unable to process fork')
|
|
138
|
+
this.unfinalizedHeads = []
|
|
139
|
+
this.templates.rollbackTo(-1)
|
|
140
|
+
} else {
|
|
141
|
+
let rollbackHead = chain[rollbackIndex]
|
|
142
|
+
log.info(`navigating a fork on a common base ${formatHead(rollbackHead)}`)
|
|
143
|
+
this.unfinalizedHeads = chain.slice(1, rollbackIndex + 1)
|
|
144
|
+
this.templates.rollbackTo(rollbackHead.number)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
prune(blockNumber: number): void {
|
|
149
|
+
this.templates.prune(blockNumber)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async transact(
|
|
153
|
+
range: FiniteRange,
|
|
154
|
+
fn: (templates: TemplateManager) => Promise<void>,
|
|
155
|
+
): Promise<{data: TemplateMutation[]; changed: boolean}> {
|
|
156
|
+
return this.templates.transact(range, fn)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
74
160
|
class Processor<B extends BlockBase, S> {
|
|
75
161
|
private metrics: RunnerMetrics
|
|
76
162
|
private statusReportTimer?: any
|
|
77
163
|
private hasStatusNews = false
|
|
164
|
+
private state = new ProcessorState()
|
|
78
165
|
|
|
79
166
|
constructor(
|
|
80
167
|
private src: DataSource<B>,
|
|
@@ -88,56 +175,60 @@ class Processor<B extends BlockBase, S> {
|
|
|
88
175
|
}
|
|
89
176
|
|
|
90
177
|
async run(): Promise<void> {
|
|
91
|
-
let getHead = this.db.supportsHotBlocks
|
|
178
|
+
let getHead = this.db.supportsHotBlocks
|
|
179
|
+
? this.src.getHead.bind(this.src)
|
|
180
|
+
: this.src.getFinalizedHead.bind(this.src)
|
|
92
181
|
let chainHeight = new Throttler(() => getHead()?.then((r) => r?.number ?? -1), 10_000)
|
|
93
182
|
|
|
94
|
-
let
|
|
95
|
-
|
|
96
|
-
unfinalizedHeads: [],
|
|
97
|
-
}
|
|
98
|
-
if (this.db.supportsHotBlocks) {
|
|
99
|
-
let dbState = await this.db.connect()
|
|
100
|
-
state.finalizedHead = dbState.height < 0 ? undefined : toBlockRef(dbState)
|
|
101
|
-
state.unfinalizedHeads = dbState.top.map(toBlockRef)
|
|
102
|
-
} else {
|
|
103
|
-
let dbState = await this.db.connect()
|
|
104
|
-
state.finalizedHead = dbState.height < 0 ? undefined : toBlockRef(dbState)
|
|
105
|
-
}
|
|
183
|
+
let dbState = await this.db.connect()
|
|
184
|
+
this.state.init(toProcessorStateInit(dbState))
|
|
106
185
|
|
|
107
|
-
let head =
|
|
186
|
+
let head = this.state.head
|
|
108
187
|
if (head != null) {
|
|
109
188
|
log.info(`last processed block was ${head.number}`)
|
|
110
189
|
}
|
|
111
190
|
await this.initMetrics(head?.number ?? -1, await chainHeight.get())
|
|
112
191
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
: this.src.getFinalizedStream.bind(this.src)
|
|
192
|
+
let getStream = this.db.supportsHotBlocks
|
|
193
|
+
? this.src.getStream.bind(this.src)
|
|
194
|
+
: this.src.getFinalizedStream.bind(this.src)
|
|
117
195
|
|
|
118
|
-
|
|
196
|
+
while (true) {
|
|
119
197
|
try {
|
|
120
|
-
for await (let data of getStream({
|
|
121
|
-
|
|
198
|
+
for await (let data of getStream({
|
|
199
|
+
from: (this.state.head?.number ?? -1) + 1,
|
|
200
|
+
parentHash: this.state.head?.hash,
|
|
201
|
+
templateRegistry: this.state,
|
|
202
|
+
})) {
|
|
203
|
+
await this.processBatch(
|
|
204
|
+
data,
|
|
205
|
+
await chainHeight.get(),
|
|
206
|
+
async (store: S, sliceBlocks: B[], isOnTop: boolean) => {
|
|
207
|
+
let batchRange = {from: sliceBlocks[0].header.number, to: last(sliceBlocks).header.number}
|
|
208
|
+
|
|
209
|
+
const {data: newTemplates, changed} = await this.state.transact(
|
|
210
|
+
batchRange,
|
|
211
|
+
async (templates) => {
|
|
212
|
+
await this.handler({store, blocks: sliceBlocks, isHead: isOnTop, templates})
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if (changed) {
|
|
217
|
+
throw new TemplateRegistryChanged()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {templates: newTemplates}
|
|
221
|
+
})
|
|
122
222
|
}
|
|
123
|
-
break
|
|
223
|
+
break
|
|
124
224
|
} catch (e) {
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
let chain = state.finalizedHead
|
|
129
|
-
? [state.finalizedHead, ...state.unfinalizedHeads]
|
|
130
|
-
: state.unfinalizedHeads
|
|
131
|
-
let rollbackIndex = findRollbackIndex(chain, e.previousBlocks)
|
|
132
|
-
if (rollbackIndex === -1) {
|
|
133
|
-
if (state.finalizedHead != null) throw new Error('Unable to process fork')
|
|
134
|
-
state.unfinalizedHeads = []
|
|
135
|
-
} else {
|
|
136
|
-
const rollbackHead = chain[rollbackIndex]
|
|
137
|
-
log.info(`navigating a fork on a common base ${formatHead(rollbackHead)}`)
|
|
138
|
-
|
|
139
|
-
state.unfinalizedHeads = chain.slice(1, rollbackIndex + 1)
|
|
225
|
+
if (isTemplateRegistryChanged(e)) {
|
|
226
|
+
log.info('template registry updated, re-fetching stream with new filters')
|
|
227
|
+
continue
|
|
140
228
|
}
|
|
229
|
+
|
|
230
|
+
if (!isForkException(e) || !this.db.supportsHotBlocks) throw e
|
|
231
|
+
this.state.handleFork(e.previousBlocks)
|
|
141
232
|
}
|
|
142
233
|
}
|
|
143
234
|
|
|
@@ -169,29 +260,30 @@ class Processor<B extends BlockBase, S> {
|
|
|
169
260
|
}
|
|
170
261
|
|
|
171
262
|
private async processBatch(
|
|
172
|
-
state: ProcessorState,
|
|
173
263
|
data: BlockBatch<B>,
|
|
174
|
-
chainHeight: number
|
|
175
|
-
|
|
264
|
+
chainHeight: number,
|
|
265
|
+
map: (store: S, blocks: B[], isOnTop: boolean) => Promise<DatabaseTransactResult | void>
|
|
266
|
+
): Promise<void> {
|
|
176
267
|
let {blocks, finalizedHead: finalizedHeadData} = data
|
|
177
268
|
|
|
178
|
-
if (blocks.length === 0) return
|
|
269
|
+
if (blocks.length === 0) return
|
|
179
270
|
|
|
180
|
-
let prevHead =
|
|
271
|
+
let prevHead = this.state.head
|
|
181
272
|
|
|
182
|
-
// Validate data continuity
|
|
183
273
|
if (prevHead && prevHead.number >= blocks[0].header.number) {
|
|
184
274
|
throw new Error('Data is not continuous')
|
|
185
275
|
}
|
|
186
276
|
|
|
187
|
-
if (
|
|
188
|
-
finalizedHeadData
|
|
277
|
+
if (
|
|
278
|
+
finalizedHeadData != null &&
|
|
279
|
+
this.state.finalizedHead != null &&
|
|
280
|
+
finalizedHeadData.number <= this.state.finalizedHead.number
|
|
281
|
+
) {
|
|
282
|
+
finalizedHeadData = this.state.finalizedHead
|
|
189
283
|
}
|
|
190
284
|
|
|
191
|
-
let unfinalizedIndex =
|
|
192
|
-
|
|
193
|
-
unfinalizedIndex = blocks.findIndex((b) => b.header.number > finalizedHeadData!.number)
|
|
194
|
-
}
|
|
285
|
+
let unfinalizedIndex =
|
|
286
|
+
finalizedHeadData == null ? 0 : blocks.findIndex((b) => b.header.number > finalizedHeadData!.number)
|
|
195
287
|
|
|
196
288
|
let nextHead = last(blocks).header
|
|
197
289
|
let isOnTop = nextHead.number >= chainHeight
|
|
@@ -201,61 +293,56 @@ class Processor<B extends BlockBase, S> {
|
|
|
201
293
|
if (this.db.supportsHotBlocks) {
|
|
202
294
|
let finalizedRef: BlockRef | undefined
|
|
203
295
|
if (unfinalizedIndex < 0) {
|
|
204
|
-
finalizedRef = blocks
|
|
296
|
+
finalizedRef = last(blocks).header
|
|
205
297
|
} else {
|
|
206
|
-
finalizedRef = blocks[unfinalizedIndex - 1]?.header
|
|
207
|
-
if (finalizedHeadData != null) {
|
|
208
|
-
finalizedRef = {hash: finalizedHeadData.hash, number: finalizedHeadData.number}
|
|
209
|
-
}
|
|
298
|
+
finalizedRef = finalizedHeadData ?? blocks[unfinalizedIndex - 1]?.header
|
|
210
299
|
}
|
|
211
|
-
let finalizedHead = maxBlockRef(finalizedRef, state.finalizedHead)
|
|
300
|
+
let finalizedHead = maxBlockRef(finalizedRef, this.state.finalizedHead)
|
|
212
301
|
await this.db.transactHot2(
|
|
213
302
|
{
|
|
214
303
|
finalizedHead: toHashAndHeight(finalizedHead),
|
|
215
304
|
baseHead: toHashAndHeight(prevHead),
|
|
216
305
|
newBlocks: blocks.map((b) => toHashAndHeight(b.header)),
|
|
217
306
|
},
|
|
218
|
-
(store, start, end) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
isHead: isOnTop,
|
|
223
|
-
})
|
|
307
|
+
async (store, start, end) => {
|
|
308
|
+
let sliceBlocks = start === 0 && end === blocks.length ? blocks : blocks.slice(start, end)
|
|
309
|
+
if (sliceBlocks.length === 0) return
|
|
310
|
+
return map(store, sliceBlocks, isOnTop)
|
|
224
311
|
}
|
|
225
312
|
)
|
|
226
313
|
|
|
227
|
-
|
|
228
|
-
|
|
314
|
+
let newFinalizedHead = finalizedHead ?? this.state.finalizedHead
|
|
315
|
+
if (newFinalizedHead) {
|
|
316
|
+
this.state.prune(newFinalizedHead.number)
|
|
229
317
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
318
|
+
let unfinalizedHeads = this.state.unfinalizedHeads
|
|
319
|
+
if (newFinalizedHead) {
|
|
320
|
+
let idx = unfinalizedHeads.findIndex((h) => h.number > newFinalizedHead!.number)
|
|
321
|
+
unfinalizedHeads = idx < 0 ? [] : unfinalizedHeads.slice(idx)
|
|
233
322
|
}
|
|
234
323
|
if (unfinalizedIndex >= 0) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
324
|
+
unfinalizedHeads = [
|
|
325
|
+
...unfinalizedHeads,
|
|
326
|
+
...blocks.slice(unfinalizedIndex).map((b) => ({number: b.header.number, hash: b.header.hash})),
|
|
327
|
+
]
|
|
238
328
|
}
|
|
329
|
+
this.state.finalizedHead = newFinalizedHead
|
|
330
|
+
this.state.unfinalizedHeads = unfinalizedHeads
|
|
239
331
|
} else {
|
|
240
332
|
assert(unfinalizedIndex < 0, 'non-hot database received unfinalized blocks')
|
|
241
333
|
|
|
242
334
|
await this.db.transact(
|
|
243
335
|
{
|
|
244
|
-
prevHead:
|
|
336
|
+
prevHead: toHashAndHeight(prevHead),
|
|
245
337
|
nextHead: toHashAndHeight(nextHead),
|
|
246
338
|
isOnTop,
|
|
247
339
|
},
|
|
248
|
-
(store) =>
|
|
249
|
-
return this.handler({
|
|
250
|
-
store,
|
|
251
|
-
blocks,
|
|
252
|
-
isHead: isOnTop,
|
|
253
|
-
})
|
|
254
|
-
}
|
|
340
|
+
(store) => map(store, blocks, isOnTop)
|
|
255
341
|
)
|
|
256
342
|
|
|
257
|
-
state.
|
|
258
|
-
state.
|
|
343
|
+
this.state.prune(nextHead.number)
|
|
344
|
+
this.state.finalizedHead = nextHead
|
|
345
|
+
this.state.unfinalizedHeads = []
|
|
259
346
|
}
|
|
260
347
|
|
|
261
348
|
let mappingEndTime = process.hrtime.bigint()
|
|
@@ -264,8 +351,6 @@ class Processor<B extends BlockBase, S> {
|
|
|
264
351
|
this.metrics.registerBatch(blocks.length, getItemsCount(blocks), mappingStartTime, mappingEndTime)
|
|
265
352
|
|
|
266
353
|
this.reportStatus()
|
|
267
|
-
|
|
268
|
-
return state
|
|
269
354
|
}
|
|
270
355
|
|
|
271
356
|
private reportStatus(): void {
|
|
@@ -330,12 +415,17 @@ function toHashAndHeight(ref: BlockRef | undefined): HashAndHeight {
|
|
|
330
415
|
return {height: ref.number, hash: ref.hash}
|
|
331
416
|
}
|
|
332
417
|
|
|
333
|
-
function toBlockRef(
|
|
334
|
-
return {number:
|
|
418
|
+
function toBlockRef(hh: HashAndHeight): BlockRef {
|
|
419
|
+
return {number: hh.height, hash: hh.hash}
|
|
335
420
|
}
|
|
336
421
|
|
|
337
|
-
function
|
|
338
|
-
|
|
422
|
+
function toProcessorStateInit(dbState: FinalDatabaseState | HotDatabaseState): ProcessorStateInit {
|
|
423
|
+
let top = 'top' in dbState ? dbState.top : undefined
|
|
424
|
+
return {
|
|
425
|
+
finalizedHead: dbState.height < 0 ? undefined : toBlockRef(dbState),
|
|
426
|
+
unfinalizedHeads: top?.map(b => ({...toBlockRef(b), templates: b.templates})),
|
|
427
|
+
templates: dbState.templates,
|
|
428
|
+
}
|
|
339
429
|
}
|
|
340
430
|
|
|
341
431
|
function maxBlockRef(a: BlockRef | undefined, b: BlockRef | undefined): BlockRef | undefined {
|
|
@@ -343,3 +433,12 @@ function maxBlockRef(a: BlockRef | undefined, b: BlockRef | undefined): BlockRef
|
|
|
343
433
|
if (b == null) return a
|
|
344
434
|
return a.number >= b.number ? a : b
|
|
345
435
|
}
|
|
436
|
+
|
|
437
|
+
export class TemplateRegistryChanged extends Error {
|
|
438
|
+
readonly isTemplateRegistryChanged = true
|
|
439
|
+
readonly name = 'TemplateRegistryChanged'
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function isTemplateRegistryChanged(err: unknown): err is TemplateRegistryChanged {
|
|
443
|
+
return err instanceof TemplateRegistryChanged
|
|
444
|
+
}
|