@sentio/runtime 2.40.0-rc.4 → 2.40.0-rc.41

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/plugin.ts CHANGED
@@ -28,7 +28,7 @@ export abstract class Plugin {
28
28
  return ProcessResult.create()
29
29
  }
30
30
 
31
- async preprocessBinding(request: DataBinding): Promise<PreprocessResult> {
31
+ async preprocessBinding(request: DataBinding, preprocessStore: { [k: string]: any }): Promise<PreprocessResult> {
32
32
  return PreprocessResult.create()
33
33
  }
34
34
  }
@@ -84,13 +84,17 @@ export class PluginManager {
84
84
  })
85
85
  }
86
86
 
87
- preprocessBinding(request: DataBinding, dbContext?: StoreContext): Promise<PreprocessResult> {
87
+ preprocessBinding(
88
+ request: DataBinding,
89
+ preprocessStore: { [k: string]: any },
90
+ dbContext?: StoreContext
91
+ ): Promise<PreprocessResult> {
88
92
  const plugin = this.typesToPlugin.get(request.handlerType)
89
93
  if (!plugin) {
90
94
  throw new Error(`No plugin for ${request.handlerType}`)
91
95
  }
92
96
  return this.dbContextLocalStorage.run(dbContext, () => {
93
- return plugin.preprocessBinding(request)
97
+ return plugin.preprocessBinding(request, preprocessStore)
94
98
  })
95
99
  }
96
100
  }
@@ -7,9 +7,9 @@ import { compressionAlgorithms } from '@grpc/grpc-js'
7
7
  import commandLineArgs from 'command-line-args'
8
8
  import { createServer } from 'nice-grpc'
9
9
  import { errorDetailsServerMiddleware } from 'nice-grpc-error-details'
10
- import { registry as niceGrpcRegistry, prometheusServerMiddleware } from 'nice-grpc-prometheus'
10
+ // import { registry as niceGrpcRegistry } from 'nice-grpc-prometheus'
11
11
  import { openTelemetryServerMiddleware } from 'nice-grpc-opentelemetry'
12
- import { register as globalRegistry, Registry } from 'prom-client'
12
+ // import { register as globalRegistry, Registry } from 'prom-client'
13
13
  import http from 'http'
14
14
  // @ts-ignore inspector promises is not included in @type/node
15
15
  import { Session } from 'node:inspector/promises'
@@ -21,22 +21,17 @@ import { FullProcessorServiceImpl } from './full-service.js'
21
21
  import { ChainConfig } from './chain-config.js'
22
22
  import { setupLogger } from './logger.js'
23
23
 
24
- import { NodeSDK } from '@opentelemetry/sdk-node'
24
+ // import { NodeSDK } from '@opentelemetry/sdk-node'
25
+ import { envDetector } from '@opentelemetry/resources'
25
26
  import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc'
26
27
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
27
- import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
28
+ import { PeriodicExportingMetricReader, MeterProvider } from '@opentelemetry/sdk-metrics'
29
+ import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
30
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
31
+ import { diag, DiagConsoleLogger, DiagLogLevel, metrics, trace } from '@opentelemetry/api'
32
+ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
28
33
 
29
- const sdk = new NodeSDK({
30
- traceExporter: new OTLPTraceExporter(),
31
- metricReader: new PeriodicExportingMetricReader({
32
- exporter: new OTLPMetricExporter()
33
- })
34
- // instrumentations: [getNodeAutoInstrumentations()],
35
- })
36
-
37
- sdk.start()
38
-
39
- const mergedRegistry = Registry.merge([globalRegistry, niceGrpcRegistry])
34
+ // const mergedRegistry = Registry.merge([globalRegistry, niceGrpcRegistry])
40
35
 
