@outputai/core 0.12.0 → 0.12.1-next.1243f78.0

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.
@@ -10,8 +10,8 @@ import { init as initTracing } from '#tracing';
10
10
  import { webpackConfigHook } from './bundler_options.js';
11
11
  import { initInterceptors } from './interceptors/index.js';
12
12
  import { createChildLogger } from '#logger';
13
- import { setupInterruptionHandler } from './interruption.js';
14
- import { CatalogJob } from './catalog_workflow/catalog_job.js';
13
+ import { setupInterruptionHandler, KillSignError } from './interruption.js';
14
+ import { CatalogPublisher } from './catalog_workflow/catalog_publisher.js';
15
15
  import { mainEventBus } from '#bus';
16
16
  import { flushPendingHooks } from '#hooks/pending_hooks';
17
17
  import { BusEventType } from '#consts';
@@ -19,9 +19,10 @@ import { setupTelemetry } from './telemetry.js';
19
19
  import { TemporalConnectionMonitor } from './connection_monitor.js';
20
20
  import { bindGlobalFunctions } from './global_functions.js';
21
21
  import { setupClientConfig } from '#temporal/client';
22
- import { runOnce } from '#helpers/function';
23
22
  import { serializeError } from '#helpers/error_serializer';
24
23
  import { setupTemporalLogger } from './temporal_logger.js';
24
+ import { WorkerRunner } from './worker_runner.js';
25
+ import { shutdownServices } from './shutdown.js';
25
26
 
26
27
  import './log_hooks.js';
27
28
 
@@ -43,197 +44,138 @@ const {
43
44
  workerTuner
44
45
  } = configs;
45
46
 
46
- const state = {
47
+ const services = {
47
48
  connection: null,
48
49
  connectionMonitor: null,
49
- catalogJob: null,
50
- workerError: null
50
+ catalogPublisher: null,
51
+ workerRunner: null
51
52
  };
52
53
 
53
54
  // Get caller directory from command line arguments
54
55
  const callerDir = process.argv[2];
55
56
 
57
+ const abortController = new AbortController();
58
+ const { signal } = abortController;
59
+
60
+ setupInterruptionHandler( abortController );
61
+
62
+ /** Run a given function or abort code if global signal was aborted */
63
+ const run = cb => signal.throwIfAborted() ?? cb();
64
+
56
65
  const execute = async () => {
57
- log.info( 'Setup Temporal Logger' );
58
- setupTemporalLogger();
66
+ log.info( 'Setting up Temporal Logger...' );
67
+ run( setupTemporalLogger );
59
68
 
60
69
  log.info( 'Loading config...', { callerDir } );
61
- await loadHooks( callerDir );
70
+ await run( () => loadHooks( callerDir ) );
62
71
 
63
72
  log.info( 'Loading workflows...', { callerDir } );
64
- const { workflows, entrypoint: workflowsPath } = await loadWorkflows( callerDir );
73
+ const { workflows, entrypoint: workflowsPath } = await run( () => loadWorkflows( callerDir ) );
65
74
 
66
75
  log.info( 'Loading activities...', { callerDir } );
67
- const { activities } = await loadActivities( callerDir, workflows );
76
+ const { activities } = await run( () => loadActivities( callerDir, workflows ) );
68
77
 
69
78
  mainEventBus.emit( BusEventType.WORKER_BEFORE_START );
70
79
 
71
80
  log.info( 'Initializing tracing...' );
72
- await initTracing();
81
+ await run( initTracing );
73
82
 
74
83
  log.info( 'Creating workflows catalog...' );
75
- const catalog = createCatalog( { workflows, activities } );
84
+ const catalog = run( () => createCatalog( { workflows, activities } ) );
76
85
 
77
86
  log.info( 'Computing catalog source code hash...' );
78
- const catalogHash = await hashSourceCode( callerDir );
87
+ const catalogHash = await run( () => hashSourceCode( callerDir ) );
88
+
89
+ log.info( 'Binding globals...' );
90
+ run( bindGlobalFunctions );
79
91
 
80
92
  log.info( 'Connecting Temporal...' );
81
93
  const proxy = grpcProxy ? { type: 'http-connect', targetHost: grpcProxy } : undefined;
82
94
  if ( proxy ) {
83
95
  log.info( 'Using gRPC proxy', { targetHost: grpcProxy } );
84
96
  }
97
+ const connection = await run( () => NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } ) );
98
+ services.connection = connection;
85
99
 
86
- bindGlobalFunctions();
100
+ log.info( 'Setting up temporal endpoint...' );
101
+ run( () => setupClientConfig( { connection, namespace } ) );
87
102
 
