@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.spec.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { BusEventType } from '#consts';
|
|
2
3
|
|
|
3
4
|
const {
|
|
4
|
-
catalogJobInstance,
|
|
5
5
|
bindGlobalFunctionsMock,
|
|
6
|
+
catalogPublisherInstance,
|
|
6
7
|
configValues,
|
|
7
8
|
connectionMonitorInstance,
|
|
8
9
|
createCatalogMock,
|
|
9
10
|
flushPendingHooksMock,
|
|
11
|
+
hashSourceCodeMock,
|
|
10
12
|
initInterceptorsMock,
|
|
13
|
+
interruption,
|
|
14
|
+
loadActivitiesMock,
|
|
15
|
+
loadHooksMock,
|
|
16
|
+
loadWorkflowsMock,
|
|
11
17
|
mainEventBusMock,
|
|
12
18
|
mockConnection,
|
|
13
19
|
mockLog,
|
|
@@ -17,8 +23,10 @@ const {
|
|
|
17
23
|
serializeErrorMock,
|
|
18
24
|
setupClientConfigMock,
|
|
19
25
|
setupInterruptionHandlerMock,
|
|
26
|
+
setupTelemetryMock,
|
|
20
27
|
setupTemporalLoggerMock,
|
|
21
|
-
|
|
28
|
+
shutdownServicesMock,
|
|
29
|
+
workerRunnerInstance
|
|
22
30
|
} = vi.hoisted( () => {
|
|
23
31
|
const createDeferred = () => {
|
|
24
32
|
const state = {};
|
|
@@ -31,9 +39,8 @@ const {
|
|
|
31
39
|
|
|
32
40
|
const promises = {};
|
|
33
41
|
const resetPromises = () => {
|
|
34
|
-
promises.
|
|
42
|
+
promises.workerRunner = createDeferred();
|
|
35
43
|
promises.connectionMonitor = createDeferred();
|
|
36
|
-
promises.catalogJob = createDeferred();
|
|
37
44
|
};
|
|
38
45
|
resetPromises();
|
|
39
46
|
|
|
@@ -54,122 +61,139 @@ const {
|
|
|
54
61
|
shutdownGraceTime: undefined
|
|
55
62
|
};
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The real runner only settles once the worker finished draining, which is triggered by stop().
|
|
66
|
+
* Aborting alone leaves it running, so the shutdown sequence is the one awaiting the drain.
|
|
67
|
+
*/
|
|
68
|
+
const workerRunnerInstance = {
|
|
58
69
|
running: false,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
options: null,
|
|
71
|
+
start: vi.fn( () => {
|
|
72
|
+
workerRunnerInstance.running = true;
|
|
73
|
+
return promises.workerRunner.promise.finally( () => {
|
|
74
|
+
workerRunnerInstance.running = false;
|
|
75
|
+
} );
|
|
62
76
|
} ),
|
|
77
|
+
stop: vi.fn( () => {
|
|
78
|
+
promises.workerRunner.resolve();
|
|
79
|
+
return promises.workerRunner.promise.catch( () => {} );
|
|
80
|
+
} )
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** The real monitor exits its loop as soon as the signal aborts, so it settles on its own. */
|
|
84
|
+
const connectionMonitorInstance = {
|
|
85
|
+
running: false,
|
|
86
|
+
options: null,
|
|
63
87
|
start: vi.fn( () => {
|
|
64
88
|
connectionMonitorInstance.running = true;
|
|
89
|
+
connectionMonitorInstance.options?.signal.addEventListener(
|
|
90
|
+
'abort',
|
|
91
|
+
() => promises.connectionMonitor.resolve(),
|
|
92
|
+
{ once: true }
|
|
93
|
+
);
|
|
65
94
|
return promises.connectionMonitor.promise.finally( () => {
|
|
66
95
|
connectionMonitorInstance.running = false;
|
|
67
96
|
} );
|
|
68
97
|
} ),
|
|
69
98
|
stop: vi.fn( () => {
|
|
70
|
-
connectionMonitorInstance.running = false;
|
|
71
99
|
promises.connectionMonitor.resolve();
|
|
72
|
-
return promises.connectionMonitor.promise;
|
|
73
|
-
} )
|
|
74
|
-
connectionLostCb: null
|
|
100
|
+
return promises.connectionMonitor.promise.catch( () => {} );
|
|
101
|
+
} )
|
|
75
102
|
};
|
|
76
103
|
|
|
77
|
-
const
|
|
104
|
+
const catalogPublisherInstance = {
|
|
78
105
|
running: false,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
catalogJobInstance.running = false;
|
|
87
|
-
} );
|
|
88
|
-
} ),
|
|
89
|
-
interrupt: vi.fn( () => {
|
|
90
|
-
catalogJobInstance.running = false;
|
|
91
|
-
promises.catalogJob.resolve();
|
|
92
|
-
return promises.catalogJob.promise;
|
|
93
|
-
} ),
|
|
94
|
-
errorCb: null
|
|
106
|
+
options: null,
|
|
107
|
+
run: vi.fn().mockResolvedValue( undefined ),
|
|
108
|
+
interrupt: vi.fn().mockResolvedValue( undefined )
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
class KillSignError extends Error {
|
|
112
|
+
name = 'KillSignError';
|
|
95
113
|
};
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
run: vi.fn( () => promises.workerRun.promise ),
|
|
100
|
-
runState: 'RUNNING',
|
|
101
|
-
shutdown: vi.fn()
|
|
115
|
+
class UncaughtError extends Error {
|
|
116
|
+
name = 'UncaughtError';
|
|
102
117
|
};
|
|
103
118
|
|
|
119
|
+
const interruption = { controller: null, KillSignError, UncaughtError };
|
|
120
|
+
|
|
104
121
|
return {
|
|
105
|
-
catalogJobInstance,
|
|
106
122
|
bindGlobalFunctionsMock: vi.fn(),
|
|
123
|
+
catalogPublisherInstance,
|
|
107
124
|
configValues,
|
|
108
125
|
connectionMonitorInstance,
|
|
109
|
-
createCatalogMock: vi.fn().mockReturnValue( {
|
|
110
|
-
createWorkflowsEntryPointMock: vi.fn().mockReturnValue( '/fake/workflows/path.js' ),
|
|
126
|
+
createCatalogMock: vi.fn().mockReturnValue( { workflowNames: [ 'demo' ] } ),
|
|
111
127
|
flushPendingHooksMock: vi.fn().mockResolvedValue( undefined ),
|
|
112
128
|
hashSourceCodeMock: vi.fn().mockResolvedValue( 'catalog-hash' ),
|
|
113
129
|
initInterceptorsMock: vi.fn().mockReturnValue( [] ),
|
|
114
|
-
|
|
130
|
+
interruption,
|
|
131
|
+
loadActivitiesMock: vi.fn().mockResolvedValue( { activities: {} } ),
|
|
115
132
|
loadHooksMock: vi.fn().mockResolvedValue( undefined ),
|
|
116
|
-
loadWorkflowsMock: vi.fn().mockResolvedValue( [] ),
|
|
133
|
+
loadWorkflowsMock: vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/path.js' } ),
|
|
117
134
|
mainEventBusMock: { emit: vi.fn(), on: vi.fn() },
|
|
118
135
|
mockConnection: { close: vi.fn().mockResolvedValue( undefined ) },
|
|
119
136
|
mockLog: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
|
|
120
|
-
mockWorker,
|
|
137
|
+
mockWorker: { name: 'temporal-worker' },
|
|
121
138
|
promises,
|
|
122
139
|
resetPromises,
|
|
123
|
-
serializeErrorMock: vi.fn( error => ( {
|
|
124
|
-
name: error.name,
|
|
125
|
-
message: error.message,
|
|
126
|
-
stack: error.stack
|
|
127
|
-
} ) ),
|
|
140
|
+
serializeErrorMock: vi.fn( error => ( { name: error.name, message: error.message } ) ),
|
|
128
141
|
setupClientConfigMock: vi.fn(),
|
|
129
|
-
setupInterruptionHandlerMock: vi.fn(
|
|
142
|
+
setupInterruptionHandlerMock: vi.fn( controller => {
|
|
143
|
+
interruption.controller = controller;
|
|
144
|
+
} ),
|
|
145
|
+
setupTelemetryMock: vi.fn(),
|
|
130
146
|
setupTemporalLoggerMock: vi.fn(),
|
|
131
|
-
|
|
147
|
+
shutdownServicesMock: vi.fn().mockResolvedValue( [] ),
|
|
148
|
+
workerRunnerInstance
|
|
132
149
|
};
|
|
133
150
|
} );
|
|
134
151
|
|
|
152
|
+
const initTracing = vi.fn().mockResolvedValue( undefined );
|
|
153
|
+
|
|
135
154
|
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
136
155
|
vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
|
|
137
|
-
const initTracing = vi.fn().mockResolvedValue( undefined );
|
|
138
156
|
vi.mock( '#tracing', () => ( { init: initTracing } ) );
|
|
139
157
|
vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
|
|
140
158
|
vi.mock( '#hooks/pending_hooks', () => ( { flushPendingHooks: flushPendingHooksMock } ) );
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const loadActivitiesMock = vi.fn().mockResolvedValue( { activities: {} } );
|
|
144
|
-
const loadHooksMock = vi.fn().mockResolvedValue( undefined );
|
|
159
|
+
vi.mock( '#temporal/client', () => ( { setupClientConfig: setupClientConfigMock } ) );
|
|
160
|
+
vi.mock( './configs.js', () => configValues );
|
|
145
161
|
vi.mock( './loader/workflows.js', () => ( { loadWorkflows: loadWorkflowsMock } ) );
|
|
146
162
|
vi.mock( './loader/activities.js', () => ( { loadActivities: loadActivitiesMock } ) );
|
|
147
163
|
vi.mock( './loader/hooks.js', () => ( { loadHooks: loadHooksMock } ) );
|
|
148
|
-
vi.mock( './configs.js', () => configValues );
|
|
149
|
-
|
|
150
|
-
const hashSourceCodeMock = vi.fn().mockResolvedValue( 'catalog-hash' );
|
|
151
164
|
vi.mock( './loader/tools.js', () => ( { hashSourceCode: hashSourceCodeMock } ) );
|
|
152
|
-
|
|
153
165
|
vi.mock( './sinks.js', () => ( { sinks: {} } ) );
|
|
154
166
|
vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
|
|
155
167
|
vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
156
168
|
vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptorsMock } ) );
|
|
157
169
|
vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
|
|
158
|
-
vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
|
|
159
170
|
vi.mock( './global_functions.js', () => ( { bindGlobalFunctions: bindGlobalFunctionsMock } ) );
|
|
171
|
+
vi.mock( './temporal_logger.js', () => ( { setupTemporalLogger: setupTemporalLoggerMock } ) );
|
|
172
|
+
vi.mock( './log_hooks.js', () => ( {} ) );
|
|
173
|
+
vi.mock( './interruption.js', () => ( {
|
|
174
|
+
setupInterruptionHandler: setupInterruptionHandlerMock,
|
|
175
|
+
KillSignError: interruption.KillSignError,
|
|
176
|
+
UncaughtError: interruption.UncaughtError
|
|
177
|
+
} ) );
|
|
178
|
+
vi.mock( './worker_runner.js', () => ( {
|
|
179
|
+
WorkerRunner: vi.fn( function ( options ) {
|
|
180
|
+
workerRunnerInstance.options = options;
|
|
181
|
+
return workerRunnerInstance;
|
|
182
|
+
} )
|
|
183
|
+
} ) );
|
|
160
184
|
vi.mock( './connection_monitor.js', () => ( {
|
|
161
|
-
TemporalConnectionMonitor: vi.fn( function () {
|
|
185
|
+
TemporalConnectionMonitor: vi.fn( function ( options ) {
|
|
186
|
+
connectionMonitorInstance.options = options;
|
|
162
187
|
return connectionMonitorInstance;
|
|
163
188
|
} )
|
|
164
189
|
} ) );
|
|
165
|
-
vi.mock( './catalog_workflow/
|
|
166
|
-
|
|
167
|
-
|
|
190
|
+
vi.mock( './catalog_workflow/catalog_publisher.js', () => ( {
|
|
191
|
+
CatalogPublisher: vi.fn( function ( options ) {
|
|
192
|
+
catalogPublisherInstance.options = options;
|
|
193
|
+
return catalogPublisherInstance;
|
|
168
194
|
} )
|
|
169
195
|
} ) );
|
|
170
|
-
vi.mock( './
|
|
171
|
-
vi.mock( './temporal_logger.js', () => ( { setupTemporalLogger: setupTemporalLoggerMock } ) );
|
|
172
|
-
vi.mock( '#temporal/client', () => ( { setupClientConfig: setupClientConfigMock } ) );
|
|
196
|
+
vi.mock( './shutdown.js', () => ( { shutdownServices: shutdownServicesMock } ) );
|
|
173
197
|
vi.mock( '@temporalio/worker', () => ( {
|
|
174
198
|
NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) },
|
|
175
199
|
Worker: { create: vi.fn().mockResolvedValue( mockWorker ) }
|
|
@@ -180,15 +204,18 @@ const importWorker = async () => {
|
|
|
180
204
|
await import( './index.js' );
|
|
181
205
|
};
|
|
182
206
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
/** Imports the worker and waits until it is polling, which is where a healthy boot settles. */
|
|
208
|
+
const bootWorker = async () => {
|
|
209
|
+
await importWorker();
|
|
210
|
+
await vi.waitFor( () => {
|
|
211
|
+
expect( workerRunnerInstance.start ).toHaveBeenCalled();
|
|
212
|
+
expect( connectionMonitorInstance.start ).toHaveBeenCalled();
|
|
213
|
+
} );
|
|
188
214
|
};
|
|
189
215
|
|
|
216
|
+
const waitForExit = () => vi.waitFor( () => expect( process.exit ).toHaveBeenCalled() );
|
|
217
|
+
|
|
190
218
|
describe( 'worker/index', () => {
|
|
191
|
-
const exitMock = vi.fn();
|
|
192
219
|
const originalArgv = process.argv;
|
|
193
220
|
|
|
194
221
|
beforeEach( () => {
|
|
@@ -199,16 +226,16 @@ describe( 'worker/index', () => {
|
|
|
199
226
|
configValues.workerTuner = undefined;
|
|
200
227
|
configValues.shutdownForceTime = undefined;
|
|
201
228
|
configValues.shutdownGraceTime = undefined;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
catalogJobInstance.running = false;
|
|
205
|
-
connectionMonitorInstance.connectionLossError = null;
|
|
206
|
-
connectionMonitorInstance.connectionLostCb = null;
|
|
229
|
+
workerRunnerInstance.running = false;
|
|
230
|
+
workerRunnerInstance.options = null;
|
|
207
231
|
connectionMonitorInstance.running = false;
|
|
232
|
+
connectionMonitorInstance.options = null;
|
|
233
|
+
catalogPublisherInstance.running = false;
|
|
234
|
+
catalogPublisherInstance.options = null;
|
|
235
|
+
interruption.controller = null;
|
|
208
236
|
mockConnection.close.mockResolvedValue( undefined );
|
|
209
|
-
mockWorker.runState = 'RUNNING';
|
|
210
237
|
process.argv = [ ...originalArgv.slice( 0, 2 ), '/test/caller/dir' ];
|
|
211
|
-
vi.spyOn( process, 'exit' ).mockImplementation(
|
|
238
|
+
vi.spyOn( process, 'exit' ).mockImplementation( () => undefined );
|
|
212
239
|
} );
|
|
213
240
|
|
|
214
241
|
afterEach( () => {
|
|
@@ -216,225 +243,284 @@ describe( 'worker/index', () => {
|
|
|
216
243
|
vi.restoreAllMocks();
|
|
217
244
|
} );
|
|
218
245
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
describe( 'startup', () => {
|
|
247
|
+
it( 'wires every lifecycle component with the shared abort signal', async () => {
|
|
248
|
+
const { NativeConnection, Worker } = await import( '@temporalio/worker' );
|
|
249
|
+
const { TemporalConnectionMonitor } = await import( './connection_monitor.js' );
|
|
250
|
+
const { CatalogPublisher } = await import( './catalog_workflow/catalog_publisher.js' );
|
|
251
|
+
const { WorkerRunner } = await import( './worker_runner.js' );
|
|
252
|
+
|
|
253
|
+
await bootWorker();
|
|
254
|
+
|
|
255
|
+
const { signal } = interruption.controller;
|
|
256
|
+
|
|
257
|
+
expect( setupInterruptionHandlerMock ).toHaveBeenCalledWith( expect.any( AbortController ) );
|
|
258
|
+
expect( setupTemporalLoggerMock ).toHaveBeenCalledOnce();
|
|
259
|
+
expect( loadHooksMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
260
|
+
expect( loadWorkflowsMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
261
|
+
expect( loadActivitiesMock ).toHaveBeenCalledWith( '/test/caller/dir', [] );
|
|
262
|
+
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKER_BEFORE_START );
|
|
263
|
+
expect( initTracing ).toHaveBeenCalledOnce();
|
|
264
|
+
expect( createCatalogMock ).toHaveBeenCalledWith( { workflows: [], activities: {} } );
|
|
265
|
+
expect( hashSourceCodeMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
266
|
+
expect( NativeConnection.connect ).toHaveBeenCalledWith( {
|
|
267
|
+
address: configValues.address,
|
|
268
|
+
tls: false,
|
|
269
|
+
apiKey: undefined,
|
|
270
|
+
proxy: undefined
|
|
271
|
+
} );
|
|
272
|
+
expect( setupClientConfigMock ).toHaveBeenCalledWith( {
|
|
273
|
+
connection: mockConnection,
|
|
274
|
+
namespace: configValues.namespace
|
|
275
|
+
} );
|
|
276
|
+
expect( CatalogPublisher ).toHaveBeenCalledWith( {
|
|
277
|
+
connection: mockConnection,
|
|
278
|
+
namespace: configValues.namespace,
|
|
279
|
+
catalog: { workflowNames: [ 'demo' ] },
|
|
280
|
+
catalogHash: 'catalog-hash',
|
|
281
|
+
signal
|
|
282
|
+
} );
|
|
283
|
+
expect( catalogPublisherInstance.run ).toHaveBeenCalledOnce();
|
|
284
|
+
expect( TemporalConnectionMonitor ).toHaveBeenCalledWith( { connection: mockConnection, signal } );
|
|
285
|
+
expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
286
|
+
connection: mockConnection,
|
|
287
|
+
namespace: configValues.namespace,
|
|
288
|
+
taskQueue: configValues.taskQueue,
|
|
289
|
+
workflowsPath: '/fake/workflows/path.js',
|
|
290
|
+
activities: {},
|
|
291
|
+
maxConcurrentWorkflowTaskExecutions: configValues.maxConcurrentWorkflowTaskExecutions,
|
|
292
|
+
maxConcurrentActivityTaskExecutions: configValues.maxConcurrentActivityTaskExecutions,
|
|
293
|
+
maxCachedWorkflows: configValues.maxCachedWorkflows,
|
|
294
|
+
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
295
|
+
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
296
|
+
} ) );
|
|
297
|
+
expect( Worker.create.mock.calls[0][0] ).not.toHaveProperty( 'shutdownForceTime' );
|
|
298
|
+
expect( Worker.create.mock.calls[0][0] ).not.toHaveProperty( 'shutdownGraceTime' );
|
|
299
|
+
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
|
|
300
|
+
expect( setupTelemetryMock ).toHaveBeenCalledWith( { worker: mockWorker } );
|
|
301
|
+
expect( WorkerRunner ).toHaveBeenCalledWith( { worker: mockWorker, signal } );
|
|
302
|
+
} );
|
|
303
|
+
|
|
304
|
+
it( 'publishes the catalog before creating the worker', async () => {
|
|
305
|
+
const { Worker } = await import( '@temporalio/worker' );
|
|
306
|
+
|
|
307
|
+
await bootWorker();
|
|
308
|
+
|
|
309
|
+
// The worker never exists unless its catalog matches its own source code.
|
|
310
|
+
expect( catalogPublisherInstance.run.mock.invocationCallOrder[0] )
|
|
311
|
+
.toBeLessThan( Worker.create.mock.invocationCallOrder[0] );
|
|
240
312
|
} );
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
313
|
+
|
|
314
|
+
it( 'passes worker tuner instead of incompatible execution concurrency options', async () => {
|
|
315
|
+
configValues.workerTuner = { tunerOptions: { targetMemoryUsage: 0.8, targetCpuUsage: 0.9 } };
|
|
316
|
+
const { Worker } = await import( '@temporalio/worker' );
|
|
317
|
+
|
|
318
|
+
await bootWorker();
|
|
319
|
+
|
|
320
|
+
const workerOptions = Worker.create.mock.calls[0][0];
|
|
321
|
+
expect( workerOptions ).toEqual( expect.objectContaining( { tuner: configValues.workerTuner } ) );
|
|
322
|
+
expect( workerOptions ).not.toHaveProperty( 'maxConcurrentWorkflowTaskExecutions' );
|
|
323
|
+
expect( workerOptions ).not.toHaveProperty( 'maxConcurrentActivityTaskExecutions' );
|
|
244
324
|
} );
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
catalog: { workflows: [], activities: {} },
|
|
254
|
-
catalogHash: 'catalog-hash'
|
|
325
|
+
|
|
326
|
+
it( 'enables TLS when an apiKey is set', async () => {
|
|
327
|
+
configValues.apiKey = 'secret';
|
|
328
|
+
const { NativeConnection } = await import( '@temporalio/worker' );
|
|
329
|
+
|
|
330
|
+
await bootWorker();
|
|
331
|
+
|
|
332
|
+
expect( NativeConnection.connect ).toHaveBeenCalledWith( expect.objectContaining( { apiKey: 'secret', tls: true } ) );
|
|
255
333
|
} );
|
|
256
|
-
expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
257
|
-
connection: mockConnection,
|
|
258
|
-
namespace: configValues.namespace,
|
|
259
|
-
taskQueue: configValues.taskQueue,
|
|
260
|
-
workflowsPath: '/fake/workflows/path.js',
|
|
261
|
-
activities: {},
|
|
262
|
-
maxConcurrentWorkflowTaskExecutions: configValues.maxConcurrentWorkflowTaskExecutions,
|
|
263
|
-
maxConcurrentActivityTaskExecutions: configValues.maxConcurrentActivityTaskExecutions,
|
|
264
|
-
maxCachedWorkflows: configValues.maxCachedWorkflows,
|
|
265
|
-
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
266
|
-
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
267
|
-
} ) );
|
|
268
|
-
expect( Worker.create.mock.calls[0][0] ).not.toHaveProperty( 'shutdownForceTime' );
|
|
269
|
-
expect( Worker.create.mock.calls[0][0] ).not.toHaveProperty( 'shutdownGraceTime' );
|
|
270
|
-
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
|
|
271
|
-
expect( setupTelemetryMock ).toHaveBeenCalledWith( { worker: mockWorker } );
|
|
272
|
-
expect( setupInterruptionHandlerMock ).toHaveBeenCalledWith( expect.any( Function ) );
|
|
273
|
-
expect( connectionMonitorInstance.onConnectionLost ).toHaveBeenCalledWith( expect.any( Function ) );
|
|
274
|
-
expect( catalogJobInstance.onError ).toHaveBeenCalledWith( expect.any( Function ) );
|
|
275
|
-
expect( mockWorker.run ).toHaveBeenCalled();
|
|
276
|
-
expect( connectionMonitorInstance.start ).toHaveBeenCalled();
|
|
277
|
-
expect( catalogJobInstance.run ).toHaveBeenCalled();
|
|
278
334
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
} );
|
|
335
|
+
it( 'routes the connection through the configured gRPC proxy', async () => {
|
|
336
|
+
configValues.grpcProxy = 'proxy:1234';
|
|
337
|
+
const { NativeConnection } = await import( '@temporalio/worker' );
|
|
283
338
|
|
|
284
|
-
|
|
285
|
-
configValues.workerTuner = {
|
|
286
|
-
tunerOptions: {
|
|
287
|
-
targetMemoryUsage: 0.8,
|
|
288
|
-
targetCpuUsage: 0.9
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
const { Worker } = await import( '@temporalio/worker' );
|
|
292
|
-
|
|
293
|
-
await importWorker();
|
|
294
|
-
|
|
295
|
-
await vi.waitFor( () => expect( Worker.create ).toHaveBeenCalled() );
|
|
296
|
-
|
|
297
|
-
const workerOptions = Worker.create.mock.calls[0][0];
|
|
298
|
-
expect( workerOptions ).toEqual( expect.objectContaining( {
|
|
299
|
-
tuner: configValues.workerTuner,
|
|
300
|
-
maxCachedWorkflows: configValues.maxCachedWorkflows,
|
|
301
|
-
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
302
|
-
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
303
|
-
} ) );
|
|
304
|
-
expect( workerOptions ).not.toHaveProperty( 'maxConcurrentWorkflowTaskExecutions' );
|
|
305
|
-
expect( workerOptions ).not.toHaveProperty( 'maxConcurrentActivityTaskExecutions' );
|
|
306
|
-
|
|
307
|
-
await settleWorker();
|
|
308
|
-
} );
|
|
339
|
+
await bootWorker();
|
|
309
340
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
341
|
+
expect( NativeConnection.connect ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
342
|
+
proxy: { type: 'http-connect', targetHost: 'proxy:1234' }
|
|
343
|
+
} ) );
|
|
344
|
+
} );
|
|
313
345
|
|
|
314
|
-
|
|
346
|
+
it( 'passes configured shutdown durations to the worker', async () => {
|
|
347
|
+
configValues.shutdownForceTime = '30s';
|
|
348
|
+
configValues.shutdownGraceTime = '10s';
|
|
349
|
+
const { Worker } = await import( '@temporalio/worker' );
|
|
315
350
|
|
|
316
|
-
|
|
317
|
-
apiKey: 'secret',
|
|
318
|
-
tls: true
|
|
319
|
-
} ) ) );
|
|
351
|
+
await bootWorker();
|
|
320
352
|
|
|
321
|
-
|
|
353
|
+
expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
354
|
+
shutdownForceTime: '30s',
|
|
355
|
+
shutdownGraceTime: '10s'
|
|
356
|
+
} ) );
|
|
357
|
+
} );
|
|
358
|
+
|
|
359
|
+
it( 'stops advancing through startup once the signal aborts', async () => {
|
|
360
|
+
const { NativeConnection } = await import( '@temporalio/worker' );
|
|
361
|
+
loadHooksMock.mockImplementationOnce( async () => {
|
|
362
|
+
interruption.controller.abort( new interruption.KillSignError( 'SIGTERM' ) );
|
|
363
|
+
} );
|
|
364
|
+
|
|
365
|
+
await importWorker();
|
|
366
|
+
await waitForExit();
|
|
367
|
+
|
|
368
|
+
expect( loadWorkflowsMock ).not.toHaveBeenCalled();
|
|
369
|
+
expect( NativeConnection.connect ).not.toHaveBeenCalled();
|
|
370
|
+
expect( mockLog.error ).not.toHaveBeenCalled();
|
|
371
|
+
expect( process.exit ).toHaveBeenCalledWith( 0 );
|
|
372
|
+
} );
|
|
322
373
|
} );
|
|
323
374
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const { Worker } = await import( '@temporalio/worker' );
|
|
375
|
+
describe( 'clean termination', () => {
|
|
376
|
+
it( 'shuts every service down and exits successfully on a kill signal', async () => {
|
|
377
|
+
await bootWorker();
|
|
328
378
|
|
|
329
|
-
|
|
379
|
+
interruption.controller.abort( new interruption.KillSignError( 'SIGTERM' ) );
|
|
380
|
+
await waitForExit();
|
|
330
381
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
382
|
+
expect( shutdownServicesMock ).toHaveBeenCalledOnce();
|
|
383
|
+
expect( shutdownServicesMock ).toHaveBeenCalledWith( {
|
|
384
|
+
workerRunner: workerRunnerInstance,
|
|
385
|
+
connectionMonitor: connectionMonitorInstance,
|
|
386
|
+
catalogPublisher: catalogPublisherInstance,
|
|
387
|
+
connection: mockConnection
|
|
388
|
+
} );
|
|
389
|
+
expect( mockLog.error ).not.toHaveBeenCalled();
|
|
390
|
+
expect( mainEventBusMock.emit ).not.toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.anything() );
|
|
391
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
392
|
+
expect( process.exit ).toHaveBeenCalledWith( 0 );
|
|
393
|
+
} );
|
|
335
394
|
|
|
336
|
-
|
|
337
|
-
|
|
395
|
+
it( 'still exits cleanly when a service other than the worker failed to stop', async () => {
|
|
396
|
+
await bootWorker();
|
|
397
|
+
shutdownServicesMock.mockResolvedValueOnce( [ { service: 'connection', error: new Error( 'close failed' ) } ] );
|
|
338
398
|
|
|
339
|
-
|
|
340
|
-
|
|
399
|
+
interruption.controller.abort( new interruption.KillSignError( 'SIGTERM' ) );
|
|
400
|
+
await waitForExit();
|
|
341
401
|
|
|
342
|
-
|
|
343
|
-
|
|
402
|
+
expect( mockLog.error ).not.toHaveBeenCalled();
|
|
403
|
+
expect( process.exit ).toHaveBeenCalledWith( 0 );
|
|
404
|
+
} );
|
|
344
405
|
|
|
345
|
-
|
|
406
|
+
it( 'does not report the worker as terminated when a signal ended the run', async () => {
|
|
407
|
+
await bootWorker();
|
|
346
408
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
expect( catalogJobInstance.interrupt ).toHaveBeenCalledOnce();
|
|
409
|
+
interruption.controller.abort( new interruption.KillSignError( 'SIGTERM' ) );
|
|
410
|
+
await waitForExit();
|
|
350
411
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
354
|
-
} );
|
|
412
|
+
expect( mockLog.info ).not.toHaveBeenCalledWith( 'Worker terminated' );
|
|
413
|
+
} );
|
|
355
414
|
|
|
356
|
-
|
|
357
|
-
|
|
415
|
+
it( 'reports a terminated worker when the run ends without an abort', async () => {
|
|
416
|
+
await bootWorker();
|
|
358
417
|
|
|
359
|
-
|
|
360
|
-
|
|
418
|
+
promises.connectionMonitor.resolve();
|
|
419
|
+
await waitForExit();
|
|
361
420
|
|
|
362
|
-
|
|
363
|
-
|
|
421
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Worker terminated' );
|
|
422
|
+
expect( process.exit ).toHaveBeenCalledWith( 0 );
|
|
423
|
+
} );
|
|
364
424
|
|
|
365
|
-
|
|
366
|
-
|
|
425
|
+
it( 'flushes pending hooks before exiting', async () => {
|
|
426
|
+
await bootWorker();
|
|
367
427
|
|
|
368
|
-
|
|
369
|
-
|
|
428
|
+
interruption.controller.abort( new interruption.KillSignError( 'SIGTERM' ) );
|
|
429
|
+
await waitForExit();
|
|
370
430
|
|
|
371
|
-
|
|
372
|
-
expect(
|
|
373
|
-
|
|
374
|
-
|
|
431
|
+
expect( flushPendingHooksMock ).toHaveBeenCalledOnce();
|
|
432
|
+
expect( shutdownServicesMock.mock.invocationCallOrder[0] )
|
|
433
|
+
.toBeLessThan( flushPendingHooksMock.mock.invocationCallOrder[0] );
|
|
434
|
+
expect( flushPendingHooksMock.mock.invocationCallOrder[0] )
|
|
435
|
+
.toBeLessThan( process.exit.mock.invocationCallOrder[0] );
|
|
375
436
|
} );
|
|
376
|
-
expect( serializeErrorMock ).toHaveBeenCalledWith( error );
|
|
377
|
-
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
378
|
-
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
|
|
379
|
-
expect( flushPendingHooksMock ).toHaveBeenCalledOnce();
|
|
380
|
-
expect( mainEventBusMock.emit.mock.invocationCallOrder[0] ).toBeLessThan( flushPendingHooksMock.mock.invocationCallOrder[0] );
|
|
381
|
-
expect( flushPendingHooksMock.mock.invocationCallOrder[0] ).toBeLessThan( exitMock.mock.invocationCallOrder[0] );
|
|
382
437
|
} );
|
|
383
438
|
|
|
384
|
-
|
|
385
|
-
|
|
439
|
+
describe( 'failures', () => {
|
|
440
|
+
it( 'reports an uncaught error captured by the interruption handler', async () => {
|
|
441
|
+
const cause = new Error( 'boom' );
|
|
442
|
+
await bootWorker();
|
|
443
|
+
|
|
444
|
+
const fault = new interruption.UncaughtError( 'uncaughtException', { cause } );
|
|
445
|
+
interruption.controller.abort( fault );
|
|
446
|
+
await waitForExit();
|
|
447
|
+
|
|
448
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Worker error', {
|
|
449
|
+
error: expect.objectContaining( { name: 'UncaughtError', message: 'uncaughtException' } )
|
|
450
|
+
} );
|
|
451
|
+
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, { error: fault } );
|
|
452
|
+
expect( process.exit ).toHaveBeenCalledWith( 1 );
|
|
453
|
+
} );
|
|
454
|
+
|
|
455
|
+
it( 'exits with a failure when the worker never finished draining', async () => {
|
|
456
|
+
await bootWorker();
|
|
457
|
+
shutdownServicesMock.mockResolvedValueOnce( [ { service: 'worker', error: new Error( 'drain failed' ) } ] );
|
|
458
|
+
|
|
459
|
+
interruption.controller.abort( new interruption.KillSignError( 'SIGTERM' ) );
|
|
460
|
+
await waitForExit();
|
|
461
|
+
|
|
462
|
+
expect( process.exit ).toHaveBeenCalledWith( 1 );
|
|
463
|
+
} );
|
|
386
464
|
|
|
387
|
-
|
|
388
|
-
|
|
465
|
+
it( 'reports a lost connection and shuts down', async () => {
|
|
466
|
+
const error = new Error( 'connection lost' );
|
|
467
|
+
await bootWorker();
|
|
389
468
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
promises.workerRun.resolve();
|
|
469
|
+
promises.connectionMonitor.reject( error );
|
|
470
|
+
await waitForExit();
|
|
393
471
|
|
|
394
|
-
|
|
395
|
-
expect(
|
|
472
|
+
expect( shutdownServicesMock ).toHaveBeenCalledOnce();
|
|
473
|
+
expect( serializeErrorMock ).toHaveBeenCalledWith( error );
|
|
474
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Worker error', {
|
|
396
475
|
error: expect.objectContaining( { message: 'connection lost' } )
|
|
397
|
-
} )
|
|
476
|
+
} );
|
|
477
|
+
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, { error } );
|
|
478
|
+
expect( process.exit ).toHaveBeenCalledWith( 1 );
|
|
398
479
|
} );
|
|
399
|
-
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
400
|
-
expect( catalogJobInstance.interrupt ).toHaveBeenCalledOnce();
|
|
401
|
-
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
402
|
-
} );
|
|
403
480
|
|
|
404
|
-
|
|
405
|
-
|
|
481
|
+
it( 'reports a failed worker run', async () => {
|
|
482
|
+
const error = new Error( 'worker crashed' );
|
|
483
|
+
await bootWorker();
|
|
406
484
|
|
|
407
|
-
|
|
408
|
-
|
|
485
|
+
promises.workerRunner.reject( error );
|
|
486
|
+
await waitForExit();
|
|
409
487
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
488
|
+
expect( shutdownServicesMock ).toHaveBeenCalledOnce();
|
|
489
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Worker error', {
|
|
490
|
+
error: expect.objectContaining( { message: 'worker crashed' } )
|
|
491
|
+
} );
|
|
492
|
+
expect( process.exit ).toHaveBeenCalledWith( 1 );
|
|
493
|
+
} );
|
|
413
494
|
|
|
414
|
-
|
|
415
|
-
|
|
495
|
+
it( 'never creates the worker when publishing the catalog fails', async () => {
|
|
496
|
+
const { Worker } = await import( '@temporalio/worker' );
|
|
497
|
+
catalogPublisherInstance.run.mockRejectedValueOnce( new Error( 'catalog failed' ) );
|
|
498
|
+
|
|
499
|
+
await importWorker();
|
|
500
|
+
await waitForExit();
|
|
501
|
+
|
|
502
|
+
expect( Worker.create ).not.toHaveBeenCalled();
|
|
503
|
+
expect( workerRunnerInstance.start ).not.toHaveBeenCalled();
|
|
504
|
+
expect( shutdownServicesMock ).toHaveBeenCalledOnce();
|
|
505
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Worker error', {
|
|
416
506
|
error: expect.objectContaining( { message: 'catalog failed' } )
|
|
417
|
-
} )
|
|
507
|
+
} );
|
|
508
|
+
expect( process.exit ).toHaveBeenCalledWith( 1 );
|
|
418
509
|
} );
|
|
419
|
-
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
420
|
-
expect( connectionMonitorInstance.stop ).toHaveBeenCalledOnce();
|
|
421
|
-
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
422
|
-
} );
|
|
423
510
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
Worker.create.mockRejectedValueOnce( error );
|
|
511
|
+
it( 'hands the partially built services to shutdown when startup fails', async () => {
|
|
512
|
+
const { Worker } = await import( '@temporalio/worker' );
|
|
513
|
+
Worker.create.mockRejectedValueOnce( new Error( 'worker create failed' ) );
|
|
428
514
|
|
|
429
|
-
|
|
515
|
+
await importWorker();
|
|
516
|
+
await waitForExit();
|
|
430
517
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
436
|
-
error: expect.objectContaining( { message: 'worker create failed' } )
|
|
518
|
+
expect( workerRunnerInstance.start ).not.toHaveBeenCalled();
|
|
519
|
+
expect( shutdownServicesMock ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
520
|
+
workerRunner: null,
|
|
521
|
+
connection: mockConnection
|
|
437
522
|
} ) );
|
|
523
|
+
expect( process.exit ).toHaveBeenCalledWith( 1 );
|
|
438
524
|
} );
|
|
439
525
|
} );
|
|
440
526
|
} );
|