@sentio/runtime 2.59.0-rc.4 → 2.59.0-rc.41

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.
@@ -1,30 +1,21 @@
1
1
  import { CallContext } from 'nice-grpc'
2
2
  import { Piscina } from 'piscina'
3
3
  import {
4
- DataBinding,
5
- DBRequest,
6
- DBResponse,
7
4
  DeepPartial,
8
5
  Empty,
9
- HandlerType,
10
6
  ProcessConfigRequest,
11
7
  ProcessConfigResponse,
12
8
  ProcessResult,
13
9
  ProcessStreamRequest,
14
10
  ProcessStreamResponse,
11
+ ProcessStreamResponse_Partitions,
15
12
  StartRequest
16
13
  } from '@sentio/protos'
17
-
18
- import { IStoreContext } from './db-context.js'
19
14
  import { Subject } from 'rxjs'
20
15
 
21
- import { processMetrics } from './metrics.js'
22
16
  import { MessageChannel } from 'node:worker_threads'
23
17
  import { ProcessorServiceImpl } from './service.js'
24
18
  import { TemplateInstanceState } from './state.js'
25
-
26
- const { process_binding_count, process_binding_time, process_binding_error } = processMetrics
27
-
28
19
  ;(BigInt.prototype as any).toJSON = function () {
29
20
  return this.toString()
30
21
  }
@@ -34,11 +25,11 @@ export class ServiceManager extends ProcessorServiceImpl {
34
25
  private workerData: any = {}
35
26
 
36
27
  constructor(
37
- readonly options: any,
38
28
  loader: () => Promise<any>,
29
+ readonly options: any,
39
30
  shutdownHandler?: () => void
40
31
  ) {
41
- super(loader, shutdownHandler)
32
+ super(loader, options, shutdownHandler)
42
33
  this.workerData.options = options
43
34
  }
44
35
 
@@ -70,76 +61,56 @@ export class ServiceManager extends ProcessorServiceImpl {
70
61
  return await super.stop(request, context)
71
62
  }
72
63
 
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
64
  private readonly contexts = new Contexts()
85
65
 
86
66
  protected async handleRequests(
87
67
  requests: AsyncIterable<ProcessStreamRequest>,
88
68
  subject: Subject<DeepPartial<ProcessStreamResponse>>
89
69
  ) {
70
+ if (!this.pool) {
71
+ await this.initPool()
72
+ }
90
73
  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
- 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
- }
74
+ this.handleSingleRequest(request, subject)
75
+ }
76
+ }
77
+
78
+ async handleSingleRequest(request: ProcessStreamRequest, subject: Subject<DeepPartial<ProcessStreamResponse>>) {
79
+ const processId = request.processId
80
+ if (request.binding) {
81
+ const context = this.contexts.new(processId)
82
+ context.mainPort.on('message', (resp: ProcessStreamResponse) => {
83
+ subject.next(resp)
84
+ if (resp.result) {
85
+ // last response
86
+ this.contexts.delete(processId)
135
87
  }
136
- } catch (e) {
137
- // should not happen
138
- console.error('unexpect error during handle loop', e)
88
+ })
89
+ await this.pool.run(
90
+ { request, workerPort: context.workerPort, processId },
91
+ { transferList: [context.workerPort] }
92
+ )
93
+ } else {
94
+ const context = this.contexts.get(processId)
95
+ if (!context) {
96
+ console.error('No context found for processId:', processId)
97
+ throw new Error(`No context found for processId: ${processId}`)
139
98
  }
99
+ context.sendRequest(request)
140
100
  }
141
101
  }
142
102
 
