@sentio/runtime 2.40.0-rc.9 → 2.40.0

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/otlp.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { envDetector } from '@opentelemetry/resources'
2
+ import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
3
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc'
4
+ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
5
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
6
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
7
+ import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
8
+ import { metrics, trace } from '@opentelemetry/api'
9
+
10
+ export async function setupOTLP() {
11
+ const resource = await envDetector.detect()
12
+
13
+ const meterProvider = new MeterProvider({
14
+ resource,
15
+ readers: [
16
+ new PeriodicExportingMetricReader({
17
+ exporter: new OTLPMetricExporter()
18
+ }),
19
+ new PrometheusExporter({
20
+ // http://localhost:4041/metrics
21
+ port: 4041
22
+ })
23
+ ]
24
+ })
25
+
26
+ const traceProvider = new NodeTracerProvider({
27
+ resource: resource
28
+ })
29
+ const exporter = new OTLPTraceExporter() // new ConsoleSpanExporter();
30
+ const processor = new BatchSpanProcessor(exporter)
31
+ traceProvider.addSpanProcessor(processor)
32
+ traceProvider.register()
33
+
34
+ metrics.setGlobalMeterProvider(meterProvider)
35
+ trace.setGlobalTracerProvider(traceProvider)
36
+ ;['SIGINT', 'SIGTERM'].forEach((signal) => {
37
+ process.on(signal as any, () => shutdownProvider())
38
+ })
39
+ }
40
+
41
+ export async function shutdownProvider() {
42
+ const traceProvider = trace.getTracerProvider()
43
+ if (traceProvider instanceof NodeTracerProvider) {
44
+ traceProvider.shutdown().catch(console.error)
45
+ }
46
+ const meterProvider = metrics.getMeterProvider()
47
+ if (meterProvider instanceof MeterProvider) {
48
+ meterProvider.shutdown().catch(console.error)
49
+ }
50
+ }
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
@@ -30,15 +30,13 @@ 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
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,7 +52,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
54
52
 
55
53
  private readonly shutdownHandler?: () => void
56
54
 
57
- private readonly preprocessedEthCalls: { [calldata: string]: any[] }
55
+ private preparedData: PreparedData | undefined
58
56
 
59
57
  constructor(loader: () => Promise<any>, shutdownHandler?: () => void) {
60
58
  this.loader = loader
@@ -128,11 +126,11 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
128
126
  }
129
127
 
130
128
  async processBindings(request: ProcessBindingsRequest, options?: CallContext): Promise<ProcessBindingResponse> {
131
- const ethCallResults = await this.preprocessBindings(request.bindings, undefined, options)
129
+ const preparedData = await this.preprocessBindings(request.bindings, {}, undefined, options)
132
130
 
133
131
  const promises = []
134
132
  for (const binding of request.bindings) {
135
- const promise = this.processBinding(binding, { ethCallResults })
133
+ const promise = this.processBinding(binding, preparedData)
136
134
  if (GLOBAL_CONFIG.execution.sequential) {
137
135
  await promise
138
136
  }
@@ -141,7 +139,9 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
141
139
  let promise
142
140
  try {
143
141
  promise = await Promise.all(promises)
142
+ processMetrics.process_binding_count.add(request.bindings.length)
144
143
  } catch (e) {
144
+ processMetrics.process_binding_error.add(request.bindings.length)
145
145
  throw e
146
146
  }
147
147
  const result = mergeProcessResults(promise)
@@ -159,13 +159,14 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
159
159
 
160
160
  async preprocessBindings(
161
161
  bindings: DataBinding[],
162
+ preprocessStore: { [k: string]: any },
162
163
  dbContext?: StoreContext,
163
164
  options?: CallContext
164
- ): Promise<{ [ethCallKey: string]: string }> {
165
- console.debug(`preprocessBindings start, bindings: ${bindings.length}`)
165
+ ): Promise<PreparedData> {
166
+ // console.debug(`preprocessBindings start, bindings: ${bindings.length}`)
166
167
  const promises = []
167
168
  for (const binding of bindings) {
168
- promises.push(this.preprocessBinding(binding, dbContext, options))
169
+ promises.push(this.preprocessBinding(binding, preprocessStore, dbContext, options))
169
170
  }
170
171
  let preprocessResults: PreprocessResult[]
171
172
  try {
@@ -173,19 +174,15 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
173
174
  } catch (e) {
174
175
  throw e
175
176
  }
176
- console.debug(
177
- 'ethCallParams: ',
178
- preprocessResults.map((r) => r.ethCallParams)
179
- )
180
177
  const groupedRequests = new Map<string, EthCallParam[]>()
181
178
  const providers = new Map<string, Provider>()
182
179
  for (const result of preprocessResults) {
183
180
  for (const param of result.ethCallParams) {
184
- const { chainId, address, blockTag } = param.context!
181
+ const { chainId, blockTag } = param.context!
185
182
  if (!providers.has(chainId)) {
186
183
  providers.set(chainId, getProvider(chainId as EthChainId))
187
184
  }
188
- const key = [chainId, address, blockTag].join('|')
185
+ const key = [chainId, blockTag].join('|')
189
186
  if (!groupedRequests.has(key)) {
190
187
  groupedRequests.set(key, [])
191
188
  }
@@ -194,36 +191,84 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
194
191
  }
195
192
 
196
193
  const start = Date.now()
197
- const callPromises = []
194
+ const MULTICALL_THRESHOLD = 1
195
+ const callPromises: Promise<[string, string]>[] = []
196
+ const multicallPromises: Promise<[string, string][]>[] = []
197
+
198
198
  for (const params of groupedRequests.values()) {
199
- const { chainId, address, blockTag } = params[0].context!
200
- console.log(`chain: ${chainId}, address: ${address}, blockTag: ${blockTag}, totalCalls: ${params.length}`)
201
- // TODO multicall
202
- for (const param of params) {
203
- 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(
204
227
  providers
205
228
  .get(chainId)!
206
229
  .call({
207
- to: address,
208
- data: param.calldata,
230
+ to: multicallAddress,
231
+ data: data,
209
232
  blockTag
210
233
  })
211
- .then((result) => [makeEthCallKey(param), result])
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
244
+ })
212
245
  )
213
246
  }
214
247
  }
215
- let results = {}
248
+
249
+ let results: { [p: string]: string } = {}
216
250
  try {
217
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
+ }
218
258
  } catch (e) {
219
259
  console.error(`eth call error: ${e}`)
220
260
  }
221
- console.log(`${callPromises.length} calls finished, elapsed: ${Date.now() - start}ms`)
222
- 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
+ }
223
267
  }
