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

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,55 @@ 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
+
64
105
  Error.stackTraceLimit = 20
65
106
 
66
107
  const fullPath = path.resolve(options['chains-config'])
@@ -99,7 +140,7 @@ const server = createServer({
99
140
  'grpc.max_receive_message_length': 384 * 1024 * 1024,
100
141
  'grpc.default_compression_algorithm': compressionAlgorithms.gzip
101
142
  })
102
- .use(prometheusServerMiddleware())
143
+ // .use(prometheusServerMiddleware())
103
144
  .use(openTelemetryServerMiddleware())
104
145
  .use(errorDetailsServerMiddleware)
105
146
  const baseService = new ProcessorServiceImpl(async () => {
@@ -122,10 +163,10 @@ const httpServer = http
122
163
  const reqUrl = new URL(req.url, `http://${req.headers.host}`)
123
164
  const queries = reqUrl.searchParams
124
165
  switch (reqUrl.pathname) {
125
- case '/metrics':
126
- const metrics = await mergedRegistry.metrics()
127
- res.write(metrics)
128
- break
166
+ // case '/metrics':
167
+ // const metrics = await mergedRegistry.metrics()
168
+ // res.write(metrics)
169
+ // break
129
170
  case '/profile': {
130
171
  try {
131
172
  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
  }
@@ -157,13 +157,14 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
157
157
 
158
158
  async preprocessBindings(
159
159
  bindings: DataBinding[],
160
+ preprocessStore: { [k: string]: any },
160
161
  dbContext?: StoreContext,
161
162
  options?: CallContext
162
- ): Promise<{ [calldata: string]: any[] }> {
163
- console.log(`preprocessBindings start, bindings: ${bindings.length}`)
163
+ ): Promise<PreparedData> {
164
+ // console.debug(`preprocessBindings start, bindings: ${bindings.length}`)
164
165
  const promises = []
165
166
  for (const binding of bindings) {
166
- promises.push(this.preprocessBinding(binding, dbContext, options))
167
+ promises.push(this.preprocessBinding(binding, preprocessStore, dbContext, options))
167
168
  }
168
169
  let preprocessResults: PreprocessResult[]
169
170
  try {
@@ -171,18 +172,15 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
171
172
  } catch (e) {
172
173
  throw e
173
174
  }
174
- console.log(
175
- 'ethCallParams: ',
176
- preprocessResults.map((r) => r.ethCallParams)
177
- )
178
175
  const groupedRequests = new Map<string, EthCallParam[]>()
179
176
  const providers = new Map<string, Provider>()
180
177
  for (const result of preprocessResults) {
181
178
  for (const param of result.ethCallParams) {
182
- if (!providers.has(param.chainId)) {
183
- providers.set(param.chainId, getProvider(param.chainId as EthChainId))
179
+ const { chainId, blockTag } = param.context!
180
+ if (!providers.has(chainId)) {
181
+ providers.set(chainId, getProvider(chainId as EthChainId))
184
182
  }
185
- const key = param.chainId + '|' + param.address
183
+ const key = [chainId, blockTag].join('|')
186
184
  if (!groupedRequests.has(key)) {
187
185
  groupedRequests.set(key, [])
188
186
  }
@@ -191,35 +189,84 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
191
189
  }
192
190
 
193
191
  const start = Date.now()
194
- const callPromises = []
192
+ const MULTICALL_THRESHOLD = 1
193
+ const callPromises: Promise<[string, string]>[] = []
194
+ const multicallPromises: Promise<[string, string][]>[] = []
195
+
195
196
  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(
197
+ const { chainId, blockTag } = params[0].context!
198
+ const multicallAddress = getMulticallAddress(chainId as EthChainId)
199
+ if (params.length <= MULTICALL_THRESHOLD || !multicallAddress) {
200
+ for (const param of params) {
201
+ callPromises.push(
202
+ providers
203
+ .get(chainId)!
204
+ .call({
205
+ to: param.context!.address,
206
+ data: param.calldata,
207
+ blockTag
208
+ })
209
+ .then((result) => [makeEthCallKey(param), result])
210
+ )
211
+ }
212
+ continue
213
+ }
214
+
215
+ // construct multicalls
216
+ const CHUNK_SIZE = 128
217
+ for (let i = 0; i < params.length; i += CHUNK_SIZE) {
218
+ const chunk = params.slice(i, i + CHUNK_SIZE)
219
+ const calls: Multicall3Call[] = chunk.map((param) => ({
220
+ target: param.context!.address,
221
+ callData: param.calldata
222
+ }))
223
+ const data = encodeMulticallData(calls)
224
+ multicallPromises.push(
201
225
  providers
202
- .get(param.chainId)!
226
+ .get(chainId)!
203
227
  .call({
204
- to: param.address,
205
- data: calldata
228
+ to: multicallAddress,
229
+ data: data,
230
+ blockTag
231
+ })
232
+ .then((raw) => {
233
+ const result = decodeMulticallResult(raw).returnData
234
+ if (result.length != chunk.length) {
235
+ throw new Error(`multicall result length mismatch, params: ${chunk.length}, result: ${result.length}`)
236
+ }
237
+ const ret: [string, string][] = []
238
+ for (let i = 0; i < chunk.length; i++) {
239
+ ret.push([makeEthCallKey(chunk[i]), result[i]])
240
+ }
241
+ return ret
206
242
  })
207
- .then((ret) => [calldata, frag.decodeFunctionResult(param.function, ret).toArray()] as [string, any[]])
208
243
  )
209
244
  }
210
245
  }
211
- let results = {}
246
+
247
+ let results: { [p: string]: string } = {}
212
248
  try {
213
249
  results = Object.fromEntries(await Promise.all(callPromises))
250
+ for (const multicallResult of await Promise.all(multicallPromises)) {
251
+ results = {
252
+ ...results,
253
+ ...Object.fromEntries(multicallResult)
254
+ }
255
+ }
214
256
  } catch (e) {
215
257
  console.error(`eth call error: ${e}`)
216
258
  }
217
- console.log(`${callPromises.length} calls finished, elapsed: ${Date.now() - start}ms`)
218
- return results
259
+ // console.debug(
260
+ // `${Object.keys(results).length} calls finished, actual calls: ${callPromises.length + multicallPromises.length}, elapsed: ${Date.now() - start}ms`
261
+ // )
262
+ return {
263
+ ethCallResults: results
264
+ }
219
265
  }
220
266
 
221
267
  async preprocessBinding(
222
268
  request: DataBinding,
269
+ preprocessStore: { [k: string]: any },
223
270
  dbContext?: StoreContext,
224
271
  options?: CallContext
225
272
  ): Promise<PreprocessResult> {
@@ -238,7 +285,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
238
285
  ]
239
286
  )
240
287
  }
241
- return await PluginManager.INSTANCE.preprocessBinding(request, dbContext)
288
+ return await PluginManager.INSTANCE.preprocessBinding(request, preprocessStore, dbContext)
242
289
  }
243
290
 
244
291
  async processBinding(
@@ -274,6 +321,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
274
321
  const subject = new Subject<DeepPartial<ProcessStreamResponse>>()
275
322
  this.handleRequests(requests, subject)
276
323
  .then(() => {
324
+ this.preparedData = { ethCallResults: {} }
277
325
  subject.complete()
278
326
  })
279
327
  .catch((e) => {
@@ -288,16 +336,23 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
288
336
  subject: Subject<DeepPartial<PreprocessStreamResponse>>
289
337
  ) {
290
338
  const contexts = new Contexts()
339
+ const preprocessStore: { [k: string]: any } = {}
291
340
 
292
341
  for await (const request of requests) {
293
342
  try {
294
- console.debug('received request:', request)
295
343
  if (request.bindings) {
296
344
  const bindings = request.bindings.bindings
297
345
  const dbContext = contexts.new(request.processId, subject)
298
346
  const start = Date.now()
299
- this.preprocessBindings(bindings, dbContext)
300
- .then(() => {
347
+ this.preprocessBindings(bindings, preprocessStore, dbContext, undefined)
348
+ .then((preparedData) => {
349
+ // TODO maybe not proper to pass data in this way
350
+ this.preparedData = {
351
+ ethCallResults: {
352
+ ...this.preparedData?.ethCallResults,
353
+ ...preparedData.ethCallResults
354
+ }
355
+ }
301
356
  subject.next({
302
357
  processId: request.processId
303
358
  })
@@ -305,12 +360,10 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
305
360
  .catch((e) => {
306
361
  console.debug(e)
307
362
  dbContext.error(request.processId, e)
308
- process_binding_error.add(1)
309
363
  })
310
364
  .finally(() => {
311
365
  const cost = Date.now() - start
312
366
  console.debug('preprocessBinding', request.processId, ' took', cost, 'ms')
313
- process_binding_time.add(cost)
314
367
  contexts.delete(request.processId)
315
368
  })
316
369
  }
@@ -350,14 +403,16 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
350
403
 
351
404
  for await (const request of requests) {
352
405
  try {
353
- console.debug('received request:', request)
406
+ // console.debug('received request:', request)
354
407
  if (request.binding) {
355
408
  process_binding_count.add(1)
356
409
  const binding = request.binding
357
410
  const dbContext = contexts.new(request.processId, subject)
358
411
  const start = Date.now()
359
- PluginManager.INSTANCE.processBinding(binding, undefined, dbContext)
360
- .then((result) => {
412
+ PluginManager.INSTANCE.processBinding(binding, this.preparedData, dbContext)
413
+ .then(async (result) => {
414
+ // await all pending db requests
415
+ await dbContext.awaitPendings()
361
416
  subject.next({
362
417
  result,
363
418
  processId: request.processId
@@ -374,11 +429,17 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
374
429
  console.debug('processBinding', request.processId, ' took', cost, 'ms')
375
430
  process_binding_time.add(cost)
376
431
  contexts.delete(request.processId)
432
+ console.debug('db stats', JSON.stringify(dbMetrics.stats()))
433
+ console.debug('provider stats', JSON.stringify(providerMetrics.stats()))
377
434
  })
378
435
  }
379
436
  if (request.dbResult) {
380
437
  const dbContext = contexts.get(request.processId)
381
- dbContext?.result(request.dbResult)
438
+ try {
439
+ dbContext?.result(request.dbResult)
440
+ } catch (e) {
441
+ subject.error(new Error('db result error, process should stop'))
442
+ }
382
443
  }
383
444
  } catch (e) {
384
445
  // 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
+ }