@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/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 {HashAndHeight, Database} from './database'
4
- import {DataSource, isForkException, BlockRef, type BlockBatch} from '@subsquid/util-internal-data-source'
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 ProcessorState {
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 ? this.src.getHead.bind(this.src) : this.src.getFinalizedHead.bind(this.src)
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 state: ProcessorState = {
95
- finalizedHead: undefined,
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 = getStateHead(state)
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
- while (true) {
114
- let getStream = this.db.supportsHotBlocks
115
- ? this.src.getStream.bind(this.src)
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
- head = getStateHead(state)
196
+ while (true) {
119
197
  try {
120
- for await (let data of getStream({after: head})) {
121
- state = await this.processBatch(state, data, await chainHeight.get())
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 // Stream completed successfully, exit loop
223
+ break
124
224
  } catch (e) {
125
- if (!isForkException(e) || !this.db.supportsHotBlocks) throw e
126
-
127
- // Handle fork and continue loop to retry
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
- ): Promise<ProcessorState> {
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 state
269
+ if (blocks.length === 0) return
179
270
 
180
- let prevHead = getStateHead(state)
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 (finalizedHeadData != null && state.finalizedHead != null && finalizedHeadData.number <= state.finalizedHead.number) {
188
- finalizedHeadData = state.finalizedHead
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 = 0
192
- if (finalizedHeadData != null) {
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[blocks.length - 1].header
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
- return this.handler({
220
- store,
221
- blocks: (start === 0 && end === blocks.length) ? blocks : blocks.slice(start, end),
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
- if (finalizedHead) {
228
- state.finalizedHead = finalizedHead
314
+ let newFinalizedHead = finalizedHead ?? this.state.finalizedHead
315
+ if (newFinalizedHead) {
316
+ this.state.prune(newFinalizedHead.number)
229
317
  }
230
- if (state.finalizedHead) {
231
- let idx = state.unfinalizedHeads.findIndex((h) => h.number > state.finalizedHead!.number)
232
- state.unfinalizedHeads = idx < 0 ? [] : state.unfinalizedHeads.slice(idx)
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
- for (let i = unfinalizedIndex; i < blocks.length; i++) {
236
- state.unfinalizedHeads.push({number: blocks[i].header.number, hash: blocks[i].header.hash})
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: prevHead ? toHashAndHeight(prevHead) : {height: -1, hash: '0x'},
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.finalizedHead = {number: nextHead.number, hash: nextHead.hash}
258
- state.unfinalizedHeads = []
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(hashAndHeight: HashAndHeight): BlockRef {
334
- return {number: hashAndHeight.height, hash: hashAndHeight.hash}
418
+ function toBlockRef(hh: HashAndHeight): BlockRef {
419
+ return {number: hh.height, hash: hh.hash}
335
420
  }
336
421
 
337
- function getStateHead(state: ProcessorState): BlockRef | undefined {
338
- return maybeLast(state.unfinalizedHeads) ?? state.finalizedHead
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
+ }