224
268
 
225
269
  async preprocessBinding(
226
270
  request: DataBinding,
271
+ preprocessStore: { [k: string]: any },
227
272
  dbContext?: StoreContext,
228
273
  options?: CallContext
229
274
  ): Promise<PreprocessResult> {
@@ -242,7 +287,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
242
287
  ]
243
288
  )
244
289
  }
245
- return await PluginManager.INSTANCE.preprocessBinding(request, dbContext)
290
+ return await PluginManager.INSTANCE.preprocessBinding(request, preprocessStore, dbContext)
246
291
  }
247
292
 
248
293
  async processBinding(
@@ -278,6 +323,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
278
323
  const subject = new Subject<DeepPartial<ProcessStreamResponse>>()
279
324
  this.handleRequests(requests, subject)
280
325
  .then(() => {
326
+ this.preparedData = { ethCallResults: {} }
281
327
  subject.complete()
282
328
  })
283
329
  .catch((e) => {
@@ -292,16 +338,23 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
292
338
  subject: Subject<DeepPartial<PreprocessStreamResponse>>
293
339
  ) {
294
340
  const contexts = new Contexts()
341
+ const preprocessStore: { [k: string]: any } = {}
295
342
 
296
343
  for await (const request of requests) {
297
344
  try {
298
- console.debug('received request:', request)
299
345
  if (request.bindings) {
300
346
  const bindings = request.bindings.bindings
301
347
  const dbContext = contexts.new(request.processId, subject)
302
348
  const start = Date.now()
303
- this.preprocessBindings(bindings, dbContext)
304
- .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
+ }
305
358
  subject.next({
306
359
  processId: request.processId
307
360
  })
@@ -352,14 +405,16 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
352
405
 
353
406
  for await (const request of requests) {
354
407
  try {
355
- console.debug('received request:', request)
408
+ // console.debug('received request:', request)
356
409
  if (request.binding) {
357
410
  process_binding_count.add(1)
358
411
  const binding = request.binding
359
412
  const dbContext = contexts.new(request.processId, subject)
360
413
  const start = Date.now()
361
- PluginManager.INSTANCE.processBinding(binding, undefined, dbContext)
362
- .then((result) => {
414
+ PluginManager.INSTANCE.processBinding(binding, this.preparedData, dbContext)
415
+ .then(async (result) => {
416
+ // await all pending db requests
417
+ await dbContext.awaitPendings()
363
418
  subject.next({
364
419
  result,
365
420
  processId: request.processId
@@ -376,11 +431,17 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
376
431
  console.debug('processBinding', request.processId, ' took', cost, 'ms')
377
432
  process_binding_time.add(cost)
378
433
  contexts.delete(request.processId)
434
+ console.debug('db stats', JSON.stringify(dbMetrics.stats()))
435
+ console.debug('provider stats', JSON.stringify(providerMetrics.stats()))
379
436
  })
380
437
  }
381
438
  if (request.dbResult) {
382
439
  const dbContext = contexts.get(request.processId)
383
- 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
+ }
384
445
  }
385
446
  } catch (e) {
386
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
@@ -34,5 +34,5 @@ export function makeEthCallKey(param: EthCallParam) {
34
34
  throw new Error('null context for eth call')
35
35
  }
36
36
  const { chainId, address, blockTag } = param.context
37
- return `${chainId}|${address}|${blockTag}|${param.calldata}`
37
+ return `${chainId}|${address}|${blockTag}|${param.calldata}`.toLowerCase()
38
38
  }