@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.
- package/package.json +1 -1
- package/src/helpers/function.js +0 -16
- package/src/helpers/function.spec.js +1 -47
- package/src/helpers/promise.js +8 -26
- package/src/helpers/promise.spec.js +49 -22
- package/src/worker/catalog_workflow/catalog_publisher.js +149 -0
- package/src/worker/catalog_workflow/catalog_publisher.spec.js +317 -0
- package/src/worker/configs.js +4 -4
- package/src/worker/configs.spec.js +5 -5
- package/src/worker/connection_monitor.js +83 -55
- package/src/worker/connection_monitor.spec.js +258 -112
- package/src/worker/index.js +86 -144
- package/src/worker/index.spec.js +339 -253
- package/src/worker/interruption.js +57 -18
- package/src/worker/interruption.spec.js +139 -60
- package/src/worker/shutdown.js +53 -0
- package/src/worker/shutdown.spec.js +197 -0
- package/src/worker/worker_runner.js +66 -0
- package/src/worker/worker_runner.spec.js +220 -0
- package/src/worker/catalog_workflow/catalog_job.js +0 -152
- package/src/worker/catalog_workflow/catalog_job.spec.js +0 -218
package/src/worker/index.js
CHANGED
|
@@ -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 {
|
|
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
|
|
47
|
+
const services = {
|
|
47
48
|
connection: null,
|
|
48
49
|
connectionMonitor: null,
|
|
49
|
-
|
|
50
|
-
|
|
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( '
|
|
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
|
-
|
|
100
|
+
log.info( 'Setting up temporal endpoint...' );
|
|
101
|
+
run( () => setupClientConfig( { connection, namespace } ) );
|
|
87
102
|
|
|
88
|
-
|
|
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
|
-
|
|
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( '
|
|
127
|
-
|
|
109
|
+
log.info( 'Publishing catalog workflow...' );
|
|
110
|
+
await run( () => services.catalogPublisher.run() );
|
|
128
111
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
142
|
+
log.info( 'Setting up telemetry...' );
|
|
143
|
+
run( () => setupTelemetry( { worker } ) );
|
|
164
144
|
|
|
165
|
-
|
|
166
|
-
|
|
145
|
+
log.info( 'Creating worker runner...' );
|
|
146
|
+
services.workerRunner = new WorkerRunner( { worker, signal } );
|
|
167
147
|
|
|
168
148
|
/**
|
|
169
|
-
* Runs the worker
|
|
170
|
-
*
|
|
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.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
} );
|