@outputai/core 0.10.1-next.09ed166.0 → 0.10.1-next.2e221c7.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 +2 -1
- package/src/helpers/fetch.js +1 -1
- package/src/hooks/index.js +9 -7
- package/src/hooks/index.spec.js +19 -0
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/index.js +2 -1
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/worker/configs.js +0 -3
- package/src/worker/index.js +11 -7
- package/src/worker/index.spec.js +7 -2
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.10.1-next.
|
|
3
|
+
"version": "0.10.1-next.2e221c7.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"#consts": "./src/consts.js",
|
|
70
70
|
"#errors": "./src/errors.js",
|
|
71
71
|
"#logger": "./src/logger/index.js",
|
|
72
|
+
"#hooks/*": "./src/hooks/*.js",
|
|
72
73
|
"#helpers/*": "./src/helpers/*.js",
|
|
73
74
|
"#tracing": "./src/tracing/internal_interface.js",
|
|
74
75
|
"#trace_attribute": "./src/tracing/trace_attribute.js",
|
package/src/helpers/fetch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FatalError } from '#errors';
|
|
2
2
|
|
|
3
|
-
/** Matches red
|
|
3
|
+
/** Matches red in "hot-red-pie", but not in "redact" */
|
|
4
4
|
const wordMatcher = term => new RegExp( `(?<![a-z\\d])${term}(?![a-z\\d])`, 'i' );
|
|
5
5
|
|
|
6
6
|
/** Matches red in "acquired", but not in "redact" */
|
package/src/hooks/index.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { mainEventBus, stepEventBus } from '#bus';
|
|
2
2
|
import { BusEventType } from '#consts';
|
|
3
3
|
import { createChildLogger } from '#logger';
|
|
4
|
+
import { pendingHooks } from './pending_hooks.js';
|
|
4
5
|
|
|
5
6
|
const log = createChildLogger( 'Hooks' );
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Invokes a function within a try catch and log the error
|
|
9
|
+
* Invokes a function within a try catch and log the error.
|
|
9
10
|
*
|
|
10
11
|
* @param {Function} fn
|
|
11
12
|
* @param {any} args - Args to invoke the function with
|
|
12
13
|
* @param {string} hookName - hookName to identify this hook function in the logs
|
|
13
14
|
*/
|
|
14
|
-
const callHookCb =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const callHookCb = ( fn, args, hookName ) => {
|
|
16
|
+
const promise = Promise.resolve()
|
|
17
|
+
.then( () => fn( args ) )
|
|
18
|
+
.catch( e => log.error( `${hookName} hook error`, { message: e.message, stack: e.stack } ) )
|
|
19
|
+
.finally( () => pendingHooks.delete( promise ) );
|
|
20
|
+
pendingHooks.add( promise );
|
|
21
|
+
return promise;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
// General Life-cycle
|
package/src/hooks/index.spec.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
onWorkflowError,
|
|
39
39
|
onWorkflowStart
|
|
40
40
|
} from './index.js';
|
|
41
|
+
import { pendingHooks } from './pending_hooks.js';
|
|
41
42
|
|
|
42
43
|
const workflowDetails = {
|
|
43
44
|
workflowId: 'wf-1',
|
|
@@ -67,6 +68,7 @@ const aggregations = {
|
|
|
67
68
|
describe( 'hooks/index', () => {
|
|
68
69
|
beforeEach( () => {
|
|
69
70
|
vi.clearAllMocks();
|
|
71
|
+
pendingHooks.clear();
|
|
70
72
|
Object.keys( mainOnHandlers ).forEach( k => {
|
|
71
73
|
delete mainOnHandlers[k];
|
|
72
74
|
} );
|
|
@@ -158,6 +160,23 @@ describe( 'hooks/index', () => {
|
|
|
158
160
|
} );
|
|
159
161
|
} );
|
|
160
162
|
|
|
163
|
+
it( 'tracks a hook callback until it settles', async () => {
|
|
164
|
+
const deferred = { resolve: null };
|
|
165
|
+
const handlerPromise = new Promise( resolve => {
|
|
166
|
+
deferred.resolve = resolve;
|
|
167
|
+
} );
|
|
168
|
+
const handler = vi.fn( () => handlerPromise );
|
|
169
|
+
onActivityEnd( handler );
|
|
170
|
+
|
|
171
|
+
const callbackPromise = mainOnHandlers[BusEventType.ACTIVITY_END]( { activityInfo } );
|
|
172
|
+
|
|
173
|
+
expect( pendingHooks.size ).toBe( 1 );
|
|
174
|
+
|
|
175
|
+
deferred.resolve();
|
|
176
|
+
await callbackPromise;
|
|
177
|
+
expect( pendingHooks.size ).toBe( 0 );
|
|
178
|
+
} );
|
|
179
|
+
|
|
161
180
|
describe( 'workflow lifecycle hooks', () => {
|
|
162
181
|
const cases = [
|
|
163
182
|
[ 'onWorkflowStart', onWorkflowStart, BusEventType.WORKFLOW_START, {} ],
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createChildLogger } from '#logger';
|
|
2
|
+
|
|
3
|
+
export const pendingHooks = new Set();
|
|
4
|
+
|
|
5
|
+
const log = createChildLogger( 'Hooks' );
|
|
6
|
+
const flushTimeoutMs = 30_000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Await all pending hooks to flush for a certain time
|
|
10
|
+
*/
|
|
11
|
+
export const flushPendingHooks = async () => {
|
|
12
|
+
const state = { timeout: null };
|
|
13
|
+
try {
|
|
14
|
+
const flushed = await Promise.race( [
|
|
15
|
+
Promise.allSettled( [ ...pendingHooks ] ).then( _ => true ),
|
|
16
|
+
new Promise( r => state.timeout = setTimeout( () => r( false ), flushTimeoutMs ) )
|
|
17
|
+
] );
|
|
18
|
+
if ( !flushed ) {
|
|
19
|
+
log.warn( 'Some hook callbacks exceeded the timeout and will not be awaited', { count: pendingHooks.size } );
|
|
20
|
+
}
|
|
21
|
+
} finally {
|
|
22
|
+
clearTimeout( state.timeout );
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const logMock = vi.hoisted( () => ( { warn: vi.fn() } ) );
|
|
4
|
+
|
|
5
|
+
vi.mock( '#logger', () => ( { createChildLogger: () => logMock } ) );
|
|
6
|
+
|
|
7
|
+
import { flushPendingHooks, pendingHooks } from './pending_hooks.js';
|
|
8
|
+
|
|
9
|
+
describe( 'pending hooks', () => {
|
|
10
|
+
beforeEach( () => {
|
|
11
|
+
pendingHooks.clear();
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
afterEach( () => {
|
|
16
|
+
pendingHooks.clear();
|
|
17
|
+
vi.useRealTimers();
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'waits for pending hooks to settle', async () => {
|
|
21
|
+
const deferred = { resolve: null };
|
|
22
|
+
const hookPromise = new Promise( resolve => {
|
|
23
|
+
deferred.resolve = resolve;
|
|
24
|
+
} );
|
|
25
|
+
pendingHooks.add( hookPromise );
|
|
26
|
+
|
|
27
|
+
const flushPromise = flushPendingHooks();
|
|
28
|
+
const state = { flushed: false };
|
|
29
|
+
flushPromise.then( () => {
|
|
30
|
+
state.flushed = true;
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
await Promise.resolve();
|
|
34
|
+
expect( state.flushed ).toBe( false );
|
|
35
|
+
|
|
36
|
+
deferred.resolve();
|
|
37
|
+
await flushPromise;
|
|
38
|
+
expect( state.flushed ).toBe( true );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'stops waiting after the timeout', async () => {
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
44
|
+
|
|
45
|
+
const flushPromise = flushPendingHooks();
|
|
46
|
+
await vi.advanceTimersByTimeAsync( 30_000 );
|
|
47
|
+
|
|
48
|
+
await expect( flushPromise ).resolves.toBeUndefined();
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'logs the number of hooks that exceeded the timeout', async () => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
54
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
55
|
+
|
|
56
|
+
const flushPromise = flushPendingHooks();
|
|
57
|
+
await vi.advanceTimersByTimeAsync( 30_000 );
|
|
58
|
+
await flushPromise;
|
|
59
|
+
|
|
60
|
+
expect( logMock.warn ).toHaveBeenCalledWith( expect.any( String ), { count: 2 } );
|
|
61
|
+
} );
|
|
62
|
+
} );
|
package/src/interface/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { evaluator } from './evaluator.js';
|
|
9
9
|
import { step } from './step.js';
|
|
10
10
|
import { workflow } from './workflow.js';
|
|
11
|
-
import { executeInParallel } from './workflow_utils.js';
|
|
11
|
+
import { executeInParallel, hasErrorType } from './workflow_utils.js';
|
|
12
12
|
import { sendHttpRequest, sendPostRequestAndAwaitWebhook } from './webhook.js';
|
|
13
13
|
import { Logger } from './logger.js';
|
|
14
14
|
|
|
@@ -23,6 +23,7 @@ export {
|
|
|
23
23
|
EvaluationVerdictResult,
|
|
24
24
|
EvaluationFeedback,
|
|
25
25
|
executeInParallel,
|
|
26
|
+
hasErrorType,
|
|
26
27
|
sendHttpRequest,
|
|
27
28
|
sendPostRequestAndAwaitWebhook
|
|
28
29
|
};
|
|
@@ -74,3 +74,18 @@ export declare function executeInParallel<T extends readonly ( () => Promise<unk
|
|
|
74
74
|
onJobCompleted?: ( result: ParallelResults<T>[number] ) => void;
|
|
75
75
|
}
|
|
76
76
|
): Promise<ParallelResults<T>>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks whether an error or any error in its cause chain matches an Error class.
|
|
80
|
+
*
|
|
81
|
+
* Temporal Application Failures are matched through their serialized `type`,
|
|
82
|
+
* in addition to standard `instanceof` and `name` checks.
|
|
83
|
+
*
|
|
84
|
+
* @param error - Error or failure chain to inspect
|
|
85
|
+
* @param ErrorType - Error class to match
|
|
86
|
+
* @returns Whether the Error class appears in the chain
|
|
87
|
+
*/
|
|
88
|
+
export declare function hasErrorType<T extends Error>(
|
|
89
|
+
error: unknown,
|
|
90
|
+
ErrorType: abstract new ( ...args: never[] ) => T
|
|
91
|
+
): boolean;
|
|
@@ -48,3 +48,22 @@ export const executeInParallel = async ( { jobs, concurrency = Infinity, onJobCo
|
|
|
48
48
|
|
|
49
49
|
return results.sort( ( a, b ) => a.index - b.index );
|
|
50
50
|
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if an error, or any parts of its cause chain are instances of a given Error class.
|
|
54
|
+
* Also check .name and .type
|
|
55
|
+
*
|
|
56
|
+
* @param {Error} error
|
|
57
|
+
* @param {Error} ErrorType
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
export const hasErrorType = ( error, ErrorType, depth = 0 ) => {
|
|
61
|
+
if ( depth > 20 || typeof ErrorType !== 'function' ) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const expectedType = ErrorType.name;
|
|
65
|
+
if ( error instanceof ErrorType || error?.type === expectedType || error?.name === expectedType || error === expectedType ) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return error?.cause ? hasErrorType( error.cause, ErrorType, depth + 1 ) : false;
|
|
69
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { executeInParallel } from './workflow_utils.js';
|
|
2
|
+
import { hasErrorType, executeInParallel } from './workflow_utils.js';
|
|
3
3
|
|
|
4
4
|
describe( 'executeInParallel', () => {
|
|
5
5
|
it( 'returns empty array for empty jobs', async () => {
|
|
@@ -188,3 +188,84 @@ describe( 'executeInParallel', () => {
|
|
|
188
188
|
] );
|
|
189
189
|
} );
|
|
190
190
|
} );
|
|
191
|
+
|
|
192
|
+
describe( 'hasErrorType', () => {
|
|
193
|
+
class FooError extends Error {}
|
|
194
|
+
|
|
195
|
+
it( 'matches a direct error instance', () => {
|
|
196
|
+
expect( hasErrorType( new FooError( 'foo' ), FooError ) ).toBe( true );
|
|
197
|
+
} );
|
|
198
|
+
|
|
199
|
+
it( 'matches an error instance in the cause chain', () => {
|
|
200
|
+
const error = new Error( 'outer', {
|
|
201
|
+
cause: new Error( 'middle', {
|
|
202
|
+
cause: new FooError( 'foo' )
|
|
203
|
+
} )
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
expect( hasErrorType( error, FooError ) ).toBe( true );
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
it( 'matches Temporal ApplicationFailure type in the cause chain', () => {
|
|
210
|
+
const applicationFailure = Object.assign( new Error( 'foo' ), {
|
|
211
|
+
name: 'ApplicationFailure',
|
|
212
|
+
type: 'FooError'
|
|
213
|
+
} );
|
|
214
|
+
const activityFailure = Object.assign( new Error( 'Activity execution failed', {
|
|
215
|
+
cause: applicationFailure
|
|
216
|
+
} ), {
|
|
217
|
+
name: 'ActivityFailure'
|
|
218
|
+
} );
|
|
219
|
+
|
|
220
|
+
expect( hasErrorType( activityFailure, FooError ) ).toBe( true );
|
|
221
|
+
} );
|
|
222
|
+
|
|
223
|
+
it( 'matches a serialized error name', () => {
|
|
224
|
+
const error = Object.assign( new Error( 'foo' ), { name: 'FooError' } );
|
|
225
|
+
|
|
226
|
+
expect( hasErrorType( error, FooError ) ).toBe( true );
|
|
227
|
+
} );
|
|
228
|
+
|
|
229
|
+
it( 'matches an error serialized as its type name', () => {
|
|
230
|
+
expect( hasErrorType( 'FooError', FooError ) ).toBe( true );
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
it( 'matches an error type in a plain object cause chain', () => {
|
|
234
|
+
const error = {
|
|
235
|
+
name: 'ActivityFailure',
|
|
236
|
+
cause: {
|
|
237
|
+
name: 'ApplicationFailure',
|
|
238
|
+
type: 'FooError'
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
expect( hasErrorType( error, FooError ) ).toBe( true );
|
|
243
|
+
} );
|
|
244
|
+
|
|
245
|
+
it.each( [ null, undefined, '', 42, false ] )( 'returns false for unsupported error value %j', error => {
|
|
246
|
+
expect( hasErrorType( error, FooError ) ).toBe( false );
|
|
247
|
+
} );
|
|
248
|
+
|
|
249
|
+
it( 'returns false when ErrorType is not a class', () => {
|
|
250
|
+
expect( hasErrorType( new FooError( 'foo' ), null ) ).toBe( false );
|
|
251
|
+
} );
|
|
252
|
+
|
|
253
|
+
it( 'returns false when the requested error type is absent', () => {
|
|
254
|
+
const error = new Error( 'outer', { cause: new TypeError( 'inner' ) } );
|
|
255
|
+
|
|
256
|
+
expect( hasErrorType( error, FooError ) ).toBe( false );
|
|
257
|
+
} );
|
|
258
|
+
|
|
259
|
+
it( 'keeps plain Error matching backwards compatible', () => {
|
|
260
|
+
expect( hasErrorType( new Error( 'foo' ), Error ) ).toBe( true );
|
|
261
|
+
} );
|
|
262
|
+
|
|
263
|
+
it( 'stops traversing cause chains after the depth limit', () => {
|
|
264
|
+
const createCauseChain = depth => depth === 20 ?
|
|
265
|
+
new FooError( 'foo' ) :
|
|
266
|
+
new Error( `depth-${depth}`, { cause: createCauseChain( depth + 1 ) } );
|
|
267
|
+
const root = new Error( 'root', { cause: createCauseChain( 0 ) } );
|
|
268
|
+
|
|
269
|
+
expect( hasErrorType( root, FooError ) ).toBe( false );
|
|
270
|
+
} );
|
|
271
|
+
} );
|
package/src/worker/configs.js
CHANGED
|
@@ -36,8 +36,6 @@ const envVarSchema = z.object( {
|
|
|
36
36
|
OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 2 * 60 * 1000 ) ), // 2min
|
|
37
37
|
// Whether to send activity heartbeats (enabled by default)
|
|
38
38
|
OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: z.transform( v => v === undefined ? true : isStringboolTrue( v ) ),
|
|
39
|
-
// Time to allow for hooks to flush before shutdown
|
|
40
|
-
OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 3000 ) ),
|
|
41
39
|
// Set temporal worker shutdown force time
|
|
42
40
|
TEMPORAL_SHUTDOWN_FORCE_TIME: durationSchema,
|
|
43
41
|
// Set temporal worker shutdown grace time
|
|
@@ -70,7 +68,6 @@ export const catalogId = envVars.OUTPUT_CATALOG_ID;
|
|
|
70
68
|
export const workerTelemetryIntervalMs = envVars.OUTPUT_WORKER_TELEMETRY_INTERVAL_MS;
|
|
71
69
|
export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
|
|
72
70
|
export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
|
|
73
|
-
export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
|
|
74
71
|
export const shutdownForceTime = envVars.TEMPORAL_SHUTDOWN_FORCE_TIME;
|
|
75
72
|
export const shutdownGraceTime = envVars.TEMPORAL_SHUTDOWN_GRACE_TIME;
|
|
76
73
|
export const grpcProxy = envVars.TEMPORAL_GRPC_PROXY;
|
package/src/worker/index.js
CHANGED
|
@@ -12,8 +12,8 @@ import { initInterceptors } from './interceptors/index.js';
|
|
|
12
12
|
import { createChildLogger } from '#logger';
|
|
13
13
|
import { setupInterruptionHandler } from './interruption.js';
|
|
14
14
|
import { CatalogJob } from './catalog_workflow/catalog_job.js';
|
|
15
|
-
import { bootstrapFetchProxy } from './proxy.js';
|
|
16
15
|
import { mainEventBus } from '#bus';
|
|
16
|
+
import { flushPendingHooks } from '#hooks/pending_hooks';
|
|
17
17
|
import { BusEventType } from '#consts';
|
|
18
18
|
import { setupTelemetry } from './telemetry.js';
|
|
19
19
|
import { TemporalConnectionMonitor } from './connection_monitor.js';
|
|
@@ -61,7 +61,6 @@ const execute = async () => {
|
|
|
61
61
|
const { activities } = await loadActivities( callerDir, workflows );
|
|
62
62
|
|
|
63
63
|
mainEventBus.emit( BusEventType.WORKER_BEFORE_START );
|
|
64
|
-
bootstrapFetchProxy();
|
|
65
64
|
|
|
66
65
|
log.info( 'Initializing tracing...' );
|
|
67
66
|
await initTracing();
|
|
@@ -214,13 +213,18 @@ execute()
|
|
|
214
213
|
.catch( e => log.warn( 'Connection close error', { error: e.message } ) );
|
|
215
214
|
}
|
|
216
215
|
} )
|
|
217
|
-
.then( () =>
|
|
218
|
-
|
|
216
|
+
.then( async () => {
|
|
217
|
+
log.info( 'Flushing hook callbacks...' );
|
|
218
|
+
await flushPendingHooks();
|
|
219
|
+
log.info( 'Bye' );
|
|
220
|
+
} )
|
|
221
|
+
.catch( async error => {
|
|
219
222
|
log.error( 'Fatal error', { error: error.message, stack: error.stack } );
|
|
220
223
|
|
|
221
224
|
mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error } );
|
|
222
225
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
log.info( 'Flushing hook callbacks...' );
|
|
227
|
+
await flushPendingHooks();
|
|
228
|
+
log.info( 'Exiting...' );
|
|
229
|
+
setTimeout( () => process.exit( 1 ) );
|
|
226
230
|
} );
|
package/src/worker/index.spec.js
CHANGED
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
configValues,
|
|
7
7
|
connectionMonitorInstance,
|
|
8
8
|
createCatalogMock,
|
|
9
|
+
flushPendingHooksMock,
|
|
9
10
|
initInterceptorsMock,
|
|
10
11
|
mainEventBusMock,
|
|
11
12
|
mockConnection,
|
|
@@ -46,7 +47,6 @@ const {
|
|
|
46
47
|
maxConcurrentActivityTaskPolls: 5,
|
|
47
48
|
maxConcurrentWorkflowTaskPolls: 5,
|
|
48
49
|
workerTuner: undefined,
|
|
49
|
-
processFailureShutdownDelay: 0,
|
|
50
50
|
shutdownForceTime: undefined,
|
|
51
51
|
shutdownGraceTime: undefined
|
|
52
52
|
};
|
|
@@ -105,6 +105,7 @@ const {
|
|
|
105
105
|
connectionMonitorInstance,
|
|
106
106
|
createCatalogMock: vi.fn().mockReturnValue( { workflows: [], activities: {} } ),
|
|
107
107
|
createWorkflowsEntryPointMock: vi.fn().mockReturnValue( '/fake/workflows/path.js' ),
|
|
108
|
+
flushPendingHooksMock: vi.fn().mockResolvedValue( undefined ),
|
|
108
109
|
hashSourceCodeMock: vi.fn().mockResolvedValue( 'catalog-hash' ),
|
|
109
110
|
initInterceptorsMock: vi.fn().mockReturnValue( [] ),
|
|
110
111
|
loadActivitiesMock: vi.fn().mockResolvedValue( {} ),
|
|
@@ -129,6 +130,7 @@ vi.mock( '#consts', async importOriginal => {
|
|
|
129
130
|
const initTracing = vi.fn().mockResolvedValue( undefined );
|
|
130
131
|
vi.mock( '#tracing', () => ( { init: initTracing } ) );
|
|
131
132
|
vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
|
|
133
|
+
vi.mock( '#hooks/pending_hooks', () => ( { flushPendingHooks: flushPendingHooksMock } ) );
|
|
132
134
|
|
|
133
135
|
const loadWorkflowsMock = vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/path.js' } );
|
|
134
136
|
const loadActivitiesMock = vi.fn().mockResolvedValue( { activities: {} } );
|
|
@@ -145,7 +147,6 @@ vi.mock( './sinks.js', () => ( { sinks: {} } ) );
|
|
|
145
147
|
vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
|
|
146
148
|
vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
147
149
|
vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptorsMock } ) );
|
|
148
|
-
vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: vi.fn() } ) );
|
|
149
150
|
vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
|
|
150
151
|
vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
|
|
151
152
|
vi.mock( './global_functions.js', () => ( { bindGlobalFunctions: bindGlobalFunctionsMock } ) );
|
|
@@ -262,6 +263,7 @@ describe( 'worker/index', () => {
|
|
|
262
263
|
expect( catalogJobInstance.run ).toHaveBeenCalled();
|
|
263
264
|
|
|
264
265
|
await settleWorker();
|
|
266
|
+
await vi.waitFor( () => expect( flushPendingHooksMock ).toHaveBeenCalledOnce() );
|
|
265
267
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
266
268
|
} );
|
|
267
269
|
|
|
@@ -359,6 +361,9 @@ describe( 'worker/index', () => {
|
|
|
359
361
|
} );
|
|
360
362
|
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
361
363
|
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
|
|
364
|
+
expect( flushPendingHooksMock ).toHaveBeenCalledOnce();
|
|
365
|
+
expect( mainEventBusMock.emit.mock.invocationCallOrder[0] ).toBeLessThan( flushPendingHooksMock.mock.invocationCallOrder[0] );
|
|
366
|
+
expect( flushPendingHooksMock.mock.invocationCallOrder[0] ).toBeLessThan( exitMock.mock.invocationCallOrder[0] );
|
|
362
367
|
} );
|
|
363
368
|
|
|
364
369
|
it( 'throws connection monitor errors after graceful shutdown', async () => {
|
package/src/worker/proxy.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
|
2
|
-
import { createChildLogger } from '#logger';
|
|
3
|
-
|
|
4
|
-
const log = createChildLogger( 'Proxy' );
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Routes all `fetch()` calls (including those inside Temporal activities)
|
|
8
|
-
* through an HTTP/HTTPS proxy when standard proxy env vars are set.
|
|
9
|
-
* No-op when none are set.
|
|
10
|
-
*/
|
|
11
|
-
export const bootstrapFetchProxy = () => {
|
|
12
|
-
const httpProxyUrl = process.env.http_proxy ?? process.env.HTTP_PROXY;
|
|
13
|
-
const httpsProxyUrl = process.env.https_proxy ?? process.env.HTTPS_PROXY;
|
|
14
|
-
|
|
15
|
-
if ( httpProxyUrl?.length > 0 || httpsProxyUrl?.length > 0 ) {
|
|
16
|
-
log.info( 'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent', { httpProxyUrl, httpsProxyUrl } );
|
|
17
|
-
/* Ignore HTTP/2. Check: https://github.com/growthxai/output/issues/299 */
|
|
18
|
-
setGlobalDispatcher( new EnvHttpProxyAgent( { allowH2: false } ) );
|
|
19
|
-
}
|
|
20
|
-
};
|
package/src/worker/proxy.spec.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const mockSetGlobalDispatcher = vi.fn();
|
|
4
|
-
const MockEnvHttpProxyAgent = vi.fn();
|
|
5
|
-
const mockLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
6
|
-
|
|
7
|
-
vi.mock( 'undici', () => ( {
|
|
8
|
-
EnvHttpProxyAgent: MockEnvHttpProxyAgent,
|
|
9
|
-
setGlobalDispatcher: mockSetGlobalDispatcher
|
|
10
|
-
} ) );
|
|
11
|
-
|
|
12
|
-
vi.mock( '#logger', () => ( {
|
|
13
|
-
createChildLogger: () => mockLogger
|
|
14
|
-
} ) );
|
|
15
|
-
|
|
16
|
-
describe( 'worker/proxy', () => {
|
|
17
|
-
const proxyEnv = [ 'http_proxy', 'HTTP_PROXY', 'https_proxy', 'HTTPS_PROXY' ];
|
|
18
|
-
|
|
19
|
-
beforeEach( () => {
|
|
20
|
-
vi.clearAllMocks();
|
|
21
|
-
for ( const key of proxyEnv ) {
|
|
22
|
-
delete process.env[key];
|
|
23
|
-
}
|
|
24
|
-
} );
|
|
25
|
-
|
|
26
|
-
afterEach( () => {
|
|
27
|
-
vi.resetModules();
|
|
28
|
-
} );
|
|
29
|
-
|
|
30
|
-
it( 'does nothing when no proxy env vars are set', async () => {
|
|
31
|
-
const { bootstrapFetchProxy } = await import( './proxy.js' );
|
|
32
|
-
bootstrapFetchProxy();
|
|
33
|
-
|
|
34
|
-
expect( MockEnvHttpProxyAgent ).not.toHaveBeenCalled();
|
|
35
|
-
expect( mockSetGlobalDispatcher ).not.toHaveBeenCalled();
|
|
36
|
-
expect( mockLogger.info ).not.toHaveBeenCalled();
|
|
37
|
-
} );
|
|
38
|
-
|
|
39
|
-
it( 'sets global dispatcher when proxy URL is available', async () => {
|
|
40
|
-
process.env.HTTP_PROXY = 'http://proxy:8080/';
|
|
41
|
-
const { bootstrapFetchProxy } = await import( './proxy.js' );
|
|
42
|
-
bootstrapFetchProxy();
|
|
43
|
-
|
|
44
|
-
expect( MockEnvHttpProxyAgent ).toHaveBeenCalledWith( { allowH2: false } );
|
|
45
|
-
expect( mockSetGlobalDispatcher ).toHaveBeenCalledTimes( 1 );
|
|
46
|
-
expect( mockLogger.info ).toHaveBeenCalledWith(
|
|
47
|
-
'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent',
|
|
48
|
-
{ httpProxyUrl: 'http://proxy:8080/', httpsProxyUrl: undefined }
|
|
49
|
-
);
|
|
50
|
-
} );
|
|
51
|
-
} );
|