@sentio/runtime 2.57.12 → 2.57.13-rc.1

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.
@@ -0,0 +1,232 @@
1
+ import { CallContext } from 'nice-grpc'
2
+ import { Piscina } from 'piscina'
3
+ import {
4
+ DataBinding,
5
+ DBRequest,
6
+ DBResponse,
7
+ DeepPartial,
8
+ Empty,
9
+ HandlerType,
10
+ ProcessConfigRequest,
11
+ ProcessConfigResponse,
12
+ ProcessResult,
13
+ ProcessStreamRequest,
14
+ ProcessStreamResponse,
15
+ StartRequest
16
+ } from '@sentio/protos'
17
+
18
+ import { IStoreContext } from './db-context.js'
19
+ import { Subject } from 'rxjs'
20
+
21
+ import { processMetrics } from './metrics.js'
22
+ import { MessageChannel } from 'node:worker_threads'
23
+ import { ProcessorServiceImpl } from './service.js'
24
+ import { TemplateInstanceState } from './state.js'
25
+
26
+ const { process_binding_count, process_binding_time, process_binding_error } = processMetrics
27
+
28
+ ;(BigInt.prototype as any).toJSON = function () {
29
+ return this.toString()
30
+ }
31
+
32
+ export class ServiceManager extends ProcessorServiceImpl {
33
+ private pool: Piscina<any, any>
34
+ private workerData: any = {}
35
+
36
+ constructor(
37
+ readonly options: any,
38
+ loader: () => Promise<any>,
39
+ shutdownHandler?: () => void
40
+ ) {
41
+ super(loader, shutdownHandler)
42
+ this.workerData.options = options
43
+ }
44
+
45
+ async getConfig(request: ProcessConfigRequest, context: CallContext): Promise<ProcessConfigResponse> {
46
+ const newConfig = await super.getConfig(request, context)
47
+
48
+ // check if templateInstances changed
49
+ if (newConfig.templateInstances?.length != this.workerData?.configRequest?.templateInstances?.length) {
50
+ this.workerData.startRequest = StartRequest.fromPartial({
51
+ templateInstances: newConfig.templateInstances
52
+ })
53
+ }
54
+
55
+ this.workerData.configRequest = request
56
+
57
+ // if pool is initialized, this will trigger restart of all workers
58
+ await this.initPool()
59
+ return newConfig
60
+ }
61
+
62
+ async start(request: StartRequest, context: CallContext): Promise<Empty> {
63
+ await super.start(request, context)
64
+ this.workerData.startRequest = request
65
+ return {}
66
+ }
67
+
68
+ async stop(request: Empty, context: CallContext): Promise<Empty> {
69
+ await this.pool?.destroy()
70
+ return await super.stop(request, context)
71
+ }
72
+
73
+ async process(request: DataBinding, dbContext?: ChannelStoreContext): Promise<ProcessResult> {
74
+ if (!this.pool) {
75
+ await this.initPool()
76
+ }
77
+
78
+ return this.pool.run(
79
+ { request, workerPort: dbContext?.workerPort },
80
+ { transferList: dbContext?.workerPort ? [dbContext?.workerPort] : [] }
81
+ )
82
+ }
83
+
84
+ private readonly contexts = new Contexts()
85
+
86
+ protected async handleRequests(
87
+ requests: AsyncIterable<ProcessStreamRequest>,
88
+ subject: Subject<DeepPartial<ProcessStreamResponse>>
89
+ ) {
90
+ for await (const request of requests) {
91
+ try {
92
+ // console.debug('received request:', request)
93
+ if (request.binding) {
94
+ process_binding_count.add(1)
95
+
96
+ // Adjust binding will make some request become invalid by setting UNKNOWN HandlerType
97
+ // for older SDK version, so we just return empty result for them here
98
+ if (request.binding.handlerType === HandlerType.UNKNOWN) {
99
+ subject.next({
100
+ processId: request.processId,
101
+ result: ProcessResult.create()
102
+ })
103
+ continue
104
+ }
105
+
106
+ const binding = request.binding
107
+
108
+ const dbContext = this.contexts.new(request.processId, subject)
109
+
110
+ const start = Date.now()
111
+ await this.process(binding, dbContext)
112
+ .then(async (result) => {
113
+ subject.next({
114
+ result,
115
+ processId: request.processId
116
+ })
117
+ })
118
+ .catch((e) => {
119
+ dbContext.error(request.processId, e)
120
+ process_binding_error.add(1)
121
+ })
122
+ .finally(() => {
123
+ const cost = Date.now() - start
124
+ process_binding_time.add(cost)
125
+ this.contexts.delete(request.processId)
126
+ })
127
+ }
128
+ if (request.dbResult) {
129
+ const dbContext = this.contexts.get(request.processId)
130
+ try {
131
+ dbContext?.result(request.dbResult)
132
+ } catch (e) {
133
+ subject.error(new Error('db result error, process should stop'))
134
+ }
135
+ }
136
+ } catch (e) {
137
+ // should not happen
138
+ console.error('unexpect error during handle loop', e)
139
+ }
140
+ }
141
+ }
142
+
143
+ private async initPool() {
144
+ if (this.pool) {
145
+ await this.pool.close()
146
+ }
147
+ this.pool = new Piscina({
148
+ maxThreads: this.options.worker,
149
+ minThreads: this.options.worker,
150
+ filename: new URL('./service-worker.js', import.meta.url).href.replaceAll('runtime/src', 'runtime/lib'),
151
+ argv: process.argv,
152
+ workerData: this.workerData
153
+ })
154
+ this.pool.on('message', (msg) => {
155
+ if (msg.event == 'add_template_instance') {
156
+ // sync the template state from worker to the main thread
157
+ TemplateInstanceState.INSTANCE.addValue(msg.value)
158
+ }
159
+ })
160
+ }
161
+ }
162
+
163
+ export type WorkerMessage = DBRequest & { processId: number }
164
+
165
+ class Contexts {
166
+ private contexts: Map<number, ChannelStoreContext> = new Map()
167
+
168
+ get(processId: number) {
169
+ return this.contexts.get(processId)
170
+ }
171
+
172
+ new(processId: number, subject: Subject<DeepPartial<ProcessStreamResponse>>) {
173
+ const context = new ChannelStoreContext(subject, processId)
174
+ this.contexts.set(processId, context)
175
+ return context
176
+ }
177
+
178
+ delete(processId: number) {
179
+ const context = this.get(processId)
180
+ context?.close()
181
+ this.contexts.delete(processId)
182
+ }
183
+ }
184
+
185
+ export class ChannelStoreContext implements IStoreContext {
186
+ channel = new MessageChannel()
187
+
188
+ constructor(
189
+ readonly subject: Subject<DeepPartial<ProcessStreamResponse>>,
190
+ readonly processId: number
191
+ ) {
192
+ this.mainPort.on('message', (req: ProcessStreamRequest) => {
193
+ subject.next({
194
+ ...req,
195
+ processId: processId
196
+ })
197
+ })
198
+ }
199
+
200
+ sendRequest(request: DeepPartial<Omit<DBRequest, 'opId'>>, timeoutSecs?: number): Promise<DBResponse> {
201
+ throw new Error('should not be used on main thread')
202
+ }
203
+
204
+ get workerPort() {
205
+ return this.channel.port2
206
+ }
207
+
208
+ get mainPort() {
209
+ return this.channel.port1
210
+ }
211
+
212
+ result(dbResult: DBResponse) {
213
+ this.mainPort.postMessage(dbResult)
214
+ }
215
+
216
+ close(): void {
217
+ this.mainPort.close()
218
+ }
219
+
220
+ error(processId: number, e: any): void {
221
+ console.error('process error', processId, e)
222
+ const errorResult = ProcessResult.create({
223
+ states: {
224
+ error: e?.toString()
225
+ }
226
+ })
227
+ this.subject.next({
228
+ result: errorResult,
229
+ processId
230
+ })
231
+ }
232
+ }
@@ -0,0 +1,150 @@
1
+ import {
2
+ DataBinding,
3
+ DBResponse,
4
+ DeepPartial,
5
+ Empty,
6
+ ProcessConfigRequest,
7
+ ProcessConfigResponse,
8
+ ProcessStreamResponse,
9
+ StartRequest
10
+ } from '@sentio/protos'
11
+ import { CallContext, ServerError, Status } from 'nice-grpc'
12
+ import { PluginManager } from './plugin.js'
13
+ import { errorString } from './utils.js'
14
+ import { freezeGlobalConfig } from './global-config.js'
15
+ import { DebugInfo, RichServerError } from 'nice-grpc-error-details'
16
+ import { recordRuntimeInfo } from './service.js'
17
+ import { BroadcastChannel, MessagePort, threadId } from 'worker_threads'
18
+ import { Piscina } from 'piscina'
19
+ import { configureEndpoints } from './endpoints.js'
20
+ import { setupLogger } from './logger.js'
21
+ import { AbstractStoreContext } from './db-context.js'
22
+
23
+ let started = false
24
+
25
+ let unhandled: Error | undefined
26
+
27
+ process
28
+ .on('uncaughtException', (err) => {
29
+ console.error('Uncaught Exception, please checking if await is properly used', err)
30
+ unhandled = err
31
+ })
32
+ .on('unhandledRejection', (reason, p) => {
33
+ // @ts-ignore ignore invalid ens error
34
+ if (reason?.message.startsWith('invalid ENS name (disallowed character: "*"')) {
35
+ return
36
+ }
37
+ console.error('Unhandled Rejection, please checking if await is properly', reason)
38
+ unhandled = reason as Error
39
+ // shutdownServers(1)
40
+ })
41
+
42
+ async function getConfig(request: ProcessConfigRequest, context?: CallContext): Promise<ProcessConfigResponse> {
43
+ if (!started) {
44
+ throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
45
+ }
46
+
47
+ const newConfig = ProcessConfigResponse.fromPartial({})
48
+ await PluginManager.INSTANCE.configure(newConfig)
49
+ return newConfig
50
+ }
51
+
52
+ const loader = async (options: any) => {
53
+ if (options.target) {
54
+ const m = await import(options.target)
55
+ console.debug('Module loaded, path:', options.target, 'module:', m)
56
+ return m
57
+ }
58
+ }
59
+
60
+ const configureChannel = new BroadcastChannel('configure_channel')
61
+ configureChannel.onmessage = (request: ProcessConfigRequest) => {
62
+ getConfig(request)
63
+ }
64
+
65
+ async function start(request: StartRequest, options: any): Promise<Empty> {
66
+ if (started) {
67
+ return {}
68
+ }
69
+ freezeGlobalConfig()
70
+
71
+ try {
72
+ await loader(options)
73
+ } catch (e) {
74
+ throw new ServerError(Status.INVALID_ARGUMENT, 'Failed to load processor: ' + errorString(e))
75
+ }
76
+
77
+ await PluginManager.INSTANCE.start(request)
78
+ started = true
79
+ return {}
80
+ }
81
+
82
+ export default async function ({
83
+ request,
84
+ processId,
85
+ workerPort
86
+ }: {
87
+ request: DataBinding
88
+ processId: number
89
+ workerPort?: MessagePort
90
+ }) {
91
+ const { startRequest, configRequest, options } = Piscina.workerData
92
+ if (!started) {
93
+ const logLevel = process.env['LOG_LEVEL']?.toUpperCase()
94
+ setupLogger(options['log-format'] === 'json', logLevel === 'debug' ? true : options.debug, threadId)
95
+
96
+ configureEndpoints(options)
97
+
98
+ if (startRequest) {
99
+ await start(startRequest, options)
100
+ console.debug('worker started, template instance:', startRequest.templateInstances?.length)
101
+ }
102
+
103
+ if (configRequest) {
104
+ await getConfig(configRequest)
105
+ console.debug('worker configured')
106
+ }
107
+ }
108
+
109
+ if (unhandled) {
110
+ const err = unhandled
111
+ unhandled = undefined
112
+ throw new RichServerError(
113
+ Status.UNAVAILABLE,
114
+ 'Unhandled exception/rejection in previous request: ' + errorString(err),
115
+ [
116
+ DebugInfo.fromPartial({
117
+ detail: err.message,
118
+ stackEntries: err.stack?.split('\n')
119
+ })
120
+ ]
121
+ )
122
+ }
123
+
124
+ const result = await PluginManager.INSTANCE.processBinding(
125
+ request,
126
+ undefined,
127
+ workerPort ? new WorkerStoreContext(workerPort, processId) : undefined
128
+ )
129
+ recordRuntimeInfo(result, request.handlerType)
130
+ return result
131
+ }
132
+
133
+ class WorkerStoreContext extends AbstractStoreContext {
134
+ constructor(
135
+ readonly port: MessagePort,
136
+ processId: number
137
+ ) {
138
+ super(processId)
139
+ this.port.on('message', (resp: DBResponse) => {
140
+ this.result(resp)
141
+ })
142
+ this.port.on('close', () => {
143
+ this.close()
144
+ })
145
+ }
146
+
147
+ doSend(req: DeepPartial<ProcessStreamResponse>): void {
148
+ this.port.postMessage(req)
149
+ }
150
+ }
package/src/service.ts CHANGED
@@ -413,7 +413,7 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
413
413
  yield* from(subject).pipe(withAbort(context.signal))
