@sentio/runtime 2.57.11 → 2.57.12-rc.a

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/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@sentio/runtime",
3
- "version": "2.57.11",
3
+ "version": "2.57.12-rc.a",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./lib/index.js"
7
+ ".": "./lib/index.js",
8
+ "./service-worker": "./lib/service-worker.js"
8
9
  },
9
10
  "bin": {
10
11
  "processor-runner": "./lib/processor-runner.js"
@@ -14,12 +15,36 @@
14
15
  "!**/*.test.{js,ts}",
15
16
  "!{lib,src}/tests"
16
17
  ],
17
- "dependencies": {},
18
+ "dependencies": {
19
+ "@grpc/grpc-js": "^1.9.14",
20
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.0",
21
+ "@opentelemetry/exporter-prometheus": "^0.57.0",
22
+ "@opentelemetry/sdk-node": "^0.57.0",
23
+ "command-line-args": "^6.0.0",
24
+ "command-line-usage": "^7.0.1",
25
+ "ethers": "npm:@sentio/ethers@6.13.1-patch.5",
26
+ "fs-extra": "^11.2.0",
27
+ "google-protobuf": "^3.21.2",
28
+ "ix": "^7.0.0",
29
+ "long": "^5.2.3",
30
+ "nice-grpc": "^2.1.10",
31
+ "nice-grpc-client-middleware-retry": "^3.1.6",
32
+ "nice-grpc-common": "^2.0.2",
33
+ "nice-grpc-error-details": "^0.2.4",
34
+ "nice-grpc-opentelemetry": "^0.1.15",
35
+ "nice-grpc-prometheus": "^0.2.2",
36
+ "protobufjs": "^7.2.6",
37
+ "rxjs": "^7.8.1",
38
+ "utility-types": "^3.11.0",
39
+ "winston": "^3.11.0",
40
+ "@sentio/protos": "2.57.12-rc.a"
41
+ },
18
42
  "devDependencies": {
19
43
  "@types/command-line-args": "^5.2.3",
20
44
  "@types/command-line-usage": "^5.0.4",
21
45
  "@types/fs-extra": "^11.0.4",
22
- "@types/google-protobuf": "^3.15.12"
46
+ "@types/google-protobuf": "^3.15.12",
47
+ "piscina": "5.0.0-alpha.0"
23
48
  },
