@outputai/core 0.10.1-next.af37678.0 → 0.10.1-next.be4ec7f.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/worker/configs.js +0 -3
- package/src/worker/index.js +11 -5
- package/src/worker/index.spec.js +7 -1
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.be4ec7f.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/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
|
@@ -13,6 +13,7 @@ import { createChildLogger } from '#logger';
|
|
|
13
13
|
import { setupInterruptionHandler } from './interruption.js';
|
|
14
14
|
import { CatalogJob } from './catalog_workflow/catalog_job.js';
|
|
15
15
|
import { mainEventBus } from '#bus';
|
|
16
|
+
import { flushPendingHooks } from '#hooks/pending_hooks';
|
|
16
17
|
import { BusEventType } from '#consts';
|
|
17
18
|
import { setupTelemetry } from './telemetry.js';
|
|
18
19
|
import { TemporalConnectionMonitor } from './connection_monitor.js';
|
|
@@ -212,13 +213,18 @@ execute()
|
|
|
212
213
|
.catch( e => log.warn( 'Connection close error', { error: e.message } ) );
|
|
213
214
|
}
|
|
214
215
|
} )
|
|
215
|
-
.then( () =>
|
|
216
|
-
|
|
216
|
+
.then( async () => {
|
|
217
|
+
log.info( 'Flushing hook callbacks...' );
|
|
218
|
+
await flushPendingHooks();
|
|
219
|
+
log.info( 'Bye' );
|
|
220
|
+
} )
|
|
221
|
+
.catch( async error => {
|
|
217
222
|
log.error( 'Fatal error', { error: error.message, stack: error.stack } );
|
|
218
223
|
|
|
219
224
|
mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error } );
|
|
220
225
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
log.info( 'Flushing hook callbacks...' );
|
|
227
|
+
await flushPendingHooks();
|
|
228
|
+
log.info( 'Exiting...' );
|
|
229
|
+
setTimeout( () => process.exit( 1 ) );
|
|
224
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: {} } );
|
|
@@ -261,6 +263,7 @@ describe( 'worker/index', () => {
|
|
|
261
263
|
expect( catalogJobInstance.run ).toHaveBeenCalled();
|
|
262
264
|
|
|
263
265
|
await settleWorker();
|
|
266
|
+
await vi.waitFor( () => expect( flushPendingHooksMock ).toHaveBeenCalledOnce() );
|
|
264
267
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
265
268
|
} );
|
|
266
269
|
|
|
@@ -358,6 +361,9 @@ describe( 'worker/index', () => {
|
|
|
358
361
|
} );
|
|
359
362
|
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
360
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] );
|
|
361
367
|
} );
|
|
362
368
|
|
|
363
369
|
it( 'throws connection monitor errors after graceful shutdown', async () => {
|