@outputai/core 0.1.13-dev.2f0a972.0 → 0.1.13-dev.59a1b6d.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 +4 -4
- package/src/worker/configs.js +4 -1
- package/src/worker/index.js +5 -2
- package/src/worker/index.spec.js +6 -1
- package/src/worker/proxy.js +16 -0
- package/src/worker/proxy.spec.js +63 -0
- package/src/worker/start_catalog.js +3 -20
- package/src/worker/start_catalog.spec.js +8 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.1.13-dev.
|
|
3
|
+
"version": "0.1.13-dev.59a1b6d.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"output-copy-assets": "./bin/copy_assets.js"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@aws-sdk/client-s3": "3.
|
|
35
|
+
"@aws-sdk/client-s3": "3.1024.0",
|
|
36
36
|
"@babel/generator": "7.29.1",
|
|
37
37
|
"@babel/parser": "7.29.2",
|
|
38
38
|
"@babel/traverse": "7.29.0",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"@temporalio/common": "1.15.0",
|
|
43
43
|
"@temporalio/worker": "1.15.0",
|
|
44
44
|
"@temporalio/workflow": "1.15.0",
|
|
45
|
-
"redis": "5.
|
|
45
|
+
"redis": "5.11.0",
|
|
46
46
|
"stacktrace-parser": "0.1.11",
|
|
47
|
-
"undici": "8.
|
|
47
|
+
"undici": "8.0.2",
|
|
48
48
|
"winston": "3.19.0",
|
|
49
49
|
"zod": "4.3.6"
|
|
50
50
|
},
|
package/src/worker/configs.js
CHANGED
|
@@ -26,7 +26,9 @@ const envVarSchema = z.object( {
|
|
|
26
26
|
// Whether to send activity heartbeats (enabled by default)
|
|
27
27
|
OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: z.transform( v => v === undefined ? true : isStringboolTrue( v ) ),
|
|
28
28
|
// Time to allow for hooks to flush before shutdown
|
|
29
|
-
OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 3000 ) )
|
|
29
|
+
OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 3000 ) ),
|
|
30
|
+
// HTTP CONNECT proxy for Temporal gRPC connections (e.g. "proxy-host:8080")
|
|
31
|
+
TEMPORAL_GRPC_PROXY: z.string().optional()
|
|
30
32
|
} );
|
|
31
33
|
|
|
32
34
|
const { data: envVars, error } = envVarSchema.safeParse( process.env );
|
|
@@ -47,3 +49,4 @@ export const catalogId = envVars.OUTPUT_CATALOG_ID;
|
|
|
47
49
|
export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
|
|
48
50
|
export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
|
|
49
51
|
export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
|
|
52
|
+
export const grpcProxy = envVars.TEMPORAL_GRPC_PROXY;
|
package/src/worker/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { initInterceptors } from './interceptors.js';
|
|
|
9
9
|
import { createChildLogger } from '#logger';
|
|
10
10
|
import { registerShutdown } from './shutdown.js';
|
|
11
11
|
import { startCatalog } from './start_catalog.js';
|
|
12
|
+
import { bootstrapFetchProxy } from './proxy.js';
|
|
12
13
|
import { messageBus } from '#bus';
|
|
13
14
|
import './log_hooks.js';
|
|
14
15
|
import { BusEventType } from '#consts';
|
|
@@ -24,6 +25,7 @@ const callerDir = process.argv[2];
|
|
|
24
25
|
apiKey,
|
|
25
26
|
namespace,
|
|
26
27
|
taskQueue,
|
|
28
|
+
grpcProxy,
|
|
27
29
|
maxConcurrentWorkflowTaskExecutions,
|
|
28
30
|
maxConcurrentActivityTaskExecutions,
|
|
29
31
|
maxCachedWorkflows,
|
|
@@ -41,6 +43,7 @@ const callerDir = process.argv[2];
|
|
|
41
43
|
const activities = await loadActivities( callerDir, workflows );
|
|
42
44
|
|
|
43
45
|
messageBus.emit( BusEventType.WORKER_BEFORE_START );
|
|
46
|
+
bootstrapFetchProxy();
|
|
44
47
|
|
|
45
48
|
log.info( 'Creating worker entry point...' );
|
|
46
49
|
const workflowsPath = createWorkflowsEntryPoint( workflows );
|
|
@@ -52,8 +55,8 @@ const callerDir = process.argv[2];
|
|
|
52
55
|
const catalog = createCatalog( { workflows, activities } );
|
|
53
56
|
|
|
54
57
|
log.info( 'Connecting Temporal...' );
|
|
55
|
-
|
|
56
|
-
const connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey } );
|
|
58
|
+
const proxy = grpcProxy ? { type: 'http-connect', targetHost: grpcProxy } : undefined;
|
|
59
|
+
const connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
|
|
57
60
|
|
|
58
61
|
log.info( 'Creating worker...' );
|
|
59
62
|
const worker = await Worker.create( {
|
package/src/worker/index.spec.js
CHANGED
|
@@ -16,6 +16,7 @@ const configValues = {
|
|
|
16
16
|
namespace: 'default',
|
|
17
17
|
taskQueue: 'test-queue',
|
|
18
18
|
catalogId: 'test-catalog',
|
|
19
|
+
grpcProxy: undefined,
|
|
19
20
|
maxConcurrentWorkflowTaskExecutions: 200,
|
|
20
21
|
maxConcurrentActivityTaskExecutions: 40,
|
|
21
22
|
maxCachedWorkflows: 1000,
|
|
@@ -52,6 +53,9 @@ vi.mock( './interceptors.js', () => ( { initInterceptors: initInterceptorsMock }
|
|
|
52
53
|
const startCatalogMock = vi.fn().mockResolvedValue( undefined );
|
|
53
54
|
vi.mock( './start_catalog.js', () => ( { startCatalog: startCatalogMock } ) );
|
|
54
55
|
|
|
56
|
+
const bootstrapFetchProxyMock = vi.fn();
|
|
57
|
+
vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: bootstrapFetchProxyMock } ) );
|
|
58
|
+
|
|
55
59
|
const registerShutdownMock = vi.fn();
|
|
56
60
|
vi.mock( './shutdown.js', () => ( { registerShutdown: registerShutdownMock } ) );
|
|
57
61
|
|
|
@@ -104,7 +108,8 @@ describe( 'worker/index', () => {
|
|
|
104
108
|
expect( NativeConnection.connect ).toHaveBeenCalledWith( {
|
|
105
109
|
address: configValues.address,
|
|
106
110
|
tls: false,
|
|
107
|
-
apiKey: undefined
|
|
111
|
+
apiKey: undefined,
|
|
112
|
+
proxy: undefined
|
|
108
113
|
} );
|
|
109
114
|
expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
110
115
|
namespace: configValues.namespace,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
|
2
|
+
import { createChildLogger } from '#logger';
|
|
3
|
+
|
|
4
|
+
const log = createChildLogger( 'Proxy' );
|
|
5
|
+
|
|
6
|
+
export const bootstrapFetchProxy = () => {
|
|
7
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
|
|
8
|
+
process.env.HTTP_PROXY || process.env.http_proxy;
|
|
9
|
+
|
|
10
|
+
if ( !proxyUrl ) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
log.info( 'Routing fetch() through HTTP proxy', { proxyUrl } );
|
|
15
|
+
setGlobalDispatcher( new EnvHttpProxyAgent() );
|
|
16
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockSetGlobalDispatcher = vi.fn();
|
|
4
|
+
const MockEnvHttpProxyAgent = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.mock( 'undici', () => ( {
|
|
7
|
+
EnvHttpProxyAgent: MockEnvHttpProxyAgent,
|
|
8
|
+
setGlobalDispatcher: mockSetGlobalDispatcher
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
11
|
+
vi.mock( '#logger', () => ( {
|
|
12
|
+
createChildLogger: () => ( { info: vi.fn(), warn: vi.fn(), error: vi.fn() } )
|
|
13
|
+
} ) );
|
|
14
|
+
|
|
15
|
+
describe( 'worker/proxy', () => {
|
|
16
|
+
const originalEnv = { ...process.env };
|
|
17
|
+
|
|
18
|
+
beforeEach( () => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
delete process.env.HTTPS_PROXY;
|
|
21
|
+
delete process.env.https_proxy;
|
|
22
|
+
delete process.env.HTTP_PROXY;
|
|
23
|
+
delete process.env.http_proxy;
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
afterEach( () => {
|
|
27
|
+
process.env = { ...originalEnv };
|
|
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( mockSetGlobalDispatcher ).not.toHaveBeenCalled();
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'sets global dispatcher when HTTPS_PROXY is set', async () => {
|
|
38
|
+
process.env.HTTPS_PROXY = 'http://proxy:8080';
|
|
39
|
+
const { bootstrapFetchProxy } = await import( './proxy.js' );
|
|
40
|
+
bootstrapFetchProxy();
|
|
41
|
+
|
|
42
|
+
expect( MockEnvHttpProxyAgent ).toHaveBeenCalled();
|
|
43
|
+
expect( mockSetGlobalDispatcher ).toHaveBeenCalledTimes( 1 );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
it( 'sets global dispatcher when HTTP_PROXY is set', async () => {
|
|
47
|
+
process.env.HTTP_PROXY = 'http://proxy:8080';
|
|
48
|
+
const { bootstrapFetchProxy } = await import( './proxy.js' );
|
|
49
|
+
bootstrapFetchProxy();
|
|
50
|
+
|
|
51
|
+
expect( MockEnvHttpProxyAgent ).toHaveBeenCalled();
|
|
52
|
+
expect( mockSetGlobalDispatcher ).toHaveBeenCalledTimes( 1 );
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
it( 'prefers HTTPS_PROXY over HTTP_PROXY for detection', async () => {
|
|
56
|
+
process.env.HTTPS_PROXY = 'http://secure-proxy:8080';
|
|
57
|
+
process.env.HTTP_PROXY = 'http://plain-proxy:8080';
|
|
58
|
+
const { bootstrapFetchProxy } = await import( './proxy.js' );
|
|
59
|
+
bootstrapFetchProxy();
|
|
60
|
+
|
|
61
|
+
expect( mockSetGlobalDispatcher ).toHaveBeenCalledTimes( 1 );
|
|
62
|
+
} );
|
|
63
|
+
} );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Client
|
|
1
|
+
import { Client } from '@temporalio/client';
|
|
2
2
|
import { WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
3
3
|
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
4
|
import { catalogId, taskQueue } from './configs.js';
|
|
@@ -8,29 +8,12 @@ const log = createChildLogger( 'Catalog' );
|
|
|
8
8
|
|
|
9
9
|
export const startCatalog = async ( { connection, namespace, catalog } ) => {
|
|
10
10
|
const client = new Client( { connection, namespace } );
|
|
11
|
-
const catalogWorkflowHandle = client.workflow.getHandle( catalogId );
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const catalogWorkflowDescription = await catalogWorkflowHandle.describe();
|
|
15
|
-
if ( !catalogWorkflowDescription.closeTime ) {
|
|
16
|
-
log.info( 'Completing previous catalog workflow...' );
|
|
17
|
-
await catalogWorkflowHandle.executeUpdate( 'complete', { args: [] } );
|
|
18
|
-
}
|
|
19
|
-
} catch ( error ) {
|
|
20
|
-
// When "not found", it's either a cold start or the catalog was already stopped/terminated, ignore it.
|
|
21
|
-
// Otherwise, create a log and try the next operation:
|
|
22
|
-
// A. If the workflow is still running, the start() will fail and throw;
|
|
23
|
-
// B. If the workflow is no running, the start() will succeed, and the error was transient;
|
|
24
|
-
if ( !( error instanceof WorkflowNotFoundError ) ) {
|
|
25
|
-
log.warn( 'Error interacting with previous catalog workflow', { error } );
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
11
|
|
|
29
12
|
log.info( 'Starting catalog workflow...' );
|
|
30
13
|
await client.workflow.start( WORKFLOW_CATALOG, {
|
|
31
14
|
taskQueue,
|
|
32
|
-
workflowId: catalogId,
|
|
33
|
-
workflowIdConflictPolicy: WorkflowIdConflictPolicy.
|
|
15
|
+
workflowId: catalogId,
|
|
16
|
+
workflowIdConflictPolicy: WorkflowIdConflictPolicy.TERMINATE_EXISTING,
|
|
34
17
|
args: [ catalog ]
|
|
35
18
|
} );
|
|
36
19
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { WorkflowNotFoundError } from '@temporalio/client';
|
|
3
2
|
|
|
4
3
|
const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
5
4
|
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
@@ -10,25 +9,18 @@ const catalogId = 'test-catalog';
|
|
|
10
9
|
const taskQueue = 'test-queue';
|
|
11
10
|
vi.mock( './configs.js', () => ( { catalogId, taskQueue } ) );
|
|
12
11
|
|
|
13
|
-
const describeMock = vi.fn();
|
|
14
|
-
const executeUpdateMock = vi.fn();
|
|
15
12
|
const workflowStartMock = vi.fn().mockResolvedValue( undefined );
|
|
16
13
|
vi.mock( '@temporalio/client', async importOriginal => {
|
|
17
14
|
const actual = await importOriginal();
|
|
18
15
|
return {
|
|
19
16
|
...actual,
|
|
20
17
|
Client: vi.fn().mockImplementation( function () {
|
|
21
|
-
return {
|
|
22
|
-
workflow: {
|
|
23
|
-
start: workflowStartMock,
|
|
24
|
-
getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
|
|
25
|
-
}
|
|
26
|
-
};
|
|
18
|
+
return { workflow: { start: workflowStartMock } };
|
|
27
19
|
} )
|
|
28
20
|
};
|
|
29
21
|
} );
|
|
30
22
|
|
|
31
|
-
vi.mock( '@temporalio/common', () => ( { WorkflowIdConflictPolicy: {
|
|
23
|
+
vi.mock( '@temporalio/common', () => ( { WorkflowIdConflictPolicy: { TERMINATE_EXISTING: 'TERMINATE_EXISTING' } } ) );
|
|
32
24
|
|
|
33
25
|
describe( 'worker/start_catalog', () => {
|
|
34
26
|
const mockConnection = {};
|
|
@@ -40,79 +32,24 @@ describe( 'worker/start_catalog', () => {
|
|
|
40
32
|
workflowStartMock.mockResolvedValue( undefined );
|
|
41
33
|
} );
|
|
42
34
|
|
|
43
|
-
it( '
|
|
44
|
-
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
45
|
-
executeUpdateMock.mockResolvedValue( undefined );
|
|
46
|
-
|
|
47
|
-
const { startCatalog } = await import( './start_catalog.js' );
|
|
48
|
-
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
49
|
-
|
|
50
|
-
expect( describeMock ).toHaveBeenCalled();
|
|
51
|
-
expect( mockLog.info ).toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
52
|
-
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
53
|
-
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
54
|
-
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
55
|
-
taskQueue,
|
|
56
|
-
workflowId: catalogId,
|
|
57
|
-
workflowIdConflictPolicy: 'FAIL',
|
|
58
|
-
args: [ catalog ]
|
|
59
|
-
} );
|
|
60
|
-
} );
|
|
61
|
-
|
|
62
|
-
it( 'when no previous catalog: ignores and starts catalog workflow', async () => {
|
|
63
|
-
describeMock.mockRejectedValue( new WorkflowNotFoundError( 'not found' ) );
|
|
64
|
-
|
|
65
|
-
const { startCatalog } = await import( './start_catalog.js' );
|
|
66
|
-
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
67
|
-
|
|
68
|
-
expect( describeMock ).toHaveBeenCalled();
|
|
69
|
-
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
70
|
-
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
71
|
-
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
72
|
-
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
73
|
-
taskQueue,
|
|
74
|
-
workflowId: catalogId,
|
|
75
|
-
workflowIdConflictPolicy: 'FAIL',
|
|
76
|
-
args: [ catalog ]
|
|
77
|
-
} );
|
|
78
|
-
} );
|
|
79
|
-
|
|
80
|
-
it( 'when previous catalog already closed: skips complete and starts catalog workflow', async () => {
|
|
81
|
-
describeMock.mockResolvedValue( { closeTime: '2024-01-01T00:00:00Z' } );
|
|
82
|
-
|
|
35
|
+
it( 'starts catalog workflow with TERMINATE_EXISTING policy', async () => {
|
|
83
36
|
const { startCatalog } = await import( './start_catalog.js' );
|
|
84
37
|
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
85
38
|
|
|
86
|
-
expect( describeMock ).toHaveBeenCalled();
|
|
87
|
-
expect( mockLog.info ).not.toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
88
|
-
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
89
39
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
90
40
|
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
91
41
|
taskQueue,
|
|
92
42
|
workflowId: catalogId,
|
|
93
|
-
workflowIdConflictPolicy: '
|
|
43
|
+
workflowIdConflictPolicy: 'TERMINATE_EXISTING',
|
|
94
44
|
args: [ catalog ]
|
|
95
45
|
} );
|
|
96
46
|
} );
|
|
97
47
|
|
|
98
|
-
it( '
|
|
99
|
-
|
|
100
|
-
executeUpdateMock.mockRejectedValue( new Error( 'Connection refused' ) );
|
|
48
|
+
it( 'propagates errors from workflow.start', async () => {
|
|
49
|
+
workflowStartMock.mockRejectedValue( new Error( 'Connection refused' ) );
|
|
101
50
|
|
|
102
51
|
const { startCatalog } = await import( './start_catalog.js' );
|
|
103
|
-
await startCatalog( { connection: mockConnection, namespace, catalog } )
|
|
104
|
-
|
|
105
|
-
expect( describeMock ).toHaveBeenCalled();
|
|
106
|
-
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
107
|
-
expect( mockLog.warn ).toHaveBeenCalledWith( 'Error interacting with previous catalog workflow', {
|
|
108
|
-
error: expect.any( Error )
|
|
109
|
-
} );
|
|
110
|
-
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
111
|
-
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
112
|
-
taskQueue,
|
|
113
|
-
workflowId: catalogId,
|
|
114
|
-
workflowIdConflictPolicy: 'FAIL',
|
|
115
|
-
args: [ catalog ]
|
|
116
|
-
} );
|
|
52
|
+
await expect( startCatalog( { connection: mockConnection, namespace, catalog } ) )
|
|
53
|
+
.rejects.toThrow( 'Connection refused' );
|
|
117
54
|
} );
|
|
118
55
|
} );
|