24
49
  "engines": {
25
50
  "node": ">=20"
@@ -22,10 +22,11 @@ import { setupLogger } from './logger.js'
22
22
 
23
23
  import { setupOTLP } from './otlp.js'
24
24
  import { ActionServer } from './action-server.js'
25
+ import { ServiceManager } from './service-manager.js'
25
26
 
26
27
  // const mergedRegistry = Registry.merge([globalRegistry, niceGrpcRegistry])
27
28
 
28
- const optionDefinitions = [
29
+ export const optionDefinitions = [
29
30
  { name: 'target', type: String, defaultOption: true },
30
31
  { name: 'port', alias: 'p', type: String, defaultValue: '4000' },
31
32
  { name: 'concurrency', type: Number, defaultValue: 4 },
@@ -42,7 +43,8 @@ const optionDefinitions = [
42
43
  { name: 'log-format', type: String, defaultValue: 'console' },
43
44
  { name: 'debug', type: Boolean, defaultValue: false },
44
45
  { name: 'otlp-debug', type: Boolean, defaultValue: false },
45
- { name: 'start-action-server', type: Boolean, defaultValue: false }
46
+ { name: 'start-action-server', type: Boolean, defaultValue: false },
47
+ { name: 'worker', type: Number, defaultValue: 8 }
46
48
  ]
47
49
 
48
50
  const options = commandLineArgs(optionDefinitions, { partial: true })
@@ -88,7 +90,7 @@ for (const [id, config] of Object.entries(chainsConfig)) {
88
90
  console.debug('Starting Server', options)
89
91
 
90
92
  let server: any
91
- let baseService: ProcessorServiceImpl
93
+ let baseService: ProcessorServiceImpl | ServiceManager
92
94
  const loader = async () => {
93
95
  const m = await import(options.target)
94
96
  console.debug('Module loaded', m)
@@ -106,7 +108,13 @@ if (options['start-action-server']) {
106
108
  // .use(prometheusServerMiddleware())
107
109
  .use(openTelemetryServerMiddleware())
108
110
  .use(errorDetailsServerMiddleware)
109
- baseService = new ProcessorServiceImpl(loader, server.shutdown)
111
+
112
+ if (options.worker > 1) {
113
+ baseService = new ServiceManager(options, loader, server.shutdown)
114
+ } else {
115
+ baseService = new ProcessorServiceImpl(loader, server.shutdown)
116
+ }
117
+
110
118
  const service = new FullProcessorServiceImpl(baseService)
111
119
 
112
120
  server.add(ProcessorDefinition, service)
@@ -0,0 +1,263 @@
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
+ import { Piscina } from 'piscina'
6
+
7
+ import {
8
+ DataBinding,
9
+ DeepPartial,
10
+ Empty,
11
+ HandlerType,
12
+ PreparedData,
13
+ PreprocessStreamRequest,
14
+ ProcessBindingResponse,
15
+ ProcessBindingsRequest,
16
+ ProcessConfigRequest,
17
+ ProcessConfigResponse,
18
+ ProcessorServiceImplementation,
19
+ ProcessResult,
20
+ ProcessStreamRequest,
21
+ ProcessStreamResponse,
22
+ StartRequest
23
+ } from '@sentio/protos'
24
+
25
+ import { PluginManager } from './plugin.js'
26
+ import { errorString, mergeProcessResults } from './utils.js'
27
+ import { GLOBAL_CONFIG } from './global-config.js'
28
+
29
+ import { StoreContext } from './db-context.js'
30
+ import { Subject } from 'rxjs'
31
+
32
+ import { processMetrics } from './metrics.js'
33
+
34
+ const { process_binding_count, process_binding_time, process_binding_error } = processMetrics
35
+
36
+ ;(BigInt.prototype as any).toJSON = function () {
37
+ return this.toString()
38
+ }
39
+
40
+ export class ServiceManager implements ProcessorServiceImplementation {
41
+ private started = false
42
+ // When there is unhandled error, stop process and return unavailable error
43
+ unhandled: Error
44
+ // private processorConfig: ProcessConfigResponse
45
+ private readonly pool: Piscina<any, any>
46
+
47
+ constructor(
48
+ readonly options: any,
49
+ readonly loader: () => Promise<any>,
50
+ readonly shutdownHandler?: () => void
51
+ ) {
52
+ this.pool = new Piscina({
53
+ maxThreads: options.worker,
54
+ minThreads: options.worker,
55
+ filename: new URL('./service-worker.js', import.meta.url).href
56
+ })
57
+ }
58
+
59
+ async *preprocessBindingsStream(requests: AsyncIterable<PreprocessStreamRequest>, context: CallContext) {
60
+ throw new Error('not supported')
61
+ }
62
+
63
+ async getConfig(request: ProcessConfigRequest, context: CallContext): Promise<ProcessConfigResponse> {
64
+ if (!this.started) {
65
+ throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
66
+ }
67
+
68
+ // broadcast to all worker
69
+ const bc = new BroadcastChannel('configure_channel')
70
+ bc.postMessage(request)
71
+
72
+ // also configure the main thread
73
+ const newConfig = ProcessConfigResponse.fromPartial({})
74
+ await PluginManager.INSTANCE.configure(newConfig)
75
+ return newConfig
76
+ }
77
+
78
+ //
79
+ // async configure() {
80
+ // this.processorConfig = ProcessConfigResponse.fromPartial({})
81
+ // await PluginManager.INSTANCE.configure(this.processorConfig)
82
+ // }
83
+
84
+ async start(request: StartRequest, context: CallContext): Promise<Empty> {
85
+ if (this.started) {
86
+ return {}
87
+ }
88
+
89
+ try {
90
+ await this.loader()
91
+ } catch (e) {
92
+ throw new ServerError(Status.INVALID_ARGUMENT, 'Failed to load processor: ' + errorString(e))
93
+ }
94
+
95
+ // broadcast to all worker
96
+ const bc = new BroadcastChannel('start_channel')
97
+ bc.postMessage(request)
98
+
99
+ // also start the processor in main thread
100
+ await PluginManager.INSTANCE.start(request)
101
+ this.started = true
102
+ return {}
103
+ }
104
+
105
+ async stop(request: Empty, context: CallContext): Promise<Empty> {
106
+ console.log('Server Shutting down in 5 seconds')
107
+ if (this.shutdownHandler) {
108
+ setTimeout(this.shutdownHandler, 5000)
109
+ }
110
+ return {}
111
+ }
112
+
113
+ async processBindings(request: ProcessBindingsRequest, options?: CallContext): Promise<ProcessBindingResponse> {
114
+ const promises = []
115
+ for (const binding of request.bindings) {
116
+ const promise = this.processBinding(binding, undefined)
117
+ if (GLOBAL_CONFIG.execution.sequential) {
118
+ await promise
119
+ }
120
+ promises.push(promise)
121
+ }
122
+ let promise
123
+ try {
124
+ promise = await Promise.all(promises)
125
+ processMetrics.process_binding_count.add(request.bindings.length)
126
+ } catch (e) {
127
+ processMetrics.process_binding_error.add(request.bindings.length)
128
+ throw e
129
+ }
130
+ const result = mergeProcessResults(promise)
131
+
132
+ // let updated = false
133
+ // if (PluginManager.INSTANCE.stateDiff(this.processorConfig)) {
134
+ // await this.configure()
135
+ // updated = true
136
+ // }
137
+
138
+ return {
139
+ result
140
+ }
141
+ }
142
+
143
+ async processBinding(
144
+ request: DataBinding,
145
+ preparedData: PreparedData | undefined,
146
+ options?: CallContext
147
+ ): Promise<ProcessResult> {
148
+ if (!this.started) {
149
+ throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
150
+ }
151
+ if (this.unhandled) {
152
+ throw new RichServerError(
153
+ Status.UNAVAILABLE,
154
+ 'Unhandled exception/rejection in previous request: ' + errorString(this.unhandled),
155
+ [
156
+ DebugInfo.fromPartial({
157
+ detail: this.unhandled.message,
158
+ stackEntries: this.unhandled.stack?.split('\n')
159
+ })
160
+ ]
161
+ )
162
+ }
163
+
164
+ return await this.pool.run({ request, preparedData }, { name: 'processBinding' })
165
+ }
166
+
167
+ async *processBindingsStream(requests: AsyncIterable<ProcessStreamRequest>, context: CallContext) {
168
+ if (!this.started) {
169
+ throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
170
+ }
171
+
172
+ const subject = new Subject<DeepPartial<ProcessStreamResponse>>()
173
+ this.handleRequests(requests, subject)
174
+ .then(() => {
175
+ subject.complete()
176
+ })
177
+ .catch((e) => {
178
+ console.error(e)
179
+ subject.error(e)
180
+ })
181
+ yield* from(subject).pipe(withAbort(context.signal))
182
+ }
183
+
184
+ private async handleRequests(
185
+ requests: AsyncIterable<ProcessStreamRequest>,
186
+ subject: Subject<DeepPartial<ProcessStreamResponse>>
187
+ ) {
188
+ const contexts = new Contexts()
189
+ for await (const request of requests) {
190
+ try {
191
+ // console.debug('received request:', request)
192
+ if (request.binding) {
193
+ process_binding_count.add(1)
194
+
195
+ // Adjust binding will make some request become invalid by setting UNKNOWN HandlerType
196
+ // for older SDK version, so we just return empty result for them here
197
+ if (request.binding.handlerType === HandlerType.UNKNOWN) {
198
+ subject.next({
199
+ processId: request.processId,
200
+ result: ProcessResult.create()
201
+ })
202
+ continue
203
+ }
204
+
205
+ const binding = request.binding
206
+ // todo support db request
207
+ // const dbContext = contexts.new(request.processId, subject)
208
+ const start = Date.now()
209
+ await this.processBinding(binding, undefined)
210
+ .then(async (result) => {
211
+ // await all pending db requests
212
+ // await dbContext.awaitPendings()
213
+ subject.next({
214
+ result,
215
+ processId: request.processId
216
+ })
217
+ })
218
+ .catch((e) => {
219
+ console.debug(e)
220
+ // dbContext.error(request.processId, e)
221
+ process_binding_error.add(1)
222
+ })
223
+ .finally(() => {
224
+ const cost = Date.now() - start
225
+ process_binding_time.add(cost)
226
+ contexts.delete(request.processId)
227
+ })
228
+ }
229
+ if (request.dbResult) {
230
+ const dbContext = contexts.get(request.processId)
231
+ try {
232
+ dbContext?.result(request.dbResult)
233
+ } catch (e) {
234
+ subject.error(new Error('db result error, process should stop'))
235
+ }
236
+ }
237
+ } catch (e) {
238
+ // should not happen
239
+ console.error('unexpect error during handle loop', e)
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ class Contexts {
246
+ private contexts: Map<number, StoreContext> = new Map()
247
+
248
+ get(processId: number) {
249
+ return this.contexts.get(processId)
250
+ }
251
+
252
+ new(processId: number, subject: Subject<DeepPartial<ProcessStreamResponse>>) {
253
+ const context = new StoreContext(subject, processId)
254
+ this.contexts.set(processId, context)
255
+ return context
256
+ }
257
+
258
+ delete(processId: number) {
259
+ const context = this.get(processId)
260
+ context?.close()
261
+ this.contexts.delete(processId)
262
+ }
263
+ }
@@ -0,0 +1,116 @@
1
+ import {
2
+ DataBinding,
3
+ Empty,
4
+ HandlerType,
5
+ PreparedData,
6
+ ProcessConfigRequest,
7
+ ProcessConfigResponse,
8
+ ProcessResult,
9
+ StartRequest
10
+ } from '@sentio/protos'
11
+ import { CallContext, ServerError, Status } from 'nice-grpc'
12
+ import { PluginManager } from './plugin.js'
13
+ import commandLineArgs from 'command-line-args'
14
+ import { optionDefinitions } from './processor-runner.js'
15
+ import { errorString } from './utils.js'
16
+ import { freezeGlobalConfig } from './global-config.js'
17
+ import { DebugInfo, RichServerError } from 'nice-grpc-error-details'
18
+ import { recordRuntimeInfo } from './service.js'
19
+ import { BroadcastChannel } from 'worker_threads'
20
+
21
+ let started = false
22
+ const options = commandLineArgs(optionDefinitions, { partial: true })
23
+
24
+ let unhandled: Error | undefined
25
+
26
+ process
27
+ .on('uncaughtException', (err) => {
28
+ console.error('Uncaught Exception, please checking if await is properly used', err)
29
+ unhandled = err
30
+ })
31
+ .on('unhandledRejection', (reason, p) => {
32
+ // @ts-ignore ignore invalid ens error
33
+ if (reason?.message.startsWith('invalid ENS name (disallowed character: "*"')) {
34
+ return
35
+ }
36
+ console.error('Unhandled Rejection, please checking if await is properly', reason)
37
+ unhandled = reason as Error
38
+ // shutdownServers(1)
39
+ })
40
+
41
+ async function getConfig(request: ProcessConfigRequest, context?: CallContext): Promise<ProcessConfigResponse> {
42
+ if (!started) {
43
+ throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
44
+ }
45
+
46
+ const newConfig = ProcessConfigResponse.fromPartial({})
47
+ await PluginManager.INSTANCE.configure(newConfig)
48
+ return newConfig
49
+ }
50
+
51
+ const loader = async () => {
52
+ const m = await import(options.target)
53
+ console.debug('Module loaded', m)
54
+ return m
55
+ }
56
+
57
+ const configureChannel = new BroadcastChannel('configure_channel')
58
+ configureChannel.onmessage = (request: ProcessConfigRequest) => {
59
+ getConfig(request)
60
+ }
61
+
62
+ async function start(request: StartRequest): Promise<Empty> {
63
+ if (started) {
64
+ return {}
65
+ }
66
+ freezeGlobalConfig()
67
+
68
+ try {
69
+ await loader()
70
+ } catch (e) {
71
+ throw new ServerError(Status.INVALID_ARGUMENT, 'Failed to load processor: ' + errorString(e))
72
+ }
73
+
74
+ await PluginManager.INSTANCE.start(request)
75
+ started = true
76
+ return {}
77
+ }
78
+
79
+ const startChannel = new BroadcastChannel('start_channel')
80
+ startChannel.onmessage = (request: StartRequest) => {
81
+ start(request)
82
+ }
83
+
84
+ async function stop(request: Empty, context: CallContext): Promise<Empty> {
85
+ return {}
86
+ }
87
+
88
+ async function processBinding(
89
+ request: DataBinding,
90
+ preparedData: PreparedData | undefined,
91
+ options?: CallContext
92
+ ): Promise<ProcessResult> {
93
+ if (!started) {
94
+ throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
95
+ }
96
+ if (unhandled) {
97
+ throw new RichServerError(
98
+ Status.UNAVAILABLE,
99
+ 'Unhandled exception/rejection in previous request: ' + errorString(unhandled),
100
+ [
101
+ DebugInfo.fromPartial({
102
+ detail: unhandled.message,
103
+ stackEntries: unhandled.stack?.split('\n')
104
+ })
105
+ ]
106
+ )
107
+ }
108
+
109
+ const result = await PluginManager.INSTANCE.processBinding(
110
+ request,
111
+ preparedData
112
+ // PluginManager.INSTANCE.dbContextLocalStorage.getStore()
113
+ )
114
+ recordRuntimeInfo(result, request.handlerType)
115
+ return result
116
+ }
package/src/service.ts CHANGED
@@ -474,7 +474,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
474
474
  }
475
475
  }
476
476
 
477
- function recordRuntimeInfo(results: ProcessResult, handlerType: HandlerType) {
477
+ export function recordRuntimeInfo(results: ProcessResult, handlerType: HandlerType) {
478
478
  for (const list of [results.gauges, results.counters, results.events, results.exports]) {
479
479
  list.forEach((e) => {
480
480
  e.runtimeInfo = {