41
36
  const optionDefinitions = [
42
37
  { name: 'target', type: String, defaultOption: true },
@@ -58,9 +53,57 @@ const optionDefinitions = [
58
53
 
59
54
  const options = commandLineArgs(optionDefinitions, { partial: true })
60
55
 
61
- setupLogger(options['log-format'] === 'json', options.debug)
56
+ const logLevel = process.env['LOG_LEVEL']?.toUpperCase()
57
+
58
+ setupLogger(options['log-format'] === 'json', logLevel === 'debug' ? true : options.debug)
62
59
  console.debug('Starting with', options.target)
63
60
 
61
+ if (options.debug) {
62
+ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG)
63
+ }
64
+
65
+ const resource = await envDetector.detect()
66
+
67
+ const meterProvider = new MeterProvider({
68
+ resource,
69
+ readers: [
70
+ new PeriodicExportingMetricReader({
71
+ exporter: new OTLPMetricExporter()
72
+ }),
73
+ new PrometheusExporter({
74
+ // http://localhost:4041/metrics
75
+ port: 4041
76
+ })
77
+ ]
78
+ })
79
+
80
+ const traceProvider = new NodeTracerProvider({
81
+ resource: resource
82
+ })
83
+ const exporter = new OTLPTraceExporter() // new ConsoleSpanExporter();
84
+ const processor = new BatchSpanProcessor(exporter)
85
+ traceProvider.addSpanProcessor(processor)
86
+ traceProvider.register()
87
+
88
+ metrics.setGlobalMeterProvider(meterProvider)
89
+ trace.setGlobalTracerProvider(traceProvider)
90
+ ;['SIGINT', 'SIGTERM'].forEach((signal) => {
91
+ process.on(signal as any, () => shutdownProvider())
92
+ })
93
+
94
+ export async function shutdownProvider() {
95
+ const traceProvider = trace.getTracerProvider()
96
+ if (traceProvider instanceof NodeTracerProvider) {
97
+ traceProvider.shutdown().catch(console.error)
98
+ }
99
+ const meterProvider = metrics.getMeterProvider()
100
+ if (meterProvider instanceof MeterProvider) {
101
+ meterProvider.shutdown().catch(console.error)
102
+ }
103
+ }
104
+
105
+ metrics.getMeter('processor').createGauge('up').record(1)
106
+
64
107
  Error.stackTraceLimit = 20
65
108
 
66
109
  const fullPath = path.resolve(options['chains-config'])
@@ -99,7 +142,7 @@ const server = createServer({
99
142
  'grpc.max_receive_message_length': 384 * 1024 * 1024,
100
143
  'grpc.default_compression_algorithm': compressionAlgorithms.gzip
101
144
  })
102
- .use(prometheusServerMiddleware())
145
+ // .use(prometheusServerMiddleware())
103
146
  .use(openTelemetryServerMiddleware())
104
147
  .use(errorDetailsServerMiddleware)
105
148
  const baseService = new ProcessorServiceImpl(async () => {
@@ -122,10 +165,10 @@ const httpServer = http
122
165
  const reqUrl = new URL(req.url, `http://${req.headers.host}`)
123
166
  const queries = reqUrl.searchParams
124
167
  switch (reqUrl.pathname) {
125
- case '/metrics':
126
- const metrics = await mergedRegistry.metrics()
127
- res.write(metrics)
128
- break
168
+ // case '/metrics':
169
+ // const metrics = await mergedRegistry.metrics()
170
+ // res.write(metrics)
171
+ // break
129
172
  case '/profile': {
130
173
  try {
131
174
  const profileTime = parseInt(queries.get('t') || '1000', 10) || 1000
package/src/provider.ts CHANGED
@@ -4,17 +4,11 @@ import PQueue from 'p-queue'
4
4
  import { Endpoints } from './endpoints.js'
5
5
  import { EthChainId } from '@sentio/chain'
6
6
  import { LRUCache } from 'lru-cache'
7
- import { metrics } from '@opentelemetry/api'
7
+ import { providerMetrics } from './metrics.js'
8
+ const { miss_count, hit_count, total_duration, total_queued, queue_size } = providerMetrics
8
9
 
9
10
  export const DummyProvider = new JsonRpcProvider('', Network.from(1))
10
11
 
11
- const meter = metrics.getMeter('processor_provider')
12
- const hit_count = meter.createCounter('provider_hit_count')
13
- const miss_count = meter.createCounter('provider_miss_count')
14
- const queue_size = meter.createGauge('provider_queue_size')
15
- const total_duration = meter.createCounter('provider_total_duration')
16
- const total_queued = meter.createCounter('provider_total_queued')
17
-
18
12
  const providers = new Map<string, JsonRpcProvider>()
19
13
 
20
14
  // export function getEthChainId(networkish?: EthContext | EthChainId): EthChainId {
@@ -38,7 +32,7 @@ export function getProvider(chainId?: EthChainId): Provider {
38
32
  const address = Endpoints.INSTANCE.chainServer.get(chainId)
39
33
  const key = network.chainId.toString() + '-' + address
40
34
 
41
- console.debug(`init provider for ${chainId}, address: ${address}`)
35
+ // console.debug(`init provider for ${chainId}, address: ${address}`)
42
36
  let provider = providers.get(key)
43
37
 
44
38
  if (provider) {
package/src/service.ts CHANGED
@@ -25,20 +25,18 @@ import {
25
25
  } from '@sentio/protos'
26
26
 
27
27
  import { PluginManager } from './plugin.js'
28
- import { errorString, mergeProcessResults } from './utils.js'
28
+ import { errorString, makeEthCallKey, mergeProcessResults } from './utils.js'
29
29
  import { freezeGlobalConfig, GLOBAL_CONFIG } from './global-config.js'
30
30
 
31
31
  import { StoreContext } from './db-context.js'
32
32
  import { Subject } from 'rxjs'
33
- import { metrics } from '@opentelemetry/api'
34
33
  import { getProvider } from './provider.js'
35
34
  import { EthChainId } from '@sentio/chain'
36
- import { Provider, Interface } from 'ethers'
35
+ import { Provider } from 'ethers'
36
+ import { decodeMulticallResult, encodeMulticallData, getMulticallAddress, Multicall3Call } from './multicall.js'
37
37
 
38
- const meter = metrics.getMeter('processor_service')
39
- const process_binding_count = meter.createCounter('process_binding_count')
40
- const process_binding_time = meter.createCounter('process_binding_time')
41
- const process_binding_error = meter.createCounter('process_binding_error')
38
+ import { processMetrics, providerMetrics, dbMetrics } from './metrics.js'
39
+ const { process_binding_count, process_binding_time, process_binding_error } = processMetrics
42
40
 
43
41
  ;(BigInt.prototype as any).toJSON = function () {
44
42
  return this.toString()
@@ -54,6 +52,8 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
54
52
 
55
53
  private readonly shutdownHandler?: () => void
56
54
 
55
+ private preparedData: PreparedData | undefined
56
+
57
57
  constructor(loader: () => Promise<any>, shutdownHandler?: () => void) {
58
58
  this.loader = loader
59
59
  this.shutdownHandler = shutdownHandler
@@ -126,11 +126,11 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
126
126
  }
127
127
 
128
128
  async processBindings(request: ProcessBindingsRequest, options?: CallContext): Promise<ProcessBindingResponse> {
129
- const ethCallResults = await this.preprocessBindings(request.bindings, undefined, options)
129
+ const preparedData = await this.preprocessBindings(request.bindings, {}, undefined, options)
130
130
 
131
131
  const promises = []
132
132
  for (const binding of request.bindings) {
133
- const promise = this.processBinding(binding, { ethCallResults })
133
+ const promise = this.processBinding(binding, preparedData)
134
134
  if (GLOBAL_CONFIG.execution.sequential) {
135
135
  await promise
136
136
  }
@@ -139,7 +139,9 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
139
139
  let promise
140
140
  try {
141
141
  promise = await Promise.all(promises)
142
+ processMetrics.process_binding_count.add(request.bindings.length)
142
143
  } catch (e) {
144
+ processMetrics.process_binding_error.add(request.bindings.length)
143
145
  throw e
144
146
  }
145
147
  const result = mergeProcessResults(promise)
@@ -157,13 +159,14 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
157
159
 
158
160
  async preprocessBindings(
159
161
  bindings: DataBinding[],
162
+ preprocessStore: { [k: string]: any },
160
163
  dbContext?: StoreContext,
161
164
  options?: CallContext
162
- ): Promise<{ [calldata: string]: any[] }> {
163
- console.log(`preprocessBindings start, bindings: ${bindings.length}`)
165
+ ): Promise<PreparedData> {
166
+ // console.debug(`preprocessBindings start, bindings: ${bindings.length}`)
164
167
  const promises = []
165
168
  for (const binding of bindings) {
166
- promises.push(this.preprocessBinding(binding, dbContext, options))
169
+ promises.push(this.preprocessBinding(binding, preprocessStore, dbContext, options))
167
170
  }
168
171
  let preprocessResults: PreprocessResult[]
169
172
  try {
@@ -171,18 +174,15 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
171
174
  } catch (e) {
172
175
  throw e
173
176
  }
174
- console.log(
175
- 'ethCallParams: ',
176
- preprocessResults.map((r) => r.ethCallParams)
177
- )
178
177
  const groupedRequests = new Map<string, EthCallParam[]>()
179
178
  const providers = new Map<string, Provider>()
180
179
  for (const result of preprocessResults) {
181
180
  for (const param of result.ethCallParams) {
182
- if (!providers.has(param.chainId)) {
183
- providers.set(param.chainId, getProvider(param.chainId as EthChainId))
181
+ const { chainId, blockTag } = param.context!
182
+ if (!providers.has(chainId)) {
183
+ providers.set(chainId, getProvider(chainId as EthChainId))
184
184
  }
185
- const key = param.chainId + '|' + param.address
185
+ const key = [chainId, blockTag].join('|')
186
186
  if (!groupedRequests.has(key)) {
187
187
  groupedRequests.set(key, [])
188
188
  }
@@ -191,35 +191,84 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
191
191
  }
192
192
 
193
193
  const start = Date.now()
194
- const callPromises = []
194
+ const MULTICALL_THRESHOLD = 1
195
+ const callPromises: Promise<[string, string]>[] = []
196
+ const multicallPromises: Promise<[string, string][]>[] = []
197
+
195
198
  for (const params of groupedRequests.values()) {
196
- console.log(`chain: ${params[0].chainId}, address: ${params[0].address}, totalCalls: ${params.length}`)
197
- for (const param of params) {
198
- const frag = new Interface([param.signature])
199
- const calldata = frag.encodeFunctionData(param.function, param.args)
200
- callPromises.push(
199
+ const { chainId, blockTag } = params[0].context!
200
+ const multicallAddress = getMulticallAddress(chainId as EthChainId)
201
+ if (params.length <= MULTICALL_THRESHOLD || !multicallAddress) {
202
+ for (const param of params) {
203
+ callPromises.push(
204
+ providers
205
+ .get(chainId)!
206
+ .call({
207
+ to: param.context!.address,
208
+ data: param.calldata,
209
+ blockTag
210
+ })
211
+ .then((result) => [makeEthCallKey(param), result])
212
+ )
213
+ }
214
+ continue
215
+ }
216
+
217
+ // construct multicalls
218
+ const CHUNK_SIZE = 128
219
+ for (let i = 0; i < params.length; i += CHUNK_SIZE) {
220
+ const chunk = params.slice(i, i + CHUNK_SIZE)
221
+ const calls: Multicall3Call[] = chunk.map((param) => ({
222
+ target: param.context!.address,
223
+ callData: param.calldata
224
+ }))
225
+ const data = encodeMulticallData(calls)
226
+ multicallPromises.push(
201
227
  providers
202
- .get(param.chainId)!
228
+ .get(chainId)!
203
229
  .call({
204
- to: param.address,
205
- data: calldata
230
+ to: multicallAddress,
231
+ data: data,
232
+ blockTag
233
+ })
234
+ .then((raw) => {
235
+ const result = decodeMulticallResult(raw).returnData
236
+ if (result.length != chunk.length) {
237
+ throw new Error(`multicall result length mismatch, params: ${chunk.length}, result: ${result.length}`)
238
+ }
239
+ const ret: [string, string][] = []
240
+ for (let i = 0; i < chunk.length; i++) {
241
+ ret.push([makeEthCallKey(chunk[i]), result[i]])
242
+ }
243
+ return ret
206
244
  })
207
- .then((ret) => [calldata, frag.decodeFunctionResult(param.function, ret).toArray()] as [string, any[]])
208
245
  )
209
246
  }
210
247
  }
211
- let results = {}
248
+
249
+ let results: { [p: string]: string } = {}
212
250
  try {
213
251
  results = Object.fromEntries(await Promise.all(callPromises))
252
+ for (const multicallResult of await Promise.all(multicallPromises)) {
253
+ results = {
254
+ ...results,
255
+ ...Object.fromEntries(multicallResult)
256
+ }
257
+ }
214
258
  } catch (e) {
215
259
  console.error(`eth call error: ${e}`)
216
260
  }
217
- console.log(`${callPromises.length} calls finished, elapsed: ${Date.now() - start}ms`)
218
- return results
261
+ // console.debug(
262
+ // `${Object.keys(results).length} calls finished, actual calls: ${callPromises.length + multicallPromises.length}, elapsed: ${Date.now() - start}ms`
263
+ // )
264
+ return {
265
+ ethCallResults: results
266
+ }
219
267
  }
220
268
 
221
269
  async preprocessBinding(
222
270
  request: DataBinding,
271
+ preprocessStore: { [k: string]: any },
223
272
  dbContext?: StoreContext,
224
273
  options?: CallContext
225
274
  ): Promise<PreprocessResult> {
@@ -238,7 +287,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
238
287
  ]
239
288
  )
240
289
  }
241
- return await PluginManager.INSTANCE.preprocessBinding(request, dbContext)
290
+ return await PluginManager.INSTANCE.preprocessBinding(request, preprocessStore, dbContext)
242
291
  }
243
292
 
244
293
  async processBinding(
@@ -274,6 +323,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
274
323
  const subject = new Subject<DeepPartial<ProcessStreamResponse>>()
275
324
  this.handleRequests(requests, subject)
276
325
  .then(() => {
326
+ this.preparedData = { ethCallResults: {} }
277
327
  subject.complete()
278
328
  })
279
329
  .catch((e) => {
@@ -288,16 +338,23 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
288
338
  subject: Subject<DeepPartial<PreprocessStreamResponse>>
289
339
  ) {
290
340
  const contexts = new Contexts()
341
+ const preprocessStore: { [k: string]: any } = {}
291
342
 
292
343
  for await (const request of requests) {
293
344
  try {
294
- console.debug('received request:', request)
295
345
  if (request.bindings) {
296
346
  const bindings = request.bindings.bindings
297
347
  const dbContext = contexts.new(request.processId, subject)
298
348
  const start = Date.now()
299
- this.preprocessBindings(bindings, dbContext)
300
- .then(() => {
349
+ this.preprocessBindings(bindings, preprocessStore, dbContext, undefined)
350
+ .then((preparedData) => {
351
+ // TODO maybe not proper to pass data in this way
352
+ this.preparedData = {
353
+ ethCallResults: {
354
+ ...this.preparedData?.ethCallResults,
355
+ ...preparedData.ethCallResults
356
+ }
357
+ }
301
358
  subject.next({
302
359
  processId: request.processId
303
360
  })
@@ -305,12 +362,10 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
305
362
  .catch((e) => {
306
363
  console.debug(e)
307
364
  dbContext.error(request.processId, e)
308
- process_binding_error.add(1)
309
365
  })
310
366
  .finally(() => {
311
367
  const cost = Date.now() - start
312
368
  console.debug('preprocessBinding', request.processId, ' took', cost, 'ms')
313
- process_binding_time.add(cost)
314
369
  contexts.delete(request.processId)
315
370
  })
316
371
  }
@@ -350,14 +405,16 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
350
405
 
351
406
  for await (const request of requests) {
352
407
  try {
353
- console.debug('received request:', request)
408
+ // console.debug('received request:', request)
354
409
  if (request.binding) {
355
410
  process_binding_count.add(1)
356
411
  const binding = request.binding
357
412
  const dbContext = contexts.new(request.processId, subject)
358
413
  const start = Date.now()
359
- PluginManager.INSTANCE.processBinding(binding, undefined, dbContext)
360
- .then((result) => {
414
+ PluginManager.INSTANCE.processBinding(binding, this.preparedData, dbContext)
415
+ .then(async (result) => {
416
+ // await all pending db requests
417
+ await dbContext.awaitPendings()
361
418
  subject.next({
362
419
  result,
363
420
  processId: request.processId
@@ -374,11 +431,17 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
374
431
  console.debug('processBinding', request.processId, ' took', cost, 'ms')
375
432
  process_binding_time.add(cost)
376
433
  contexts.delete(request.processId)
434
+ console.debug('db stats', JSON.stringify(dbMetrics.stats()))
435
+ console.debug('provider stats', JSON.stringify(providerMetrics.stats()))
377
436
  })
378
437
  }
379
438
  if (request.dbResult) {
380
439
  const dbContext = contexts.get(request.processId)
381
- dbContext?.result(request.dbResult)
440
+ try {
441
+ dbContext?.result(request.dbResult)
442
+ } catch (e) {
443
+ subject.error(new Error('db result error, process should stop'))
444
+ }
382
445
  }
383
446
  } catch (e) {
384
447
  // should not happen
@@ -8,6 +8,8 @@ export default defineConfig({
8
8
  },
9
9
  entry: ['src/index.ts', 'src/processor-runner.ts'],
10
10
  outDir: 'lib',
11
+ minify: true,
12
+ sourcemap: true,
11
13
  clean: true,
12
14
  dts: true,
13
15
  format: 'esm'
package/src/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ProcessResult } from '@sentio/protos'
1
+ import { EthCallParam, ProcessResult } from '@sentio/protos'
2
2
 
3
3
  // TODO better handling this, because old proto doesn't have this
4
4
  import { StateResult, ProcessResult as ProcessResultFull } from './gen/processor/protos/processor.js'
@@ -8,7 +8,7 @@ import { Required } from 'utility-types'
8
8
  export function mergeProcessResults(results: ProcessResult[]): Required<ProcessResult, 'states'> {
9
9
  const res = {
10
10
  ...ProcessResultFull.create(),
11
- states: StateResult.create(),
11
+ states: StateResult.create()
12
12
  }
13
13
 
14
14
  for (const r of results) {
@@ -17,7 +17,7 @@ export function mergeProcessResults(results: ProcessResult[]): Required<ProcessR
17
17
  res.events = res.events.concat(r.events)
18
18
  res.exports = res.exports.concat(r.exports)
19
19
  res.states = {
20
- configUpdated: res.states?.configUpdated || r.states?.configUpdated || false,
20
+ configUpdated: res.states?.configUpdated || r.states?.configUpdated || false
21
21
  }
22
22
  }
23
23
  return res
@@ -28,3 +28,11 @@ export function errorString(e: Error): string {
28
28
  }
29
29
 
30
30
  export const USER_PROCESSOR = 'user_processor'
31
+
32
+ export function makeEthCallKey(param: EthCallParam) {
33
+ if (!param.context) {
34
+ throw new Error('null context for eth call')
35
+ }
36
+ const { chainId, address, blockTag } = param.context
37
+ return `${chainId}|${address}|${blockTag}|${param.calldata}`.toLowerCase()
38
+ }