@outputai/core 0.7.0 → 0.7.1-next.0e958f3.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/bin/worker.sh +6 -0
- package/package.json +1 -1
- package/src/consts.js +0 -4
- package/src/errors.js +6 -2
- package/src/hooks/index.d.ts +10 -0
- package/src/interface/evaluator.js +7 -20
- package/src/interface/evaluator.spec.js +117 -1
- package/src/interface/step.js +8 -9
- package/src/interface/step.spec.js +124 -0
- package/src/interface/validations/index.js +108 -0
- package/src/interface/validations/index.spec.js +182 -0
- package/src/interface/validations/schemas.js +113 -0
- package/src/interface/validations/schemas.spec.js +209 -0
- package/src/interface/webhook.js +1 -1
- package/src/interface/webhook.spec.js +1 -1
- package/src/interface/workflow.d.ts +10 -9
- package/src/interface/workflow.js +76 -164
- package/src/interface/workflow.spec.js +637 -521
- package/src/interface/workflow_activity_options.js +16 -0
- package/src/interface/workflow_utils.js +1 -1
- package/src/interface/zod_integration.spec.js +2 -2
- package/src/internal_utils/aggregations.js +0 -10
- package/src/internal_utils/aggregations.spec.js +1 -48
- package/src/internal_utils/errors.js +14 -8
- package/src/internal_utils/errors.spec.js +73 -27
- package/src/utils/index.d.ts +19 -0
- package/src/utils/utils.js +46 -0
- package/src/utils/utils.spec.js +82 -1
- package/src/worker/bundle.js +26 -0
- package/src/worker/bundle.spec.js +52 -0
- package/src/worker/catalog_workflow/catalog_job.js +148 -0
- package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
- package/src/worker/check.js +24 -0
- package/src/worker/connection_monitor.js +112 -0
- package/src/worker/connection_monitor.spec.js +199 -0
- package/src/worker/index.js +140 -34
- package/src/worker/index.spec.js +280 -108
- package/src/worker/interceptors/activity.js +7 -24
- package/src/worker/interceptors/activity.spec.js +97 -66
- package/src/worker/interceptors/index.js +4 -7
- package/src/worker/interceptors/modules.js +15 -0
- package/src/worker/interceptors/workflow.js +4 -7
- package/src/worker/interceptors/workflow.spec.js +49 -42
- package/src/worker/interruption.js +33 -0
- package/src/worker/interruption.spec.js +86 -0
- package/src/worker/loader_tools.js +1 -1
- package/src/worker/loader_tools.spec.js +36 -0
- package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
- package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
- package/src/interface/validations/runtime.js +0 -20
- package/src/interface/validations/runtime.spec.js +0 -29
- package/src/interface/validations/schema_utils.js +0 -8
- package/src/interface/validations/schema_utils.spec.js +0 -67
- package/src/interface/validations/static.js +0 -137
- package/src/interface/validations/static.spec.js +0 -397
- package/src/interface/workflow.replay_compatibility.spec.js +0 -254
- package/src/worker/shutdown.js +0 -26
- package/src/worker/shutdown.spec.js +0 -82
- package/src/worker/start_catalog.js +0 -96
- package/src/worker/start_catalog.spec.js +0 -179
package/src/worker/index.js
CHANGED
|
@@ -7,34 +7,44 @@ import { init as initTracing } from '#tracing';
|
|
|
7
7
|
import { webpackConfigHook } from './bundler_options.js';
|
|
8
8
|
import { initInterceptors } from './interceptors/index.js';
|
|
9
9
|
import { createChildLogger } from '#logger';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { setupInterruptionHandler } from './interruption.js';
|
|
11
|
+
import { CatalogJob } from './catalog_workflow/catalog_job.js';
|
|
12
12
|
import { bootstrapFetchProxy } from './proxy.js';
|
|
13
13
|
import { messageBus } from '#bus';
|
|
14
14
|
import { BusEventType } from '#consts';
|
|
15
15
|
import { hashSourceCode } from './loader_tools.js';
|
|
16
|
-
import { setupTelemetry } from './
|
|
16
|
+
import { setupTelemetry } from './telemetry.js';
|
|
17
|
+
import { TemporalConnectionMonitor } from './connection_monitor.js';
|
|
18
|
+
import { runOnce } from '#utils';
|
|
19
|
+
|
|
17
20
|
import './log_hooks.js';
|
|
18
21
|
|
|
19
22
|
const log = createChildLogger( 'Worker' );
|
|
20
23
|
|
|
24
|
+
const {
|
|
25
|
+
address,
|
|
26
|
+
apiKey,
|
|
27
|
+
namespace,
|
|
28
|
+
taskQueue,
|
|
29
|
+
grpcProxy,
|
|
30
|
+
maxConcurrentWorkflowTaskExecutions,
|
|
31
|
+
maxConcurrentActivityTaskExecutions,
|
|
32
|
+
maxCachedWorkflows,
|
|
33
|
+
maxConcurrentActivityTaskPolls,
|
|
34
|
+
maxConcurrentWorkflowTaskPolls
|
|
35
|
+
} = configs;
|
|
36
|
+
|
|
37
|
+
const state = {
|
|
38
|
+
connection: null,
|
|
39
|
+
connectionMonitor: null,
|
|
40
|
+
catalogJob: null,
|
|
41
|
+
workerError: null
|
|
42
|
+
};
|
|
43
|
+
|
|
21
44
|
// Get caller directory from command line arguments
|
|
22
45
|
const callerDir = process.argv[2];
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
const {
|
|
26
|
-
address,
|
|
27
|
-
apiKey,
|
|
28
|
-
namespace,
|
|
29
|
-
taskQueue,
|
|
30
|
-
grpcProxy,
|
|
31
|
-
maxConcurrentWorkflowTaskExecutions,
|
|
32
|
-
maxConcurrentActivityTaskExecutions,
|
|
33
|
-
maxCachedWorkflows,
|
|
34
|
-
maxConcurrentActivityTaskPolls,
|
|
35
|
-
maxConcurrentWorkflowTaskPolls
|
|
36
|
-
} = configs;
|
|
37
|
-
|
|
47
|
+
const execute = async () => {
|
|
38
48
|
log.info( 'Loading config...', { callerDir } );
|
|
39
49
|
await loadHooks( callerDir );
|
|
40
50
|
|
|
@@ -64,17 +74,23 @@ const callerDir = process.argv[2];
|
|
|
64
74
|
if ( proxy ) {
|
|
65
75
|
log.info( 'Using gRPC proxy', { targetHost: grpcProxy } );
|
|
66
76
|
}
|
|
67
|
-
|
|
77
|
+
state.connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
|
|
78
|
+
|
|
79
|
+
log.info( 'Creating connection monitor...' );
|
|
80
|
+
state.connectionMonitor = new TemporalConnectionMonitor( state.connection );
|
|
81
|
+
|
|
82
|
+
log.info( 'Creating catalog job manager...' );
|
|
83
|
+
state.catalogJob = new CatalogJob( { connection: state.connection, namespace, catalog, catalogHash } );
|
|
68
84
|
|
|
69
85
|
log.info( 'Creating worker...' );
|
|
70
86
|
const worker = await Worker.create( {
|
|
71
|
-
connection,
|
|
87
|
+
connection: state.connection,
|
|
72
88
|
namespace,
|
|
73
89
|
taskQueue,
|
|
74
90
|
workflowsPath,
|
|
75
91
|
activities,
|
|
76
92
|
sinks,
|
|
77
|
-
interceptors: initInterceptors( { activities, workflows
|
|
93
|
+
interceptors: initInterceptors( { activities, workflows } ),
|
|
78
94
|
maxConcurrentWorkflowTaskExecutions,
|
|
79
95
|
maxConcurrentActivityTaskExecutions,
|
|
80
96
|
maxCachedWorkflows,
|
|
@@ -83,22 +99,112 @@ const callerDir = process.argv[2];
|
|
|
83
99
|
bundlerOptions: { webpackConfigHook }
|
|
84
100
|
} );
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
log.info( 'Setting up telemetry...' );
|
|
88
103
|
setupTelemetry( { worker } );
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
/**
|
|
106
|
+
* NOTE
|
|
107
|
+
* Temporal worker shutdown is a bit odd.
|
|
108
|
+
* worker.run() is an async job that only resolves when calling worker.shutdown().
|
|
109
|
+
* But worker.shutdown() is not async and returns nothing, so there is no way to await it.
|
|
110
|
+
* All code that needs to run after shutdown needs to be after `await worker.run()`.
|
|
111
|
+
*
|
|
112
|
+
* The following code needs to cover these scenarios:
|
|
113
|
+
* 1. Connection monitor detects connection loss
|
|
114
|
+
* 2. Catalog.run() has a failure
|
|
115
|
+
* 3. Interruption is received
|
|
116
|
+
* 4. Worker throws an error
|
|
117
|
+
*
|
|
118
|
+
* For each scenario all promises in the Promise.all() need to be completed via functions:
|
|
119
|
+
* connectionMonitor.stop(), catalogJob.interrupt(), worker.shutdown()
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Graceful shutdown
|
|
124
|
+
* Triggers the actions that will resolve all promises in the Promise.all(), so the code can resume
|
|
125
|
+
*/
|
|
126
|
+
const shutdown = runOnce( () => {
|
|
127
|
+
log.info( 'Shutdown started...' );
|
|
128
|
+
if ( worker.getStatus().runState === 'RUNNING' ) {
|
|
129
|
+
worker.shutdown();
|
|
130
|
+
}
|
|
131
|
+
state.connectionMonitor.stop();
|
|
132
|
+
state.catalogJob.interrupt();
|
|
133
|
+
} );
|
|
92
134
|
|
|
93
|
-
|
|
94
|
-
|
|
135
|
+
/** When receiving an interruption, call shutdown */
|
|
136
|
+
setupInterruptionHandler( shutdown );
|
|
95
137
|
|
|
96
|
-
|
|
138
|
+
/** When the connection is lost, call shutdown */
|
|
139
|
+
state.connectionMonitor.onConnectionLost( shutdown );
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
/** If the catalog job manager fails, call shutdown */
|
|
142
|
+
state.catalogJob.onError( shutdown );
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Runs the worker, connection monitor and catalogJob (ephemeral)
|
|
146
|
+
* None of these will reject in normal conditions. Errors need to be inspected later
|
|
147
|
+
* They will resolve only when calling the actions in shutdown(),
|
|
148
|
+
* except catalogJob, which can resolve by itself given a bit of time
|
|
149
|
+
*/
|
|
150
|
+
log.info( 'Running worker...' );
|
|
151
|
+
await Promise.all( [
|
|
152
|
+
// When the worker fails, store the error and call shutdown
|
|
153
|
+
worker.run().catch( error => {
|
|
154
|
+
state.workerError = error;
|
|
155
|
+
shutdown();
|
|
156
|
+
} ),
|
|
157
|
+
state.connectionMonitor.start(),
|
|
158
|
+
state.catalogJob.run()
|
|
159
|
+
] );
|
|
160
|
+
|
|
161
|
+
log.info( 'Worker terminated' );
|
|
162
|
+
|
|
163
|
+
/** After the Promise.all() is resolved, check which services had an error, since none rejects the promise */
|
|
164
|
+
const error =
|
|
165
|
+
state.connectionMonitor.connectionLossError ??
|
|
166
|
+
state.catalogJob.error ??
|
|
167
|
+
state.workerError;
|
|
168
|
+
|
|
169
|
+
/** If any error is found, throws it, so this process can exit with code=1 (failure) */
|
|
170
|
+
if ( error ) {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
execute()
|
|
176
|
+
.finally( async () => {
|
|
177
|
+
/**
|
|
178
|
+
* This will make sure that if we had any uncaught failures, everything is tore down.
|
|
179
|
+
* Ignore any errors here in order to not mask actually errors from before and because at this point
|
|
180
|
+
* the code is already shutting down, so no need to throw anyway.
|
|
181
|
+
*
|
|
182
|
+
* worker.shutdown() is not tried here, because it cannot be awaited. By this point is safe to say
|
|
183
|
+
* that worker never started, already crashed or was stopped anyway.
|
|
184
|
+
*/
|
|
185
|
+
if ( state.connectionMonitor?.running ) {
|
|
186
|
+
log.info( 'Stopping connection monitor...' );
|
|
187
|
+
await state.connectionMonitor.stop()
|
|
188
|
+
.catch( e => log.warn( 'Connection monitor stop error', { error: e.message } ) );
|
|
189
|
+
}
|
|
190
|
+
if ( state.catalogJob?.running ) {
|
|
191
|
+
log.info( 'Interrupting catalog job...' );
|
|
192
|
+
await state.catalogJob.interrupt()
|
|
193
|
+
.catch( e => log.warn( 'Catalog job interruption error', { error: e.message } ) );
|
|
194
|
+
}
|
|
195
|
+
if ( state.connection ) {
|
|
196
|
+
log.info( 'Closing connection...' );
|
|
197
|
+
await state.connection.close()
|
|
198
|
+
.catch( e => log.warn( 'Connection close error', { error: e.message } ) );
|
|
199
|
+
}
|
|
200
|
+
} )
|
|
201
|
+
.then( () => log.info( 'Bye' ) )
|
|
202
|
+
.catch( error => {
|
|
203
|
+
log.error( 'Fatal error', { error: error.message, stack: error.stack } );
|
|
204
|
+
|
|
205
|
+
messageBus.emit( BusEventType.RUNTIME_ERROR, { error } );
|
|
206
|
+
|
|
207
|
+
const timeToFlushEvent = configs.processFailureShutdownDelay;
|
|
208
|
+
log.info( `Exiting in ${timeToFlushEvent}ms` );
|
|
209
|
+
setTimeout( () => process.exit( 1 ), timeToFlushEvent );
|
|
210
|
+
} );
|
package/src/worker/index.spec.js
CHANGED
|
@@ -1,125 +1,232 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
catalogJobInstance,
|
|
5
|
+
configValues,
|
|
6
|
+
connectionMonitorInstance,
|
|
7
|
+
createCatalogMock,
|
|
8
|
+
createWorkflowsEntryPointMock,
|
|
9
|
+
hashSourceCodeMock,
|
|
10
|
+
initInterceptorsMock,
|
|
11
|
+
loadActivitiesMock,
|
|
12
|
+
loadHooksMock,
|
|
13
|
+
loadWorkflowsMock,
|
|
14
|
+
messageBusMock,
|
|
15
|
+
mockConnection,
|
|
16
|
+
mockLog,
|
|
17
|
+
mockWorker,
|
|
18
|
+
promises,
|
|
19
|
+
resetPromises,
|
|
20
|
+
setupInterruptionHandlerMock,
|
|
21
|
+
setupTelemetryMock
|
|
22
|
+
} = vi.hoisted( () => {
|
|
23
|
+
const createDeferred = () => {
|
|
24
|
+
const state = {};
|
|
25
|
+
state.promise = new Promise( ( resolve, reject ) => {
|
|
26
|
+
state.resolve = resolve;
|
|
27
|
+
state.reject = reject;
|
|
28
|
+
} );
|
|
29
|
+
return state;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const promises = {};
|
|
33
|
+
const resetPromises = () => {
|
|
34
|
+
promises.workerRun = createDeferred();
|
|
35
|
+
promises.connectionMonitor = createDeferred();
|
|
36
|
+
promises.catalogJob = createDeferred();
|
|
37
|
+
};
|
|
38
|
+
resetPromises();
|
|
39
|
+
|
|
40
|
+
const configValues = {
|
|
41
|
+
address: 'localhost:7233',
|
|
42
|
+
apiKey: undefined,
|
|
43
|
+
namespace: 'default',
|
|
44
|
+
taskQueue: 'test-queue',
|
|
45
|
+
catalogId: 'test-catalog',
|
|
46
|
+
grpcProxy: undefined,
|
|
47
|
+
maxConcurrentWorkflowTaskExecutions: 200,
|
|
48
|
+
maxConcurrentActivityTaskExecutions: 40,
|
|
49
|
+
maxCachedWorkflows: 1000,
|
|
50
|
+
maxConcurrentActivityTaskPolls: 5,
|
|
51
|
+
maxConcurrentWorkflowTaskPolls: 5,
|
|
52
|
+
processFailureShutdownDelay: 0
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const connectionMonitorInstance = {
|
|
56
|
+
running: false,
|
|
57
|
+
connectionLossError: null,
|
|
58
|
+
onConnectionLost: vi.fn( cb => {
|
|
59
|
+
connectionMonitorInstance.connectionLostCb = cb;
|
|
60
|
+
} ),
|
|
61
|
+
start: vi.fn( () => {
|
|
62
|
+
connectionMonitorInstance.running = true;
|
|
63
|
+
return promises.connectionMonitor.promise.finally( () => {
|
|
64
|
+
connectionMonitorInstance.running = false;
|
|
65
|
+
} );
|
|
66
|
+
} ),
|
|
67
|
+
stop: vi.fn( () => {
|
|
68
|
+
connectionMonitorInstance.running = false;
|
|
69
|
+
promises.connectionMonitor.resolve();
|
|
70
|
+
return promises.connectionMonitor.promise;
|
|
71
|
+
} ),
|
|
72
|
+
connectionLostCb: null
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const catalogJobInstance = {
|
|
76
|
+
running: false,
|
|
77
|
+
error: null,
|
|
78
|
+
onError: vi.fn( cb => {
|
|
79
|
+
catalogJobInstance.errorCb = cb;
|
|
80
|
+
} ),
|
|
81
|
+
run: vi.fn( () => {
|
|
82
|
+
catalogJobInstance.running = true;
|
|
83
|
+
return promises.catalogJob.promise.finally( () => {
|
|
84
|
+
catalogJobInstance.running = false;
|
|
85
|
+
} );
|
|
86
|
+
} ),
|
|
87
|
+
interrupt: vi.fn( () => {
|
|
88
|
+
catalogJobInstance.running = false;
|
|
89
|
+
promises.catalogJob.resolve();
|
|
90
|
+
return promises.catalogJob.promise;
|
|
91
|
+
} ),
|
|
92
|
+
errorCb: null
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const mockWorker = {
|
|
96
|
+
getStatus: vi.fn( () => ( { runState: mockWorker.runState } ) ),
|
|
97
|
+
run: vi.fn( () => promises.workerRun.promise ),
|
|
98
|
+
runState: 'RUNNING',
|
|
99
|
+
shutdown: vi.fn()
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
catalogJobInstance,
|
|
104
|
+
configValues,
|
|
105
|
+
connectionMonitorInstance,
|
|
106
|
+
createCatalogMock: vi.fn().mockReturnValue( { workflows: [], activities: {} } ),
|
|
107
|
+
createWorkflowsEntryPointMock: vi.fn().mockReturnValue( '/fake/workflows/path.js' ),
|
|
108
|
+
hashSourceCodeMock: vi.fn().mockResolvedValue( 'catalog-hash' ),
|
|
109
|
+
initInterceptorsMock: vi.fn().mockReturnValue( [] ),
|
|
110
|
+
loadActivitiesMock: vi.fn().mockResolvedValue( {} ),
|
|
111
|
+
loadHooksMock: vi.fn().mockResolvedValue( undefined ),
|
|
112
|
+
loadWorkflowsMock: vi.fn().mockResolvedValue( [] ),
|
|
113
|
+
messageBusMock: { emit: vi.fn(), on: vi.fn() },
|
|
114
|
+
mockConnection: { close: vi.fn().mockResolvedValue( undefined ) },
|
|
115
|
+
mockLog: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
|
|
116
|
+
mockWorker,
|
|
117
|
+
promises,
|
|
118
|
+
resetPromises,
|
|
119
|
+
setupInterruptionHandlerMock: vi.fn(),
|
|
120
|
+
setupTelemetryMock: vi.fn()
|
|
121
|
+
};
|
|
122
|
+
} );
|
|
2
123
|
|
|
3
|
-
const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
4
124
|
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
5
|
-
|
|
6
125
|
vi.mock( '#consts', async importOriginal => {
|
|
7
126
|
const actual = await importOriginal();
|
|
8
127
|
return { ...actual };
|
|
9
128
|
} );
|
|
10
|
-
|
|
11
129
|
vi.mock( '#tracing', () => ( { init: vi.fn().mockResolvedValue( undefined ) } ) );
|
|
12
|
-
|
|
13
|
-
const configValues = {
|
|
14
|
-
address: 'localhost:7233',
|
|
15
|
-
apiKey: undefined,
|
|
16
|
-
namespace: 'default',
|
|
17
|
-
taskQueue: 'test-queue',
|
|
18
|
-
catalogId: 'test-catalog',
|
|
19
|
-
grpcProxy: undefined,
|
|
20
|
-
maxConcurrentWorkflowTaskExecutions: 200,
|
|
21
|
-
maxConcurrentActivityTaskExecutions: 40,
|
|
22
|
-
maxCachedWorkflows: 1000,
|
|
23
|
-
maxConcurrentActivityTaskPolls: 5,
|
|
24
|
-
maxConcurrentWorkflowTaskPolls: 5,
|
|
25
|
-
processFailureShutdownDelay: 0
|
|
26
|
-
};
|
|
27
|
-
vi.mock( './configs.js', () => configValues );
|
|
28
|
-
|
|
29
|
-
const messageBusMock = { on: vi.fn(), emit: vi.fn() };
|
|
30
130
|
vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
|
|
31
|
-
|
|
32
|
-
const loadWorkflowsMock = vi.fn().mockResolvedValue( [] );
|
|
33
|
-
const loadActivitiesMock = vi.fn().mockResolvedValue( {} );
|
|
34
|
-
const loadHooksMock = vi.fn().mockResolvedValue( undefined );
|
|
35
|
-
const createWorkflowsEntryPointMock = vi.fn().mockReturnValue( '/fake/workflows/path.js' );
|
|
131
|
+
vi.mock( './configs.js', () => configValues );
|
|
36
132
|
vi.mock( './loader.js', () => ( {
|
|
37
|
-
|
|
133
|
+
createWorkflowsEntryPoint: createWorkflowsEntryPointMock,
|
|
38
134
|
loadActivities: loadActivitiesMock,
|
|
39
135
|
loadHooks: loadHooksMock,
|
|
40
|
-
|
|
136
|
+
loadWorkflows: loadWorkflowsMock
|
|
41
137
|
} ) );
|
|
42
|
-
|
|
43
|
-
const hashSourceCodeMock = vi.fn().mockResolvedValue( 'catalog-hash' );
|
|
44
138
|
vi.mock( './loader_tools.js', () => ( { hashSourceCode: hashSourceCodeMock } ) );
|
|
45
|
-
|
|
46
139
|
vi.mock( './sinks.js', () => ( { sinks: {} } ) );
|
|
47
|
-
|
|
48
|
-
const createCatalogMock = vi.fn().mockReturnValue( { workflows: [], activities: {} } );
|
|
49
140
|
vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
|
|
50
|
-
|
|
51
141
|
vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
52
|
-
|
|
53
|
-
const initInterceptorsMock = vi.fn().mockReturnValue( [] );
|
|
54
142
|
vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptorsMock } ) );
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
vi.mock( './
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
vi.mock( './
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
143
|
+
vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: vi.fn() } ) );
|
|
144
|
+
vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
|
|
145
|
+
vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
|
|
146
|
+
vi.mock( './connection_monitor.js', () => ( {
|
|
147
|
+
TemporalConnectionMonitor: vi.fn( function () {
|
|
148
|
+
return connectionMonitorInstance;
|
|
149
|
+
} )
|
|
150
|
+
} ) );
|
|
151
|
+
vi.mock( './catalog_workflow/catalog_job.js', () => ( {
|
|
152
|
+
CatalogJob: vi.fn( function () {
|
|
153
|
+
return catalogJobInstance;
|
|
154
|
+
} )
|
|
155
|
+
} ) );
|
|
68
156
|
vi.mock( './log_hooks.js', () => ( {} ) );
|
|
69
|
-
|
|
70
|
-
const runState = { resolve: null };
|
|
71
|
-
const runPromise = new Promise( r => {
|
|
72
|
-
runState.resolve = r;
|
|
73
|
-
} );
|
|
74
|
-
const shutdownMock = vi.fn();
|
|
75
|
-
const mockConnection = { close: vi.fn().mockResolvedValue( undefined ) };
|
|
76
|
-
const mockWorker = { run: () => runPromise, shutdown: shutdownMock };
|
|
77
|
-
|
|
78
157
|
vi.mock( '@temporalio/worker', () => ( {
|
|
79
|
-
|
|
80
|
-
|
|
158
|
+
NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) },
|
|
159
|
+
Worker: { create: vi.fn().mockResolvedValue( mockWorker ) }
|
|
81
160
|
} ) );
|
|
82
161
|
|
|
162
|
+
const importWorker = async () => {
|
|
163
|
+
vi.resetModules();
|
|
164
|
+
await import( './index.js' );
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const settleWorker = async () => {
|
|
168
|
+
promises.catalogJob.resolve();
|
|
169
|
+
promises.connectionMonitor.resolve();
|
|
170
|
+
promises.workerRun.resolve();
|
|
171
|
+
await vi.waitFor( () => expect( mockConnection.close ).toHaveBeenCalled() );
|
|
172
|
+
};
|
|
173
|
+
|
|
83
174
|
describe( 'worker/index', () => {
|
|
84
175
|
const exitMock = vi.fn();
|
|
85
176
|
const originalArgv = process.argv;
|
|
86
|
-
const originalExit = process.exit;
|
|
87
177
|
|
|
88
178
|
beforeEach( () => {
|
|
89
179
|
vi.clearAllMocks();
|
|
180
|
+
resetPromises();
|
|
181
|
+
configValues.apiKey = undefined;
|
|
182
|
+
configValues.grpcProxy = undefined;
|
|
183
|
+
catalogJobInstance.error = null;
|
|
184
|
+
catalogJobInstance.errorCb = null;
|
|
185
|
+
catalogJobInstance.running = false;
|
|
186
|
+
connectionMonitorInstance.connectionLossError = null;
|
|
187
|
+
connectionMonitorInstance.connectionLostCb = null;
|
|
188
|
+
connectionMonitorInstance.running = false;
|
|
189
|
+
mockConnection.close.mockResolvedValue( undefined );
|
|
190
|
+
mockWorker.runState = 'RUNNING';
|
|
90
191
|
process.argv = [ ...originalArgv.slice( 0, 2 ), '/test/caller/dir' ];
|
|
91
|
-
process
|
|
192
|
+
vi.spyOn( process, 'exit' ).mockImplementation( exitMock );
|
|
92
193
|
} );
|
|
93
194
|
|
|
94
195
|
afterEach( () => {
|
|
95
196
|
process.argv = originalArgv;
|
|
96
|
-
|
|
97
|
-
configValues.apiKey = undefined;
|
|
197
|
+
vi.restoreAllMocks();
|
|
98
198
|
} );
|
|
99
199
|
|
|
100
|
-
it( '
|
|
101
|
-
const {
|
|
102
|
-
const {
|
|
200
|
+
it( 'creates the worker lifecycle jobs with expected dependencies', async () => {
|
|
201
|
+
const { NativeConnection, Worker } = await import( '@temporalio/worker' );
|
|
202
|
+
const { TemporalConnectionMonitor } = await import( './connection_monitor.js' );
|
|
203
|
+
const { CatalogJob } = await import( './catalog_workflow/catalog_job.js' );
|
|
103
204
|
|
|
104
|
-
|
|
205
|
+
await importWorker();
|
|
105
206
|
|
|
106
|
-
await vi.waitFor( () =>
|
|
107
|
-
|
|
108
|
-
|
|
207
|
+
await vi.waitFor( () => expect( Worker.create ).toHaveBeenCalled() );
|
|
208
|
+
|
|
209
|
+
expect( loadHooksMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
109
210
|
expect( loadWorkflowsMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
110
211
|
expect( loadActivitiesMock ).toHaveBeenCalledWith( '/test/caller/dir', [] );
|
|
111
212
|
expect( createWorkflowsEntryPointMock ).toHaveBeenCalledWith( [] );
|
|
112
|
-
expect( initTracing ).toHaveBeenCalled();
|
|
113
213
|
expect( createCatalogMock ).toHaveBeenCalledWith( { workflows: [], activities: {} } );
|
|
114
214
|
expect( hashSourceCodeMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
115
|
-
expect( bootstrapFetchProxyMock ).toHaveBeenCalled();
|
|
116
215
|
expect( NativeConnection.connect ).toHaveBeenCalledWith( {
|
|
117
216
|
address: configValues.address,
|
|
118
217
|
tls: false,
|
|
119
218
|
apiKey: undefined,
|
|
120
219
|
proxy: undefined
|
|
121
220
|
} );
|
|
221
|
+
expect( TemporalConnectionMonitor ).toHaveBeenCalledWith( mockConnection );
|
|
222
|
+
expect( CatalogJob ).toHaveBeenCalledWith( {
|
|
223
|
+
connection: mockConnection,
|
|
224
|
+
namespace: configValues.namespace,
|
|
225
|
+
catalog: { workflows: [], activities: {} },
|
|
226
|
+
catalogHash: 'catalog-hash'
|
|
227
|
+
} );
|
|
122
228
|
expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
229
|
+
connection: mockConnection,
|
|
123
230
|
namespace: configValues.namespace,
|
|
124
231
|
taskQueue: configValues.taskQueue,
|
|
125
232
|
workflowsPath: '/fake/workflows/path.js',
|
|
@@ -130,63 +237,128 @@ describe( 'worker/index', () => {
|
|
|
130
237
|
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
131
238
|
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
132
239
|
} ) );
|
|
133
|
-
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: []
|
|
134
|
-
expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
|
|
240
|
+
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
|
|
135
241
|
expect( setupTelemetryMock ).toHaveBeenCalledWith( { worker: mockWorker } );
|
|
136
|
-
expect(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
expect( mockConnection.close ).toHaveBeenCalled();
|
|
146
|
-
} );
|
|
147
|
-
expect( exitMock ).toHaveBeenCalledWith( 0 );
|
|
242
|
+
expect( setupInterruptionHandlerMock ).toHaveBeenCalledWith( expect.any( Function ) );
|
|
243
|
+
expect( connectionMonitorInstance.onConnectionLost ).toHaveBeenCalledWith( expect.any( Function ) );
|
|
244
|
+
expect( catalogJobInstance.onError ).toHaveBeenCalledWith( expect.any( Function ) );
|
|
245
|
+
expect( mockWorker.run ).toHaveBeenCalled();
|
|
246
|
+
expect( connectionMonitorInstance.start ).toHaveBeenCalled();
|
|
247
|
+
expect( catalogJobInstance.run ).toHaveBeenCalled();
|
|
248
|
+
|
|
249
|
+
await settleWorker();
|
|
250
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
148
251
|
} );
|
|
149
252
|
|
|
150
253
|
it( 'enables TLS when apiKey is set', async () => {
|
|
151
254
|
configValues.apiKey = 'secret';
|
|
152
|
-
vi.resetModules();
|
|
153
|
-
|
|
154
255
|
const { NativeConnection } = await import( '@temporalio/worker' );
|
|
155
|
-
|
|
256
|
+
|
|
257
|
+
await importWorker();
|
|
258
|
+
|
|
259
|
+
await vi.waitFor( () => expect( NativeConnection.connect ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
260
|
+
apiKey: 'secret',
|
|
261
|
+
tls: true
|
|
262
|
+
} ) ) );
|
|
263
|
+
|
|
264
|
+
await settleWorker();
|
|
265
|
+
} );
|
|
266
|
+
|
|
267
|
+
it( 'runs graceful shutdown when interrupted', async () => {
|
|
268
|
+
await importWorker();
|
|
269
|
+
|
|
270
|
+
await vi.waitFor( () => expect( setupInterruptionHandlerMock ).toHaveBeenCalled() );
|
|
271
|
+
const [ shutdown ] = setupInterruptionHandlerMock.mock.calls[0];
|
|
272
|
+
|
|
273
|
+
shutdown();
|
|
274
|
+
|
|
275
|
+
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
276
|
+
expect( connectionMonitorInstance.stop ).toHaveBeenCalledOnce();
|
|
277
|
+
expect( catalogJobInstance.interrupt ).toHaveBeenCalledOnce();
|
|
278
|
+
|
|
279
|
+
promises.workerRun.resolve();
|
|
280
|
+
await vi.waitFor( () => expect( mockConnection.close ).toHaveBeenCalled() );
|
|
281
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
282
|
+
} );
|
|
283
|
+
|
|
284
|
+
it( 'does not call worker.shutdown when worker has already failed', async () => {
|
|
285
|
+
const error = new Error( 'Big Failure' );
|
|
286
|
+
|
|
287
|
+
await importWorker();
|
|
288
|
+
await vi.waitFor( () => expect( mockWorker.run ).toHaveBeenCalled() );
|
|
289
|
+
|
|
290
|
+
mockWorker.runState = 'FAILED';
|
|
291
|
+
promises.workerRun.reject( error );
|
|
292
|
+
|
|
293
|
+
await vi.waitFor( () => expect( connectionMonitorInstance.stop ).toHaveBeenCalled() );
|
|
294
|
+
expect( mockWorker.shutdown ).not.toHaveBeenCalled();
|
|
295
|
+
|
|
296
|
+
promises.connectionMonitor.resolve();
|
|
297
|
+
promises.catalogJob.resolve();
|
|
156
298
|
|
|
157
299
|
await vi.waitFor( () => {
|
|
158
|
-
expect(
|
|
159
|
-
|
|
160
|
-
apiKey: 'secret'
|
|
300
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
301
|
+
error: 'Big Failure'
|
|
161
302
|
} ) );
|
|
162
303
|
} );
|
|
163
|
-
|
|
164
|
-
await vi.waitFor( () => expect( exitMock ).
|
|
304
|
+
expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
305
|
+
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
|
|
165
306
|
} );
|
|
166
307
|
|
|
167
|
-
it( '
|
|
168
|
-
|
|
308
|
+
it( 'throws connection monitor errors after graceful shutdown', async () => {
|
|
309
|
+
const error = new Error( 'connection lost' );
|
|
169
310
|
|
|
170
|
-
|
|
311
|
+
await importWorker();
|
|
312
|
+
await vi.waitFor( () => expect( connectionMonitorInstance.onConnectionLost ).toHaveBeenCalled() );
|
|
313
|
+
|
|
314
|
+
connectionMonitorInstance.connectionLossError = error;
|
|
315
|
+
connectionMonitorInstance.connectionLostCb( error );
|
|
316
|
+
promises.workerRun.resolve();
|
|
171
317
|
|
|
172
318
|
await vi.waitFor( () => {
|
|
173
|
-
expect(
|
|
319
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
320
|
+
error: 'connection lost'
|
|
321
|
+
} ) );
|
|
174
322
|
} );
|
|
175
|
-
|
|
176
|
-
|
|
323
|
+
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
324
|
+
expect( catalogJobInstance.interrupt ).toHaveBeenCalledOnce();
|
|
325
|
+
expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
177
326
|
} );
|
|
178
327
|
|
|
179
|
-
it( '
|
|
180
|
-
|
|
181
|
-
|
|
328
|
+
it( 'throws catalog job errors after graceful shutdown', async () => {
|
|
329
|
+
const error = new Error( 'catalog failed' );
|
|
330
|
+
|
|
331
|
+
await importWorker();
|
|
332
|
+
await vi.waitFor( () => expect( catalogJobInstance.onError ).toHaveBeenCalled() );
|
|
182
333
|
|
|
183
|
-
|
|
334
|
+
catalogJobInstance.error = error;
|
|
335
|
+
catalogJobInstance.errorCb( error );
|
|
336
|
+
promises.workerRun.resolve();
|
|
184
337
|
|
|
185
338
|
await vi.waitFor( () => {
|
|
186
|
-
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.
|
|
339
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
340
|
+
error: 'catalog failed'
|
|
341
|
+
} ) );
|
|
187
342
|
} );
|
|
343
|
+
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
344
|
+
expect( connectionMonitorInstance.stop ).toHaveBeenCalledOnce();
|
|
345
|
+
expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
346
|
+
} );
|
|
347
|
+
|
|
348
|
+
it( 'cleans up partial startup failures after connecting', async () => {
|
|
349
|
+
const { Worker } = await import( '@temporalio/worker' );
|
|
350
|
+
const error = new Error( 'worker create failed' );
|
|
351
|
+
Worker.create.mockRejectedValueOnce( error );
|
|
352
|
+
|
|
353
|
+
await importWorker();
|
|
354
|
+
|
|
355
|
+
await vi.waitFor( () => expect( mockConnection.close ).toHaveBeenCalled() );
|
|
356
|
+
expect( connectionMonitorInstance.stop ).not.toHaveBeenCalled();
|
|
357
|
+
expect( catalogJobInstance.interrupt ).not.toHaveBeenCalled();
|
|
188
358
|
await vi.waitFor( () => {
|
|
189
|
-
expect(
|
|
359
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
360
|
+
error: 'worker create failed'
|
|
361
|
+
} ) );
|
|
190
362
|
} );
|
|
191
363
|
} );
|
|
192
364
|
} );
|