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