103
+ async process(processId: number, context: ChannelContext): Promise<ProcessResult | ProcessStreamResponse_Partitions> {
104
+ if (!this.pool) {
105
+ await this.initPool()
106
+ }
107
+
108
+ return this.pool.run(
109
+ { workerPort: context?.workerPort, processId },
110
+ { transferList: context?.workerPort ? [context?.workerPort] : [] }
111
+ )
112
+ }
113
+
143
114
  private async initPool() {
144
115
  if (this.pool) {
145
116
  await this.pool.close()
@@ -161,17 +132,19 @@ export class ServiceManager extends ProcessorServiceImpl {
161
132
  }
162
133
  }
163
134
 
164
- export type WorkerMessage = DBRequest & { processId: number }
165
-
166
135
  class Contexts {
167
- private contexts: Map<number, ChannelStoreContext> = new Map()
136
+ private contexts: Map<number, ChannelContext> = new Map()
168
137
 
169
138
  get(processId: number) {
170
139
  return this.contexts.get(processId)
171
140
  }
172
141
 
173
- new(processId: number, subject: Subject<DeepPartial<ProcessStreamResponse>>) {
174
- const context = new ChannelStoreContext(subject, processId)
142
+ new(processId: number) {
143
+ let context = this.get(processId)
144
+ if (context) {
145
+ return context
146
+ }
147
+ context = new ChannelContext(processId)
175
148
  this.contexts.set(processId, context)
176
149
  return context
177
150
  }
@@ -181,25 +154,19 @@ class Contexts {
181
154
  context?.close()
182
155
  this.contexts.delete(processId)
183
156
  }
157
+
158
+ has(processId: number) {
159
+ return this.contexts.has(processId)
160
+ }
184
161
  }
185
162
 
186
- export class ChannelStoreContext implements IStoreContext {
163
+ export class ChannelContext {
187
164
  channel = new MessageChannel()
188
165
 
189
- constructor(
190
- readonly subject: Subject<DeepPartial<ProcessStreamResponse>>,
191
- readonly processId: number
192
- ) {
193
- this.mainPort.on('message', (req: ProcessStreamRequest) => {
194
- subject.next({
195
- ...req,
196
- processId: processId
197
- })
198
- })
199
- }
166
+ constructor(readonly processId: number) {}
200
167
 
201
- sendRequest(request: DeepPartial<Omit<DBRequest, 'opId'>>, timeoutSecs?: number): Promise<DBResponse> {
202
- throw new Error('should not be used on main thread')
168
+ sendRequest(request: ProcessStreamRequest) {
169
+ this.mainPort.postMessage(request)
203
170
  }
204
171
 
205
172
  get workerPort() {
@@ -210,24 +177,7 @@ export class ChannelStoreContext implements IStoreContext {
210
177
  return this.channel.port1
211
178
  }
212
179
 
213
- result(dbResult: DBResponse) {
214
- this.mainPort.postMessage(dbResult)
215
- }
216
-
217
180
  close(): void {
218
181
  this.mainPort.close()
219
182
  }
220
-
221
- error(processId: number, e: any): void {
222
- console.error('process error', processId, e)
223
- const errorResult = ProcessResult.create({
224
- states: {
225
- error: e?.toString()
226
- }
227
- })
228
- this.subject.next({
229
- result: errorResult,
230
- processId
231
- })
232
- }
233
183
  }
@@ -1,24 +1,21 @@
1
1
  import {
2
- DataBinding,
3
- DBResponse,
4
2
  DeepPartial,
5
3
  Empty,
6
4
  ProcessConfigRequest,
7
- ProcessConfigResponse,
5
+ ProcessStreamRequest,
8
6
  ProcessStreamResponse,
9
7
  StartRequest
10
8
  } from '@sentio/protos'
11
9
  import { CallContext, ServerError, Status } from 'nice-grpc'
12
- import { PluginManager } from './plugin.js'
13
10
  import { errorString } from './utils.js'
14
11
  import { freezeGlobalConfig } from './global-config.js'
15
12
  import { DebugInfo, RichServerError } from 'nice-grpc-error-details'
16
- import { recordRuntimeInfo } from './service.js'
13
+ import { ProcessorServiceImpl } from './service.js'
17
14
  import { BroadcastChannel, MessagePort, threadId } from 'worker_threads'
18
15
  import { Piscina } from 'piscina'
19
16
  import { configureEndpoints } from './endpoints.js'
20
17
  import { setupLogger } from './logger.js'
21
- import { AbstractStoreContext } from './db-context.js'
18
+ import { Subject } from 'rxjs'
22
19
 
23
20
  let started = false
24
21
 
@@ -38,16 +35,11 @@ process
38
35
  unhandled = reason as Error
39
36
  // shutdownServers(1)
40
37
  })
38
+ .on('exit', () => {
39
+ console.info('Worker thread exiting, threadId:', threadId)
40
+ })
41
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
- }
42
+ let service: ProcessorServiceImpl | undefined
51
43
 
52
44
  const loader = async (options: any) => {
53
45
  if (options.target) {
@@ -59,9 +51,11 @@ const loader = async (options: any) => {
59
51
 
60
52
  const configureChannel = new BroadcastChannel('configure_channel')
61
53
  configureChannel.onmessage = (request: ProcessConfigRequest) => {
62
- getConfig(request)
54
+ service?.getConfig(request, emptyCallContext)
63
55
  }
64
56
 
57
+ const emptyCallContext = <CallContext>{}
58
+
65
59
  async function start(request: StartRequest, options: any): Promise<Empty> {
66
60
  if (started) {
67
61
  return {}
@@ -69,24 +63,24 @@ async function start(request: StartRequest, options: any): Promise<Empty> {
69
63
  freezeGlobalConfig()
70
64
 
71
65
  try {
72
- await loader(options)
66
+ service = new ProcessorServiceImpl(() => loader(options), options)
73
67
  } catch (e) {
74
68
  throw new ServerError(Status.INVALID_ARGUMENT, 'Failed to load processor: ' + errorString(e))
75
69
  }
76
70
 
77
- await PluginManager.INSTANCE.start(request)
71
+ await service.start(request, emptyCallContext)
78
72
  started = true
79
73
  return {}
80
74
  }
81
75
 
82
76
  export default async function ({
83
- request,
84
77
  processId,
78
+ request: firstRequest,
85
79
  workerPort
86
80
  }: {
87
- request: DataBinding
88
81
  processId: number
89
- workerPort?: MessagePort
82
+ request: ProcessStreamRequest
83
+ workerPort: MessagePort
90
84
  }) {
91
85
  const { startRequest, configRequest, options } = Piscina.workerData
92
86
  if (!started) {
@@ -97,18 +91,19 @@ export default async function ({
97
91
 
98
92
  if (startRequest) {
99
93
  await start(startRequest, options)
100
- console.debug('worker started, template instance:', startRequest.templateInstances?.length)
94
+ console.debug('worker', threadId, ' started, template instance:', startRequest.templateInstances?.length)
101
95
  }
102
96
 
103
97
  if (configRequest) {
104
- await getConfig(configRequest)
105
- console.debug('worker configured')
98
+ await service?.getConfig(configRequest, emptyCallContext)
99
+ console.debug('worker', threadId, ' configured')
106
100
  }
107
101
  }
108
102
 
109
103
  if (unhandled) {
110
104
  const err = unhandled
111
105
  unhandled = undefined
106
+ console.error('Unhandled exception/rejection in previous request:', err)
112
107
  throw new RichServerError(
113
108
  Status.UNAVAILABLE,
114
109
  'Unhandled exception/rejection in previous request: ' + errorString(err),
@@ -121,30 +116,20 @@ export default async function ({
121
116
  )
122
117
  }
123
118
 
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)
119
+ await new Promise<void>((resolve) => {
120
+ const subject = new Subject<DeepPartial<ProcessStreamResponse>>()
121
+ subject.subscribe((resp: ProcessStreamResponse) => {
122
+ workerPort.postMessage(resp)
123
+ // receive the response from the processor , close and resolve the promise
124
+ if (resp.result) {
125
+ resolve()
126
+ workerPort.close()
127
+ }
141
128
  })
142
- this.port.on('close', () => {
143
- this.close()
129
+ workerPort.on('message', (msg: ProcessStreamRequest) => {
130
+ const request = msg as ProcessStreamRequest
131
+ service?.handleRequest(request, firstRequest.binding, subject)
144
132
  })
145
- }
146
-
147
- doSend(req: DeepPartial<ProcessStreamResponse>): void {
148
- this.port.postMessage(req)
149
- }
133
+ service?.handleRequest(firstRequest, firstRequest.binding, subject)
134
+ })
150
135
  }
package/src/service.ts CHANGED
@@ -56,14 +56,17 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
56
56
  private readonly enablePreprocess: boolean
57
57
 
58
58
  private preparedData: PreparedData | undefined
59
+ readonly enablePartition: boolean
59
60
 
60
- constructor(loader: () => Promise<any>, shutdownHandler?: () => void) {
61
+ constructor(loader: () => Promise<any>, options?: any, shutdownHandler?: () => void) {
61
62
  this.loader = loader
62
63
  this.shutdownHandler = shutdownHandler
63
64
 
64
65
  this.enablePreprocess = process.env['ENABLE_PREPROCESS']
65
66
  ? process.env['ENABLE_PREPROCESS'].toLowerCase() == 'true'
66
67
  : false
68
+
69
+ this.enablePartition = options?.['enable-partition'] == true
67
70
  }
68
71
 
69
72
  async getConfig(request: ProcessConfigRequest, context: CallContext): Promise<ProcessConfigResponse> {
@@ -413,65 +416,105 @@ export class ProcessorServiceImpl implements ProcessorServiceImplementation {
413
416
  yield* from(subject).pipe(withAbort(context.signal))
414
417
  }
415
418
 
419
+ private dbContexts = new Contexts()
420
+
416
421
  protected async handleRequests(
417
422
  requests: AsyncIterable<ProcessStreamRequest>,
418
423
  subject: Subject<DeepPartial<ProcessStreamResponse>>
419
424
  ) {
420
- const contexts = new Contexts()
425
+ let lastBinding: DataBinding | undefined = undefined
421
426
  for await (const request of requests) {
422
427
  try {
423
- // console.debug('received request:', request)
428
+ // console.log('received request:', request, 'lastBinding:', lastBinding)
424
429
  if (request.binding) {
425
- process_binding_count.add(1)
426
-
427
- // Adjust binding will make some request become invalid by setting UNKNOWN HandlerType
428
- // for older SDK version, so we just return empty result for them here
429
- if (request.binding.handlerType === HandlerType.UNKNOWN) {
430
- subject.next({
431
- processId: request.processId,
432
- result: ProcessResult.create()
433
- })
434
- continue
435
- }
436
-
437
- const binding = request.binding
438
- const dbContext = contexts.new(request.processId, subject)
439
- const start = Date.now()
440
- PluginManager.INSTANCE.processBinding(binding, this.preparedData, dbContext)
441
- .then(async (result) => {
442
- // await all pending db requests
443
- await dbContext.awaitPendings()
444
- subject.next({
445
- result,
446
- processId: request.processId
447
- })
448
- recordRuntimeInfo(result, binding.handlerType)
449
- })
450
- .catch((e) => {
451
- console.debug(e)
452
- dbContext.error(request.processId, e)
453
- process_binding_error.add(1)
454
- })
455
- .finally(() => {
456
- const cost = Date.now() - start
457
- process_binding_time.add(cost)
458
- contexts.delete(request.processId)
459
- })
460
- }
461
- if (request.dbResult) {
462
- const dbContext = contexts.get(request.processId)
463
- try {
464
- dbContext?.result(request.dbResult)
465
- } catch (e) {
466
- subject.error(new Error('db result error, process should stop'))
467
- }
430
+ lastBinding = request.binding
468
431
  }
432
+ this.handleRequest(request, lastBinding, subject)
469
433
  } catch (e) {
470
434
  // should not happen
471
435
  console.error('unexpect error during handle loop', e)
472
436
  }
473
437
  }
474
438
  }
439
+
440
+ async handleRequest(
441
+ request: ProcessStreamRequest,
442
+ lastBinding: DataBinding | undefined,
443
+ subject: Subject<DeepPartial<ProcessStreamResponse>>
444
+ ) {
445
+ if (request.binding) {
446
+ process_binding_count.add(1)
447
+
448
+ // Adjust binding will make some request become invalid by setting UNKNOWN HandlerType
449
+ // for older SDK version, so we just return empty result for them here
450
+ if (request.binding.handlerType === HandlerType.UNKNOWN) {
451
+ subject.next({
452
+ processId: request.processId,
453
+ result: ProcessResult.create()
454
+ })
455
+ return
456
+ }
457
+
458
+ if (this.enablePartition) {
459
+ try {
460
+ const partitions = await PluginManager.INSTANCE.partition(request.binding)
461
+ subject.next({
462
+ processId: request.processId,
463
+ partitions
464
+ })
465
+ } catch (e) {
466
+ console.error('Partition error:', e)
467
+ subject.error(new Error('Partition error: ' + errorString(e)))
468
+ return
469
+ }
470
+ } else {
471
+ this.startProcess(request.processId, request.binding, subject)
472
+ }
473
+ }
474
+
475
+ if (request.start) {
476
+ if (!lastBinding) {
477
+ console.error('start request received without binding')
478
+ subject.error(new Error('start request received without binding'))
479
+ return
480
+ }
481
+ this.startProcess(request.processId, lastBinding, subject)
482
+ }
483
+
484
+ if (request.dbResult) {
485
+ const dbContext = this.dbContexts.get(request.processId)
486
+ try {
487
+ dbContext?.result(request.dbResult)
488
+ } catch (e) {
489
+ subject.error(new Error('db result error, process should stop'))
490
+ }
491
+ }
492
+ }
493
+
494
+ private startProcess(processId: number, binding: DataBinding, subject: Subject<DeepPartial<ProcessStreamResponse>>) {
495
+ const dbContext = this.dbContexts.new(processId, subject)
496
+ const start = Date.now()
497
+ PluginManager.INSTANCE.processBinding(binding, this.preparedData, dbContext)
498
+ .then(async (result) => {
499
+ // await all pending db requests
500
+ await dbContext.awaitPendings()
501
+ subject.next({
502
+ result,
503
+ processId: processId
504
+ })
505
+ recordRuntimeInfo(result, binding.handlerType)
506
+ })
507
+ .catch((e) => {
508
+ console.error(e)
509
+ dbContext.error(processId, e)
510
+ process_binding_error.add(1)
511
+ })
512
+ .finally(() => {
513
+ const cost = Date.now() - start
514
+ process_binding_time.add(cost)
515
+ this.dbContexts.delete(processId)
516
+ })
517
+ }
475
518
  }
476
519
 
477
520
  export function recordRuntimeInfo(results: ProcessResult, handlerType: HandlerType) {
package/src/utils.ts CHANGED
@@ -10,12 +10,22 @@ export function mergeProcessResults(results: ProcessResult[]): Required<ProcessR
10
10
  ...ProcessResultFull.create(),
11
11
  states: StateResult.create()
12
12
  }
13
+ return mergeProcessResultsInPlace(res, results)
14
+ }
13
15
 
16
+ export function mergeProcessResultsInPlace(
17
+ res: ProcessResult,
18
+ results: ProcessResult[]
19
+ ): Required<ProcessResult, 'states'> {
20
+ res.states = res.states || StateResult.create()
14
21
  for (const r of results) {
15
- res.counters = res.counters.concat(r.counters)
16
- res.gauges = res.gauges.concat(r.gauges)
17
- res.events = res.events.concat(r.events)
18
- res.exports = res.exports.concat(r.exports)
22
+ // not using spread operator since it puts all element on the stack
23
+ // cause maximum call stack size exceeded error if it's a large array
24
+ mergeArrayInPlace(res.counters, r.counters)
25
+ mergeArrayInPlace(res.gauges, r.gauges)
26
+ mergeArrayInPlace(res.events, r.events)
27
+ mergeArrayInPlace(res.exports, r.exports)
28
+ mergeArrayInPlace(res.timeseriesResult, r.timeseriesResult)
19
29
  res.states = {
20
30
  configUpdated: res.states?.configUpdated || r.states?.configUpdated || false
21
31
  }
@@ -23,6 +33,12 @@ export function mergeProcessResults(results: ProcessResult[]): Required<ProcessR
23
33
  return res
24
34
  }
25
35
 
36
+ function mergeArrayInPlace<T>(dst: T[], src: T[]) {
37
+ for (const r of src) {
38
+ dst.push(r)
39
+ }
40
+ }
41
+
26
42
  export function errorString(e: Error): string {
27
43
  return e.message + '\n' + e.stack
28
44
  }