414
414
  }
415
415
 
416
- private async handleRequests(
416
+ protected async handleRequests(
417
417
  requests: AsyncIterable<ProcessStreamRequest>,
418
418
  subject: Subject<DeepPartial<ProcessStreamResponse>>
419
419
  ) {
@@ -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 = {
package/src/state.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { TemplateInstance } from '@sentio/protos'
2
+ import { isMainThread, parentPort, threadId } from 'node:worker_threads'
3
+
1
4
  export class State {
2
5
  stateMap = new Map<string, any>()
3
6
 
@@ -81,3 +84,19 @@ export abstract class ListStateStorage<T> extends StateStorage<T[]> {
81
84
  return value
82
85
  }
83
86
  }
87
+
88
+ export class TemplateInstanceState extends ListStateStorage<TemplateInstance> {
89
+ static INSTANCE = new TemplateInstanceState()
90
+
91
+ constructor() {
92
+ super()
93
+ }
94
+
95
+ override addValue(value: TemplateInstance): TemplateInstance {
96
+ if (!isMainThread) {
97
+ // I'm worker thread, should notice the main thread
98
+ parentPort?.postMessage({ event: 'add_template_instance', value, from: threadId })
99
+ }
100
+ return super.addValue(value)
101
+ }
102
+ }
@@ -6,11 +6,12 @@ export default defineConfig({
6
6
  js: `import { createRequire as createRequireShim } from 'module'; const require = createRequireShim(import.meta.url);`
7
7
  }
8
8
  },
9
- entry: ['src/index.ts', 'src/processor-runner.ts'],
9
+ entry: ['src/index.ts', 'src/processor-runner.ts', 'src/service-worker.ts', 'src/test-processor.test.ts'],
10
10
  outDir: 'lib',
11
11
  minify: process.env['BRANCH'] === 'release',
12
12
  sourcemap: true,
13
13
  clean: true,
14
14
  dts: true,
15
- format: 'esm'
15
+ format: 'esm',
16
+ external: [/^piscina.*$/]
16
17
  })