@sentio/runtime 2.57.12-rc.j → 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.
@@ -1,35 +1,27 @@
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'
1
+ import { CallContext } from 'nice-grpc'
5
2
  import { Piscina } from 'piscina'
6
-
7
3
  import {
8
4
  DataBinding,
5
+ DBRequest,
6
+ DBResponse,
9
7
  DeepPartial,
10
8
  Empty,
11
9
  HandlerType,
12
- PreparedData,
13
- PreprocessStreamRequest,
14
- ProcessBindingResponse,
15
- ProcessBindingsRequest,
16
10
  ProcessConfigRequest,
17
11
  ProcessConfigResponse,
18
- ProcessorServiceImplementation,
19
12
  ProcessResult,
20
13
  ProcessStreamRequest,
21
14
  ProcessStreamResponse,
22
15
  StartRequest
23
16
  } from '@sentio/protos'
24
17
 
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'
18
+ import { IStoreContext } from './db-context.js'
30
19
  import { Subject } from 'rxjs'
31
20
 
32
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'
33
25
 
34
26
  const { process_binding_count, process_binding_time, process_binding_error } = processMetrics
35
27
 
@@ -37,152 +29,64 @@ const { process_binding_count, process_binding_time, process_binding_error } = p
37
29
  return this.toString()
38
30
  }
39
31
 
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
32
+ export class ServiceManager extends ProcessorServiceImpl {
45
33
  private pool: Piscina<any, any>
46
-
47
34
  private workerData: any = {}
48
35
 
49
36
  constructor(
50
37
  readonly options: any,
51
- readonly loader: () => Promise<any>,
52
- readonly shutdownHandler?: () => void
38
+ loader: () => Promise<any>,
39
+ shutdownHandler?: () => void
53
40
  ) {
41
+ super(loader, shutdownHandler)
54
42
  this.workerData.options = options
55
43
  }
56
44
 
57
- async *preprocessBindingsStream(requests: AsyncIterable<PreprocessStreamRequest>, context: CallContext) {
58
- throw new Error('not supported')
59
- }
60
-
61
45
  async getConfig(request: ProcessConfigRequest, context: CallContext): Promise<ProcessConfigResponse> {
62
- if (!this.started) {
63
- throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
64
- }
46
+ const newConfig = await super.getConfig(request, context)
65
47
 
66
- // broadcast to all worker
67
- const bc = new BroadcastChannel('configure_channel')
68
- bc.postMessage(request)
69
-
70
- // also configure the main thread
71
- const newConfig = ProcessConfigResponse.fromPartial({})
72
- await PluginManager.INSTANCE.configure(newConfig)
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
+ }
73
54
 
74
55
  this.workerData.configRequest = request
56
+
57
+ // if pool is initialized, this will trigger restart of all workers
58
+ await this.initPool()
75
59
  return newConfig
76
60
  }
77
61
 
78
62
  async start(request: StartRequest, context: CallContext): Promise<Empty> {
79
- if (this.started) {
80
- return {}
81
- }
82
-
83
- try {
84
- await this.loader()
85
- } catch (e) {
86
- throw new ServerError(Status.INVALID_ARGUMENT, 'Failed to load processor: ' + errorString(e))
87
- }
88
-
89
- // also start the processor in main thread
90
- await PluginManager.INSTANCE.start(request)
91
- this.started = true
63
+ await super.start(request, context)
92
64
  this.workerData.startRequest = request
93
65
  return {}
94
66
  }
95
67
 
96
68
  async stop(request: Empty, context: CallContext): Promise<Empty> {
97
- await this.pool.destroy()
98
- if (this.shutdownHandler) {
99
- console.log('Server Shutting down in 5 seconds')
100
- setTimeout(this.shutdownHandler, 5000)
101
- }
102
- return {}
69
+ await this.pool?.destroy()
70
+ return await super.stop(request, context)
103
71
  }
104
72
 