88
- state.connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
89
-
90
- // add configs for the /temporal endpoint
91
- setupClientConfig( { connection: state.connection, namespace } );
103
+ log.info( 'Creating catalog publisher...' );
104
+ services.catalogPublisher = run( () => new CatalogPublisher( { connection, namespace, catalog, catalogHash, signal } ) );
92
105
 
93
106
  log.info( 'Creating connection monitor...' );
94
- state.connectionMonitor = new TemporalConnectionMonitor( state.connection );
95
-
96
- log.info( 'Creating catalog job manager...' );
97
- state.catalogJob = new CatalogJob( { connection: state.connection, namespace, catalog, catalogHash } );
98
-
99
- log.info( 'Creating worker...' );
100
- if ( workerTuner ) {
101
- log.info( 'Using worker tuner options', { ...workerTuner } );
102
- }
103
- const worker = await Worker.create( {
104
- connection: state.connection,
105
- namespace,
106
- taskQueue,
107
- workflowsPath,
108
- activities,
109
- sinks,
110
- interceptors: initInterceptors( { activities, workflows } ),
111
- // tuner isn't compatible with concurrent task executions configs
112
- ...( workerTuner ? {
113
- tuner: workerTuner
114
- } : {
115
- maxConcurrentWorkflowTaskExecutions,
116
- maxConcurrentActivityTaskExecutions
117
- } ),
118
- maxCachedWorkflows,
119
- maxConcurrentActivityTaskPolls,
120
- maxConcurrentWorkflowTaskPolls,
121
- bundlerOptions: { webpackConfigHook },
122
- ...( shutdownForceTime !== undefined && { shutdownForceTime } ),
123
- ...( shutdownGraceTime !== undefined && { shutdownGraceTime } )
124
- } );
107
+ services.connectionMonitor = run( () => new TemporalConnectionMonitor( { connection, signal } ) );
125
108
 
126
- log.info( 'Setting up telemetry...' );
127
- setupTelemetry( { worker } );
109
+ log.info( 'Publishing catalog workflow...' );
110
+ await run( () => services.catalogPublisher.run() );
128
111
 
129
- /**
130
- * NOTE
131
- * Temporal worker shutdown is a bit odd.
132
- * worker.run() is an async job that only resolves when calling worker.shutdown().
133
- * But worker.shutdown() is not async and returns nothing, so there is no way to await it.
134
- * All code that needs to run after shutdown needs to be after `await worker.run()`.
135
- *
136
- * The following code needs to cover these scenarios:
137
- * 1. Connection monitor detects connection loss
138
- * 2. Catalog.run() has a failure
139
- * 3. Interruption is received
140
- * 4. Worker throws an error
141
- *
142
- * For each scenario all promises in the Promise.all() need to be completed via functions:
143
- * connectionMonitor.stop(), catalogJob.interrupt(), worker.shutdown()
144
- */
145
-
146
- /**
147
- * Graceful shutdown
148
- * Triggers the actions that will resolve all promises in the Promise.all(), so the code can resume
149
- */
150
- const shutdown = runOnce( () => {
151
- log.info( 'Shutdown started...' );
152
- if ( worker.getStatus().runState === 'RUNNING' ) {
153
- worker.shutdown();
112
+ log.info( 'Creating Temporal worker...' );
113
+ const worker = await run( () => {
114
+ if ( workerTuner ) {
115
+ log.info( 'Using worker tuner options', { ...workerTuner } );
154
116
  }
155
- state.connectionMonitor.stop();
156
- state.catalogJob.interrupt();
157
- } );
158
117
 
159
- /** When receiving an interruption, call shutdown */
160
- setupInterruptionHandler( shutdown );
118
+ return Worker.create( {
119
+ connection,
120
+ namespace,
121
+ taskQueue,
122
+ workflowsPath,
123
+ activities,
124
+ sinks,
125
+ interceptors: initInterceptors( { activities, workflows } ),
126
+ // tuner isn't compatible with concurrent task executions configs
127
+ ...( workerTuner ? {
128
+ tuner: workerTuner
129
+ } : {
130
+ maxConcurrentWorkflowTaskExecutions,
131
+ maxConcurrentActivityTaskExecutions
132
+ } ),
133
+ maxCachedWorkflows,
134
+ maxConcurrentActivityTaskPolls,
135
+ maxConcurrentWorkflowTaskPolls,
136
+ bundlerOptions: { webpackConfigHook },
137
+ ...( shutdownForceTime !== undefined && { shutdownForceTime } ),
138
+ ...( shutdownGraceTime !== undefined && { shutdownGraceTime } )
139
+ } );
140
+ } );
161
141
 
162
- /** When the connection is lost, call shutdown */
163
- state.connectionMonitor.onConnectionLost( shutdown );
142
+ log.info( 'Setting up telemetry...' );
143
+ run( () => setupTelemetry( { worker } ) );
164
144
 
165
- /** If the catalog job manager fails, call shutdown */
166
- state.catalogJob.onError( shutdown );
145
+ log.info( 'Creating worker runner...' );
146
+ services.workerRunner = new WorkerRunner( { worker, signal } );
167
147
 
168
148
  /**
169
- * Runs the worker, connection monitor and catalogJob (ephemeral)
170
- * None of these will reject in normal conditions. Errors need to be inspected later
171
- * They will resolve only when calling the actions in shutdown(),
172
- * except catalogJob, which can resolve by itself given a bit of time
149
+ * Runs the worker and connection monitor together
150
+ * They will stop only upon receiving a wired kill signal (eg SIGINT), which resolves the promise
173
151
  */
174
152
  log.info( 'Running worker...' );
175
- await Promise.all( [
176
- // When the worker fails, store the error and call shutdown
177
- worker.run().catch( error => {
178
- state.workerError = error;
179
- shutdown();
180
- } ),
181
- state.connectionMonitor.start(),
182
- state.catalogJob.run()
183
- ] );
153
+ await run( () => Promise.race( [
154
+ services.workerRunner.start(),
155
+ services.connectionMonitor.start()
156
+ ] ) );
184
157
 
158
+ signal.throwIfAborted();
185
159
  log.info( 'Worker terminated' );
186
-
187
- /** After the Promise.all() is resolved, check which services had an error, since none rejects the promise */
188
- const error =
189
- state.connectionMonitor.connectionLossError ??
190
- state.catalogJob.error ??
191
- state.workerError;
192
-
193
- /** If any error is found, throws it, so this process can exit with code=1 (failure) */
194
- if ( error ) {
195
- throw error;
196
- }
197
160
  };
