@sentio/runtime 2.40.0-rc.9 → 2.40.1-rc.1

Sign up to get free protection for your applications and to get access to all the features.
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
  }