105
- async processBindings(request: ProcessBindingsRequest, options?: CallContext): Promise<ProcessBindingResponse> {
106
- const promises = []
107
- for (const binding of request.bindings) {
108
- const promise = this.processBinding(binding)
109
- if (GLOBAL_CONFIG.execution.sequential) {
110
- await promise
111
- }
112
- promises.push(promise)
113
- }
114
- let promise
115
- try {
116
- promise = await Promise.all(promises)
117
- processMetrics.process_binding_count.add(request.bindings.length)
118
- } catch (e) {
119
- processMetrics.process_binding_error.add(request.bindings.length)
120
- throw e
121
- }
122
- const result = mergeProcessResults(promise)
123
-
124
- // let updated = false
125
- // if (PluginManager.INSTANCE.stateDiff(this.processorConfig)) {
126
- // await this.configure()
127
- // updated = true
128
- // }
129
-
130
- return {
131
- result
132
- }
133
- }
134
-
135
- async processBinding(request: DataBinding): Promise<ProcessResult> {
136
- if (!this.started) {
137
- throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
138
- }
73
+ async process(request: DataBinding, dbContext?: ChannelStoreContext): Promise<ProcessResult> {
139
74
  if (!this.pool) {
140
- this.pool = new Piscina({
141
- maxThreads: this.options.worker,
142
- minThreads: this.options.worker,
143
- filename: new URL('./service-worker.js', import.meta.url).href.replaceAll('runtime/src', 'runtime/lib'),
144
- argv: process.argv,
145
- workerData: this.workerData
146
- })
147
- }
148
- if (this.unhandled) {
149
- throw new RichServerError(
150
- Status.UNAVAILABLE,
151
- 'Unhandled exception/rejection in previous request: ' + errorString(this.unhandled),
152
- [
153
- DebugInfo.fromPartial({
154
- detail: this.unhandled.message,
155
- stackEntries: this.unhandled.stack?.split('\n')
156
- })
157
- ]
158
- )
75
+ await this.initPool()
159
76
  }
160
77
 
161
- return this.pool.run({ request })
78
+ return this.pool.run(
79
+ { request, workerPort: dbContext?.workerPort },
80
+ { transferList: dbContext?.workerPort ? [dbContext?.workerPort] : [] }
81
+ )
162
82
  }
163
83
 
164
- async *processBindingsStream(requests: AsyncIterable<ProcessStreamRequest>, context: CallContext) {
165
- if (!this.started) {
166
- throw new ServerError(Status.UNAVAILABLE, 'Service Not started.')
167
- }
84
+ private readonly contexts = new Contexts()
168
85
 
169
- const subject = new Subject<DeepPartial<ProcessStreamResponse>>()
170
- this.handleRequests(requests, subject)
171
- .then(() => {
172
- subject.complete()
173
- })
174
- .catch((e) => {
175
- console.error(e)
176
- subject.error(e)
177
- })
178
- yield* from(subject).pipe(withAbort(context.signal))
179
- }
180
-
181
- private async handleRequests(
86
+ protected async handleRequests(
182
87
  requests: AsyncIterable<ProcessStreamRequest>,
183
88
  subject: Subject<DeepPartial<ProcessStreamResponse>>
184
89
  ) {
185
- const contexts = new Contexts()
186
90
  for await (const request of requests) {
187
91
  try {
188
92
  // console.debug('received request:', request)
@@ -200,13 +104,12 @@ export class ServiceManager implements ProcessorServiceImplementation {
200
104
  }
201
105
 
202
106
  const binding = request.binding
203
- // todo support db request
204
- const dbContext = contexts.new(request.processId, subject)
107
+
108
+ const dbContext = this.contexts.new(request.processId, subject)
109
+
205
110
  const start = Date.now()
206
- await this.processBinding(binding)
111
+ await this.process(binding, dbContext)
207
112
  .then(async (result) => {
208
- // await all pending db requests
209
- // await dbContext.awaitPendings()
210
113
  subject.next({
211
114
  result,
212
115
  processId: request.processId
@@ -219,11 +122,11 @@ export class ServiceManager implements ProcessorServiceImplementation {
219
122
  .finally(() => {
220
123
  const cost = Date.now() - start
221
124
  process_binding_time.add(cost)
222
- contexts.delete(request.processId)
125
+ this.contexts.delete(request.processId)
223
126
  })
224
127
  }
225
128
  if (request.dbResult) {
226
- const dbContext = contexts.get(request.processId)
129
+ const dbContext = this.contexts.get(request.processId)
227
130
  try {
228
131
  dbContext?.result(request.dbResult)
229
132
  } catch (e) {
@@ -236,17 +139,38 @@ export class ServiceManager implements ProcessorServiceImplementation {
236
139
  }
237
140
  }
238
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
+ }
239
161
  }
240
162
 
163
+ export type WorkerMessage = DBRequest & { processId: number }
164
+
241
165
  class Contexts {
242
- private contexts: Map<number, StoreContext> = new Map()
166
+ private contexts: Map<number, ChannelStoreContext> = new Map()
243
167
 
244
168
  get(processId: number) {
245
169
  return this.contexts.get(processId)
246
170
  }
247
171
 
248
172
  new(processId: number, subject: Subject<DeepPartial<ProcessStreamResponse>>) {
249
- const context = new StoreContext(subject, processId)
173
+ const context = new ChannelStoreContext(subject, processId)
250
174
  this.contexts.set(processId, context)
251
175
  return context
252
176
  }
@@ -257,3 +181,52 @@ class Contexts {
257
181
  this.contexts.delete(processId)
258
182
  }
259
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
+ }
@@ -1,13 +1,24 @@
1
- import { DataBinding, Empty, ProcessConfigRequest, ProcessConfigResponse, StartRequest } from '@sentio/protos'
1
+ import {
2
+ DataBinding,
3
+ DBResponse,
4
+ DeepPartial,
5
+ Empty,
6
+ ProcessConfigRequest,
7
+ ProcessConfigResponse,
8
+ ProcessStreamResponse,
9
+ StartRequest
10
+ } from '@sentio/protos'
2
11
  import { CallContext, ServerError, Status } from 'nice-grpc'
3
12
  import { PluginManager } from './plugin.js'
4
13
  import { errorString } from './utils.js'
5
14
  import { freezeGlobalConfig } from './global-config.js'
6
15
  import { DebugInfo, RichServerError } from 'nice-grpc-error-details'
7
16
  import { recordRuntimeInfo } from './service.js'
8
- import { BroadcastChannel } from 'worker_threads'
17
+ import { BroadcastChannel, MessagePort, threadId } from 'worker_threads'
9
18
  import { Piscina } from 'piscina'
10
19
  import { configureEndpoints } from './endpoints.js'
20
+ import { setupLogger } from './logger.js'
21
+ import { AbstractStoreContext } from './db-context.js'
11
22
 
12
23
  let started = false
13
24
 
@@ -68,14 +79,25 @@ async function start(request: StartRequest, options: any): Promise<Empty> {
68
79
  return {}
69
80
  }
70
81
 
71
- export default async function ({ request }: { request: DataBinding }) {
82
+ export default async function ({
83
+ request,
84
+ processId,
85
+ workerPort
86
+ }: {
87
+ request: DataBinding
88
+ processId: number
89
+ workerPort?: MessagePort
90
+ }) {
72
91
  const { startRequest, configRequest, options } = Piscina.workerData
73
92
  if (!started) {
93
+ const logLevel = process.env['LOG_LEVEL']?.toUpperCase()
94
+ setupLogger(options['log-format'] === 'json', logLevel === 'debug' ? true : options.debug, threadId)
95
+
74
96
  configureEndpoints(options)
75
97
 
76
98
  if (startRequest) {
77
99
  await start(startRequest, options)
78
- console.debug('worker started')
100
+ console.debug('worker started, template instance:', startRequest.templateInstances?.length)
79
101
  }
80
102
 
81
103
  if (configRequest) {
@@ -85,13 +107,15 @@ export default async function ({ request }: { request: DataBinding }) {
85
107
  }
86
108
 
87
109
  if (unhandled) {
110
+ const err = unhandled
111
+ unhandled = undefined
88
112
  throw new RichServerError(
89
113
  Status.UNAVAILABLE,
90
- 'Unhandled exception/rejection in previous request: ' + errorString(unhandled),
114
+ 'Unhandled exception/rejection in previous request: ' + errorString(err),
91
115
  [
92
116
  DebugInfo.fromPartial({
93
- detail: unhandled.message,
94
- stackEntries: unhandled.stack?.split('\n')
117
+ detail: err.message,
118
+ stackEntries: err.stack?.split('\n')
95
119
  })
96
120
  ]
97
121
  )
@@ -99,9 +123,28 @@ export default async function ({ request }: { request: DataBinding }) {
99
123
 
100
124
  const result = await PluginManager.INSTANCE.processBinding(
101
125
  request,
102
- undefined
103
- // PluginManager.INSTANCE.dbContextLocalStorage.getStore()
126
+ undefined,
127
+ workerPort ? new WorkerStoreContext(workerPort, processId) : undefined
104
128
  )
105
129
  recordRuntimeInfo(result, request.handlerType)
106
130
  return result
107
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
  ) {
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
+ }