198
161
 
199
162
  execute()
163
+ .catch( async error => abortController.abort( error ) )
200
164
  .finally( async () => {
201
- /**
202
- * This will make sure that if we had any uncaught failures, everything is tore down.
203
- * Ignore any errors here in order to not mask actually errors from before and because at this point
204
- * the code is already shutting down, so no need to throw anyway.
205
- *
206
- * worker.shutdown() is not tried here, because it cannot be awaited. By this point is safe to say
207
- * that worker never started, already crashed or was stopped anyway.
208
- */
209
- if ( state.connectionMonitor?.running ) {
210
- log.info( 'Stopping connection monitor...' );
211
- await state.connectionMonitor.stop()
212
- .catch( e => log.warn( 'Connection monitor stop error', { error: e.message } ) );
213
- }
214
- if ( state.catalogJob?.running ) {
215
- log.info( 'Interrupting catalog job...' );
216
- await state.catalogJob.interrupt()
217
- .catch( e => log.warn( 'Catalog job interruption error', { error: e.message } ) );
218
- }
219
- if ( state.connection ) {
220
- log.info( 'Closing connection...' );
221
- await state.connection.close()
222
- .catch( e => log.warn( 'Connection close error', { error: e.message } ) );
223
- }
224
- } )
225
- .then( async () => {
226
- log.info( 'Flushing hook callbacks...' );
227
- await flushPendingHooks();
228
- log.info( 'Bye' );
229
- } )
230
- .catch( async error => {
231
- log.error( 'Fatal error', { error: serializeError( error ) } );
165
+ const shutdownFailures = await shutdownServices( services );
232
166
 
233
- mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error } );
167
+ const hasError = signal.aborted && !( signal.reason instanceof KillSignError );
168
+ if ( hasError ) {
169
+ log.error( 'Worker error', { error: serializeError( signal.reason ) } );
170
+ mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error: signal.reason } );
171
+ }
234
172
 
235
173
  log.info( 'Flushing hook callbacks...' );
236
174
  await flushPendingHooks();
237
- log.info( 'Exiting...' );
238
- setTimeout( () => process.exit( 1 ) );
175
+
176
+ const exitCode = shutdownFailures.some( v => v.service === 'worker' ) || hasError ? 1 : 0;
177
+ setTimeout( () => {
178
+ log.info( 'Bye' );
179
+ process.exit( exitCode );
180
+ } );
239
181
  } );