@sentio/runtime 4.0.0-rc.2 → 4.0.0-rc.3

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/service.ts DELETED
@@ -1,556 +0,0 @@
1
- import { ConnectError, Code, type HandlerContext, type ServiceImpl } from '@connectrpc/connect'
2
- import { from } from 'ix/Ix.asynciterable'
3
- import { withAbort } from 'ix/Ix.asynciterable.operators'
4
-
5
- import {
6
- type DataBinding,
7
- type Empty,
8
- EmptySchema,
9
- type EthCallParam,
10
- HandlerType,
11
- type PreparedData,
12
- PreparedDataSchema,
13
- type PreprocessResult,
14
- type PreprocessStreamRequest,
15
- ProcessBindingResponseSchema,
16
- type ProcessBindingsRequest,
17
- type ProcessConfigRequest,
18
- type ProcessConfigResponse,
19
- ProcessConfigResponseSchema,
20
- Processor,
21
- type ProcessResult,
22
- ProcessResultSchema,
23
- RuntimeInfoSchema,
24
- type ProcessStreamRequest,
25
- ProcessStreamResponseSchema,
26
- PreprocessStreamResponseSchema,
27
- type StartRequest
28
- } from '@sentio/protos'
29
- import { create, type MessageInitShape } from '@bufbuild/protobuf'
30
-
31
- import { PluginManager } from './plugin.js'
32
- import { errorString, makeEthCallKey, mergeProcessResults } from './utils.js'
33
- import { freezeGlobalConfig, GLOBAL_CONFIG } from './global-config.js'
34
-
35
- import { StoreContext } from './db-context.js'
36
- import { Subject } from 'rxjs'
37
- import { getProvider } from './provider.js'
38
- import { EthChainId } from '@sentio/chain'
39
- import { Provider } from 'ethers'
40
- import { decodeMulticallResult, encodeMulticallData, getMulticallAddress, Multicall3Call } from './multicall.js'
41
-
42
- import { processMetrics } from './metrics.js'
43
- import { ProcessorRuntimeOptions } from './processor-runner-program.js'
44
-
45
- const { process_binding_count, process_binding_time, process_binding_error } = processMetrics
46
-
47
- ;(BigInt.prototype as any).toJSON = function () {
48
- return this.toString()
49
- }
50
-
51
- // Init-shapes carried over the rxjs Subject before being yielded by connect.
52
- // connect accepts MessageInitShape for streaming outputs, so the oneof discriminated
53
- // union must be filled in (e.g. { value: { case: 'result', value: ... } }).
54
- export type ProcessStreamResponseInit = MessageInitShape<typeof ProcessStreamResponseSchema>
55
- export type PreprocessStreamResponseInit = MessageInitShape<typeof PreprocessStreamResponseSchema>
56
-
57
- export class ProcessorServiceImpl implements ServiceImpl<typeof Processor> {
58
- private started = false
59
- // When there is unhandled error, stop process and return unavailable error
60
- unhandled: Error
61
- // private processorConfig: ProcessConfigResponse
62
-
63
- private readonly loader: () => Promise<any>
64
-
65
- private readonly shutdownHandler?: () => void
66
-
67
- private readonly enablePreprocess: boolean
68
-
69
- private preparedData: PreparedData | undefined
70
- readonly enablePartition: boolean
71
-
72
- constructor(loader: () => Promise<any>, options?: ProcessorRuntimeOptions, shutdownHandler?: () => void) {
73
- this.loader = loader
74
- this.shutdownHandler = shutdownHandler
75
-
76
- this.enablePreprocess = process.env['ENABLE_PREPROCESS']
77
- ? process.env['ENABLE_PREPROCESS'].toLowerCase() == 'true'
78
- : false
79
-
80
- this.enablePartition = options?.enablePartition == true
81
- }
82
-
83
- async getConfig(request: ProcessConfigRequest, context: HandlerContext): Promise<ProcessConfigResponse> {
84
- if (!this.started) {
85
- throw new ConnectError('Service Not started.', Code.Unavailable)
86
- }
87
- // if (!this.processorConfig) {
88
- // throw new ConnectError('Process config empty.', Code.Internal)
89
- // }
90
-
91
- // Don't use .create to keep compatiblity
92
- const newConfig = create(ProcessConfigResponseSchema, {})
93
- await PluginManager.INSTANCE.configure(newConfig)
94
- return newConfig
95
- }
96
-
97
- //
98
- // async configure() {
99
- // this.processorConfig = ProcessConfigResponse.fromPartial({})
100
- // await PluginManager.INSTANCE.configure(this.processorConfig)
101
- // }
102
-
103
- async start(request: StartRequest, context: HandlerContext): Promise<MessageInitShape<typeof EmptySchema>> {
104
- if (this.started) {
105
- return {}
106
- }
107
-
108
- freezeGlobalConfig()
109
-
110
- try {
111
- // for (const plugin of ['@sentio/sdk', '@sentio/sdk/eth']) {
112
- // try {
113
- // await import(plugin)
114
- // } catch (e) {
115
- // console.error('Failed to load plugin: ', plugin)
116
- // }
117
- // }
118
- //
119
- // for (const plugin of ['@sentio/sdk/aptos', '@sentio/sdk/solana']) {
120
- // try {
121
- // await import(plugin)
122
- // } catch (e) {}
123
- // }
124
-
125
- await this.loader()
126
- } catch (e) {
127
- throw new ConnectError('Failed to load processor: ' + errorString(e), Code.InvalidArgument)
128
- }
129
-
130
- await PluginManager.INSTANCE.start(request)
131
-
132
- // try {
133
- // await this.configure()
134
- // } catch (e) {
135
- // throw new ConnectError('Failed to start processor : ' + errorString(e), Code.Internal)
136
- // }
137
- this.started = true
138
- return {}
139
- }
140
-
141
- async stop(request: Empty, context: HandlerContext): Promise<MessageInitShape<typeof EmptySchema>> {
142
- console.log('Server Shutting down in 5 seconds')
143
- if (this.shutdownHandler) {
144
- setTimeout(this.shutdownHandler, 5000)
145
- }
146
- return {}
147
- }
148
-
149
- async processBindings(
150
- request: ProcessBindingsRequest,
151
- context?: HandlerContext
152
- ): Promise<MessageInitShape<typeof ProcessBindingResponseSchema>> {
153
- const preparedData = this.enablePreprocess
154
- ? await this.preprocessBindings(request.bindings, {}, undefined, context)
155
- : create(PreparedDataSchema, { ethCallResults: {} })
156
-
157
- const promises = []
158
- for (const binding of request.bindings) {
159
- const promise = this.processBinding(binding, preparedData)
160
- if (GLOBAL_CONFIG.execution.sequential) {
161
- await promise
162
- }
163
- promises.push(promise)
164
- }
165
- let promise
166
- try {
167
- promise = await Promise.all(promises)
168
- processMetrics.process_binding_count.add(request.bindings.length)
169
- } catch (e) {
170
- processMetrics.process_binding_error.add(request.bindings.length)
171
- throw e
172
- }
173
- const result = mergeProcessResults(promise)
174
-
175
- // let updated = false
176
- // if (PluginManager.INSTANCE.stateDiff(this.processorConfig)) {
177
- // await this.configure()
178
- // updated = true
179
- // }
180
-
181
- return {
182
- result
183
- }
184
- }
185
-
186
- async preprocessBindings(
187
- bindings: DataBinding[],
188
- preprocessStore: { [k: string]: any },
189
- dbContext?: StoreContext,
190
- options?: HandlerContext
191
- ): Promise<PreparedData> {
192
- // console.debug(`preprocessBindings start, bindings: ${bindings.length}`)
193
- const promises = []
194
- for (const binding of bindings) {
195
- promises.push(this.preprocessBinding(binding, preprocessStore, dbContext, options))
196
- }
197
- let preprocessResults: PreprocessResult[]
198
- try {
199
- preprocessResults = await Promise.all(promises)
200
- } catch (e) {
201
- throw e
202
- }
203
- const groupedRequests = new Map<string, EthCallParam[]>()
204
- const providers = new Map<string, Provider>()
205
- for (const result of preprocessResults) {
206
- for (const param of result.ethCallParams) {
207
- const { chainId, blockTag } = param.context!
208
- if (!providers.has(chainId)) {
209
- providers.set(chainId, getProvider(chainId as EthChainId))
210
- }
211
- const key = [chainId, blockTag].join('|')
212
- if (!groupedRequests.has(key)) {
213
- groupedRequests.set(key, [])
214
- }
215
- groupedRequests.get(key)!.push(param)
216
- }
217
- }
218
-
219
- const start = Date.now()
220
- const MULTICALL_THRESHOLD = 1
221
- const callPromises: Promise<[string, string]>[] = []
222
- const multicallPromises: Promise<[string, string][]>[] = []
223
-
224
- for (const params of groupedRequests.values()) {
225
- const { chainId, blockTag } = params[0].context!
226
- const multicallAddress = getMulticallAddress(chainId as EthChainId)
227
- if (params.length <= MULTICALL_THRESHOLD || !multicallAddress) {
228
- for (const param of params) {
229
- callPromises.push(
230
- providers
231
- .get(chainId)!
232
- .call({
233
- to: param.context!.address,
234
- data: param.calldata,
235
- blockTag
236
- })
237
- .then((result) => [makeEthCallKey(param), result])
238
- )
239
- }
240
- continue
241
- }
242
-
243
- // construct multicalls
244
- const CHUNK_SIZE = 128
245
- for (let i = 0; i < params.length; i += CHUNK_SIZE) {
246
- const chunk = params.slice(i, i + CHUNK_SIZE)
247
- const calls: Multicall3Call[] = chunk.map((param) => ({
248
- target: param.context!.address,
249
- callData: param.calldata
250
- }))
251
- const data = encodeMulticallData(calls)
252
- multicallPromises.push(
253
- providers
254
- .get(chainId)!
255
- .call({
256
- to: multicallAddress,
257
- data: data,
258
- blockTag
259
- })
260
- .then((raw) => {
261
- const result = decodeMulticallResult(raw).returnData
262
- if (result.length != chunk.length) {
263
- throw new Error(`multicall result length mismatch, params: ${chunk.length}, result: ${result.length}`)
264
- }
265
- const ret: [string, string][] = []
266
- for (let i = 0; i < chunk.length; i++) {
267
- ret.push([makeEthCallKey(chunk[i]), result[i]])
268
- }
269
- return ret
270
- })
271
- )
272
- }
273
- }
274
-
275
- let results: { [p: string]: string } = {}
276
- try {
277
- results = Object.fromEntries(await Promise.all(callPromises))
278
- for (const multicallResult of await Promise.all(multicallPromises)) {
279
- results = {
280
- ...results,
281
- ...Object.fromEntries(multicallResult)
282
- }
283
- }
284
- } catch (e) {
285
- console.error(`eth call error: ${e}`)
286
- }
287
- // console.debug(
288
- // `${Object.keys(results).length} calls finished, actual calls: ${callPromises.length + multicallPromises.length}, elapsed: ${Date.now() - start}ms`
289
- // )
290
- return create(PreparedDataSchema, {
291
- ethCallResults: results
292
- })
293
- }
294
-
295
- async preprocessBinding(
296
- request: DataBinding,
297
- preprocessStore: { [k: string]: any },
298
- dbContext?: StoreContext,
299
- options?: HandlerContext
300
- ): Promise<PreprocessResult> {
301
- if (!this.started) {
302
- throw new ConnectError('Service Not started.', Code.Unavailable)
303
- }
304
- if (this.unhandled) {
305
- throw new ConnectError(
306
- 'Unhandled exception/rejection in previous request: ' + errorString(this.unhandled),
307
- Code.Unavailable
308
- )
309
- }
310
- return await PluginManager.INSTANCE.preprocessBinding(request, preprocessStore, dbContext)
311
- }
312
-
313
- async processBinding(
314
- request: DataBinding,
315
- preparedData: PreparedData | undefined,
316
- options?: HandlerContext
317
- ): Promise<ProcessResult> {
318
- if (!this.started) {
319
- throw new ConnectError('Service Not started.', Code.Unavailable)
320
- }
321
- if (this.unhandled) {
322
- throw new ConnectError(
323
- 'Unhandled exception/rejection in previous request: ' + errorString(this.unhandled),
324
- Code.Unavailable
325
- )
326
- }
327
-
328
- const result = await PluginManager.INSTANCE.processBinding(
329
- request,
330
- preparedData,
331
- PluginManager.INSTANCE.dbContextLocalStorage.getStore()
332
- )
333
- recordRuntimeInfo(result, request.handlerType)
334
- return result
335
- }
336
-
337
- async *processBindingsStream(requests: AsyncIterable<ProcessStreamRequest>, context: HandlerContext) {
338
- if (!this.started) {
339
- throw new ConnectError('Service Not started.', Code.Unavailable)
340
- }
341
-
342
- const subject = new Subject<ProcessStreamResponseInit>()
343
- this.handleRequests(requests, subject)
344
- .then(() => {
345
- if (this.preparedData) {
346
- this.preparedData = create(PreparedDataSchema, { ethCallResults: {} })
347
- }
348
- subject.complete()
349
- })
350
- .catch((e) => {
351
- console.error(e)
352
- subject.error(e)
353
- })
354
- yield* from(subject).pipe(withAbort(context.signal))
355
- }
356
-
357
- async handlePreprocessRequests(
358
- requests: AsyncIterable<PreprocessStreamRequest>,
359
- subject: Subject<PreprocessStreamResponseInit>
360
- ) {
361
- const contexts = new Contexts()
362
- const preprocessStore: { [k: string]: any } = {}
363
-
364
- for await (const request of requests) {
365
- try {
366
- if (request.value.case === 'bindings') {
367
- const bindings = request.value.value.bindings
368
- // NOTE: StoreContext/Contexts are typed for the V2 ProcessStreamResponse stream, but the
369
- // preprocess flow reuses them only to drive DB request/response plumbing. The preprocess
370
- // stream message (flat `dbRequest`) differs from the V2 oneof shape, so we hand the
371
- // preprocess subject in via a cast. db-context.ts owns the actual emit shape; integrator
372
- // should confirm StoreContext.doSend stays compatible with both stream message types.
373
- const dbContext = contexts.new(request.processId, subject as unknown as Subject<ProcessStreamResponseInit>)
374
- const start = Date.now()
375
- this.preprocessBindings(bindings, preprocessStore, dbContext, undefined)
376
- .then((preparedData) => {
377
- // TODO maybe not proper to pass data in this way
378
- this.preparedData = create(PreparedDataSchema, {
379
- ethCallResults: {
380
- ...this.preparedData?.ethCallResults,
381
- ...preparedData.ethCallResults
382
- }
383
- })
384
- subject.next({
385
- processId: request.processId
386
- })
387
- })
388
- .catch((e) => {
389
- console.debug(e)
390
- dbContext.error(request.processId, e)
391
- })
392
- .finally(() => {
393
- const cost = Date.now() - start
394
- console.debug('preprocessBinding', request.processId, ' took', cost, 'ms')
395
- contexts.delete(request.processId)
396
- })
397
- }
398
- if (request.value.case === 'dbResult') {
399
- const dbContext = contexts.get(request.processId)
400
- dbContext?.result(request.value.value)
401
- }
402
- } catch (e) {
403
- // should not happen
404
- console.error('unexpect error during handle loop', e)
405
- }
406
- }
407
- }
408
-
409
- async *preprocessBindingsStream(requests: AsyncIterable<PreprocessStreamRequest>, context: HandlerContext) {
410
- if (!this.started) {
411
- throw new ConnectError('Service Not started.', Code.Unavailable)
412
- }
413
-
414
- const subject = new Subject<PreprocessStreamResponseInit>()
415
- this.handlePreprocessRequests(requests, subject)
416
- .then(() => {
417
- subject.complete()
418
- })
419
- .catch((e) => {
420
- console.error(e)
421
- subject.error(e)
422
- })
423
- yield* from(subject).pipe(withAbort(context.signal))
424
- }
425
-
426
- private dbContexts = new Contexts()
427
-
428
- protected async handleRequests(
429
- requests: AsyncIterable<ProcessStreamRequest>,
430
- subject: Subject<ProcessStreamResponseInit>
431
- ) {
432
- let lastBinding: DataBinding | undefined = undefined
433
- for await (const request of requests) {
434
- try {
435
- // console.log('received request:', request, 'lastBinding:', lastBinding)
436
- if (request.value.case === 'binding') {
437
- lastBinding = request.value.value
438
- }
439
- this.handleRequest(request, lastBinding, subject)
440
- } catch (e) {
441
- // should not happen
442
- console.error('unexpect error during handle loop', e)
443
- }
444
- }
445
- }
446
-
447
- async handleRequest(
448
- request: ProcessStreamRequest,
449
- lastBinding: DataBinding | undefined,
450
- subject: Subject<ProcessStreamResponseInit>
451
- ) {
452
- if (request.value.case === 'binding') {
453
- const binding = request.value.value
454
- process_binding_count.add(1)
455
-
456
- // Adjust binding will make some request become invalid by setting UNKNOWN HandlerType
457
- // for older SDK version, so we just return empty result for them here
458
- if (binding.handlerType === HandlerType.UNKNOWN) {
459
- subject.next({
460
- processId: request.processId,
461
- value: { case: 'result', value: create(ProcessResultSchema) }
462
- })
463
- return
464
- }
465
-
466
- if (this.enablePartition) {
467
- try {
468
- const partitions = await PluginManager.INSTANCE.partition(binding)
469
- subject.next({
470
- processId: request.processId,
471
- value: { case: 'partitions', value: partitions }
472
- })
473
- } catch (e) {
474
- console.error('Partition error:', e)
475
- subject.error(new Error('Partition error: ' + errorString(e)))
476
- return
477
- }
478
- } else {
479
- this.startProcess(request.processId, binding, subject)
480
- }
481
- }
482
-
483
- if (request.value.case === 'start') {
484
- if (!lastBinding) {
485
- console.error('start request received without binding')
486
- subject.error(new Error('start request received without binding'))
487
- return
488
- }
489
- this.startProcess(request.processId, lastBinding, subject)
490
- }
491
-
492
- if (request.value.case === 'dbResult') {
493
- const dbContext = this.dbContexts.get(request.processId)
494
- try {
495
- dbContext?.result(request.value.value)
496
- } catch (e) {
497
- subject.error(new Error('db result error, process should stop'))
498
- }
499
- }
500
- }
501
-
502
- private startProcess(processId: number, binding: DataBinding, subject: Subject<ProcessStreamResponseInit>) {
503
- const dbContext = this.dbContexts.new(processId, subject)
504
- const start = Date.now()
505
- PluginManager.INSTANCE.processBinding(binding, this.preparedData, dbContext)
506
- .then(async (result) => {
507
- // await all pending db requests
508
- await dbContext.awaitPendings()
509
- subject.next({
510
- value: { case: 'result', value: result },
511
- processId: processId
512
- })
513
- recordRuntimeInfo(result, binding.handlerType)
514
- })
515
- .catch((e) => {
516
- console.error(e, e.stack)
517
- dbContext.error(processId, e)
518
- process_binding_error.add(1)
519
- })
520
- .finally(() => {
521
- const cost = Date.now() - start
522
- process_binding_time.add(cost)
523
- this.dbContexts.delete(processId)
524
- })
525
- }
526
- }
527
-
528
- export function recordRuntimeInfo(results: ProcessResult, handlerType: HandlerType) {
529
- for (const list of [results.gauges, results.counters, results.events, results.exports]) {
530
- list.forEach((e) => {
531
- e.runtimeInfo = create(RuntimeInfoSchema, {
532
- from: handlerType
533
- })
534
- })
535
- }
536
- }
537
-
538
- class Contexts {
539
- private contexts: Map<number, StoreContext> = new Map()
540
-
541
- get(processId: number) {
542
- return this.contexts.get(processId)
543
- }
544
-
545
- new(processId: number, subject: Subject<ProcessStreamResponseInit>) {
546
- const context = new StoreContext(subject, processId)
547
- this.contexts.set(processId, context)
548
- return context
549
- }
550
-
551
- delete(processId: number) {
552
- const context = this.get(processId)
553
- context?.close()
554
- this.contexts.delete(processId)
555
- }
556
- }