@outputai/core 0.10.1-next.fc0a41f.0 → 0.11.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 +12 -7
- package/src/consts.js +5 -2
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +55 -13
- package/src/helpers/errors.spec.js +166 -66
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +35 -44
- package/src/hooks/index.js +2 -1
- package/src/hooks/index.spec.js +15 -7
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +24 -52
- package/src/interface/workflow.spec.js +109 -108
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/temporal/client.js +47 -0
- package/src/temporal/client.spec.js +124 -0
- package/src/temporal/index.d.ts +32 -0
- package/src/temporal/index.js +1 -0
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/index.js +10 -1
- package/src/worker/index.spec.js +23 -8
- package/src/worker/interceptors/activity.js +32 -19
- package/src/worker/interceptors/activity.spec.js +71 -166
- package/src/worker/interceptors/workflow.js +53 -11
- package/src/worker/interceptors/workflow.spec.js +176 -49
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +37 -5
- package/src/worker/log_hooks.js +13 -10
- package/src/worker/log_hooks.spec.js +12 -4
- package/src/worker/sinks.js +3 -1
- package/src/worker/sinks.spec.js +21 -4
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Client, WorkflowHandle } from '@temporalio/client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Worker-side access to the Temporal layer.
|
|
5
|
+
*
|
|
6
|
+
* `createTemporalClient()` is available after the worker establishes its Temporal
|
|
7
|
+
* connection. `getCurrentWorkflowHandle()` is only available from Temporal Activities.
|
|
8
|
+
*
|
|
9
|
+
* > [!WARNING]
|
|
10
|
+
* > These are not Temporal sandbox safe, do not import in workflows directly.
|
|
11
|
+
* > These require the worker runtime, so in unit tests, mock the functions.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a Temporal [Client](https://typescript.temporal.io/api/classes/client.Client)
|
|
18
|
+
* instance with the same connection and namespace as the worker.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* - Only available after the worker establishes its Temporal connection.
|
|
22
|
+
*/
|
|
23
|
+
export declare const createTemporalClient: () => Client;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return the [WorkflowHandle](https://typescript.temporal.io/api/interfaces/client.WorkflowHandle)
|
|
27
|
+
* for the current workflow execution.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* - Only available from a Temporal Activity running in the worker.
|
|
31
|
+
*/
|
|
32
|
+
export declare const getCurrentWorkflowHandle: () => WorkflowHandle;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createTemporalClient, getCurrentWorkflowHandle } from './client.js';
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Storage } from '#async_storage';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { serializeError } from '
|
|
3
|
+
import { serializeError } from '#helpers/error_serializer';
|
|
4
4
|
import { isStringboolTrue } from '#helpers/string';
|
|
5
5
|
import * as localProcessor from './processors/local/index.js';
|
|
6
6
|
import * as s3Processor from './processors/s3/index.js';
|
|
7
7
|
import { createChildLogger } from '#logger';
|
|
8
8
|
import { EventAction } from './trace_consts.js';
|
|
9
9
|
import { BaseAttribute } from './trace_attribute.js';
|
|
10
|
+
import { TransparentFatalError } from '#errors';
|
|
10
11
|
|
|
11
12
|
const log = createChildLogger( 'Tracing' );
|
|
12
13
|
|
|
@@ -51,7 +52,7 @@ export const init = async () => {
|
|
|
51
52
|
try {
|
|
52
53
|
await p.exec( ...args );
|
|
53
54
|
} catch ( error ) {
|
|
54
|
-
log.error( 'Processor execution error', { processor: p.name, error: error
|
|
55
|
+
log.error( 'Processor execution error', { processor: p.name, error: serializeError( error ) } );
|
|
55
56
|
}
|
|
56
57
|
} );
|
|
57
58
|
}
|
|
@@ -87,15 +88,12 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, tra
|
|
|
87
88
|
export function addEventActionWithContext( action, options ) {
|
|
88
89
|
const storeContent = Storage.load();
|
|
89
90
|
if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
|
|
90
|
-
const { parentId, traceInfo
|
|
91
|
-
if ( action === EventAction.ADD_ATTR ) {
|
|
92
|
-
|
|
93
|
-
if ( !( attribute instanceof BaseAttribute ) ) {
|
|
94
|
-
throw new Error( `Event ${EventAction.ADD_ATTR} argument is not a BaseAttribute instance` );
|
|
95
|
-
} else {
|
|
96
|
-
addAttribute( options.details );
|
|
97
|
-
}
|
|
91
|
+
const { parentId, traceInfo } = storeContent;
|
|
92
|
+
if ( action === EventAction.ADD_ATTR && !( options.details instanceof BaseAttribute ) ) {
|
|
93
|
+
throw new Error( `Event ${EventAction.ADD_ATTR} argument is not a BaseAttribute instance` );
|
|
98
94
|
}
|
|
99
|
-
|
|
95
|
+
/** Unwraps Transparent error set in details so it behaves like in the interceptors */
|
|
96
|
+
const details = options.details instanceof TransparentFatalError ? options.details.cause : options.details;
|
|
97
|
+
addEventAction( action, { ...options, details, parentId, traceInfo } );
|
|
100
98
|
}
|
|
101
99
|
};
|
|
@@ -5,10 +5,15 @@ vi.mock( '#async_storage', () => ( {
|
|
|
5
5
|
Storage: { load: storageLoadMock }
|
|
6
6
|
} ) );
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const serializeErrorMock = vi.fn( error => ( {
|
|
9
|
+
name: error.name,
|
|
10
|
+
message: error.message
|
|
11
|
+
} ) );
|
|
12
|
+
vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
|
|
13
|
+
|
|
9
14
|
const logErrorMock = vi.fn();
|
|
10
15
|
vi.mock( '#logger', () => ( {
|
|
11
|
-
createChildLogger: () => ( {
|
|
16
|
+
createChildLogger: () => ( { error: logErrorMock } )
|
|
12
17
|
} ) );
|
|
13
18
|
|
|
14
19
|
const localInitMock = vi.fn( async () => {} );
|
|
@@ -71,6 +76,24 @@ describe( 'tracing/trace_engine', () => {
|
|
|
71
76
|
expect( payload.traceInfo ).toBe( traceInfo );
|
|
72
77
|
} );
|
|
73
78
|
|
|
79
|
+
it( 'init() logs processor failures with serialized stacks', async () => {
|
|
80
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
81
|
+
const processorError = new Error( 'processor failed' );
|
|
82
|
+
localExecMock.mockRejectedValueOnce( processorError );
|
|
83
|
+
const { init, addEventAction } = await loadTraceEngine();
|
|
84
|
+
await init();
|
|
85
|
+
|
|
86
|
+
addEventAction( 'start', {
|
|
87
|
+
kind: 'step', name: 'N', id: '1', parentId: 'p', details: null, traceInfo
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
await vi.waitFor( () => expect( logErrorMock ).toHaveBeenCalledWith( 'Processor execution error', {
|
|
91
|
+
processor: 'LOCAL',
|
|
92
|
+
error: { name: 'Error', message: 'processor failed' }
|
|
93
|
+
} ) );
|
|
94
|
+
expect( serializeErrorMock ).toHaveBeenCalledWith( processorError );
|
|
95
|
+
} );
|
|
96
|
+
|
|
74
97
|
it( 'addEventAction() emits an entry consumed by processors', async () => {
|
|
75
98
|
process.env.OUTPUT_TRACE_LOCAL_ON = 'on';
|
|
76
99
|
const { init, addEventAction } = await loadTraceEngine();
|
|
@@ -87,34 +110,34 @@ describe( 'tracing/trace_engine', () => {
|
|
|
87
110
|
expect( payload.entry.details ).toBe( 'done' );
|
|
88
111
|
} );
|
|
89
112
|
|
|
90
|
-
it( 'addEventAction()
|
|
91
|
-
process.env.OUTPUT_TRACE_LOCAL_ON = '
|
|
113
|
+
it( 'addEventAction() serializes error details before emitting', async () => {
|
|
114
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = 'on';
|
|
92
115
|
const { init, addEventAction } = await loadTraceEngine();
|
|
93
116
|
await init();
|
|
117
|
+
const error = new TypeError( 'step failed' );
|
|
94
118
|
|
|
95
|
-
addEventAction( '
|
|
96
|
-
kind: 'step', name: '
|
|
97
|
-
traceInfo
|
|
119
|
+
addEventAction( 'error', {
|
|
120
|
+
kind: 'step', name: 'S', id: '3', parentId: 'p3', details: error,
|
|
121
|
+
traceInfo
|
|
122
|
+
} );
|
|
123
|
+
|
|
124
|
+
expect( serializeErrorMock ).toHaveBeenCalledWith( error );
|
|
125
|
+
expect( localExecMock.mock.calls[0][0].entry.details ).toEqual( {
|
|
126
|
+
name: 'TypeError',
|
|
127
|
+
message: 'step failed'
|
|
98
128
|
} );
|
|
99
|
-
expect( localExecMock ).not.toHaveBeenCalled();
|
|
100
129
|
} );
|
|
101
130
|
|
|
102
|
-
it( 'addEventAction()
|
|
131
|
+
it( 'addEventAction() does not emit when traceInfo is absent', async () => {
|
|
103
132
|
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
104
133
|
const { init, addEventAction } = await loadTraceEngine();
|
|
105
134
|
await init();
|
|
106
135
|
|
|
107
136
|
addEventAction( 'start', {
|
|
108
|
-
kind: '
|
|
109
|
-
traceInfo
|
|
110
|
-
} );
|
|
111
|
-
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
112
|
-
expect( localExecMock.mock.calls[0][0].entry ).toMatchObject( {
|
|
113
|
-
kind: 'internal_step',
|
|
114
|
-
name: 'Internal',
|
|
115
|
-
id: '1',
|
|
116
|
-
parentId: 'p'
|
|
137
|
+
kind: 'step', name: 'X', id: '1', parentId: 'p', details: {},
|
|
138
|
+
traceInfo: undefined
|
|
117
139
|
} );
|
|
140
|
+
expect( localExecMock ).not.toHaveBeenCalled();
|
|
118
141
|
} );
|
|
119
142
|
|
|
120
143
|
it( 'addEventActionWithContext() uses storage when available', async () => {
|
|
@@ -135,13 +158,34 @@ describe( 'tracing/trace_engine', () => {
|
|
|
135
158
|
expect( payload.entry.action ).toBe( 'tick' );
|
|
136
159
|
} );
|
|
137
160
|
|
|
138
|
-
it( 'addEventActionWithContext()
|
|
161
|
+
it( 'addEventActionWithContext() unwraps transparent errors without mutating options', async () => {
|
|
139
162
|
process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
|
|
140
|
-
const addAttributeMock = vi.fn();
|
|
141
163
|
storageLoadMock.mockReturnValue( {
|
|
142
164
|
parentId: 'ctx-p',
|
|
143
|
-
traceInfo
|
|
144
|
-
|
|
165
|
+
traceInfo
|
|
166
|
+
} );
|
|
167
|
+
const { init, addEventActionWithContext } = await loadTraceEngine();
|
|
168
|
+
const { TransparentFatalError } = await import( '#errors' );
|
|
169
|
+
await init();
|
|
170
|
+
|
|
171
|
+
const cause = new TypeError( 'provider rejected request' );
|
|
172
|
+
const error = new TransparentFatalError( cause );
|
|
173
|
+
const options = { kind: 'llm', name: 'generate', id: 'llm-1', details: error };
|
|
174
|
+
addEventActionWithContext( 'error', options );
|
|
175
|
+
|
|
176
|
+
expect( options.details ).toBe( error );
|
|
177
|
+
expect( serializeErrorMock ).toHaveBeenCalledWith( cause );
|
|
178
|
+
expect( localExecMock.mock.calls[0][0].entry.details ).toEqual( {
|
|
179
|
+
name: 'TypeError',
|
|
180
|
+
message: 'provider rejected request'
|
|
181
|
+
} );
|
|
182
|
+
} );
|
|
183
|
+
|
|
184
|
+
it( 'addEventActionWithContext() emits validated ADD_ATTR trace entries', async () => {
|
|
185
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
|
|
186
|
+
storageLoadMock.mockReturnValue( {
|
|
187
|
+
parentId: 'ctx-p',
|
|
188
|
+
traceInfo
|
|
145
189
|
} );
|
|
146
190
|
const { init, addEventActionWithContext } = await loadTraceEngine();
|
|
147
191
|
const { EventAction } = await import( './trace_consts.js' );
|
|
@@ -151,8 +195,6 @@ describe( 'tracing/trace_engine', () => {
|
|
|
151
195
|
const attribute = new Attribute.HTTPRequestCount( 'https://example.test', 'req-1' );
|
|
152
196
|
addEventActionWithContext( EventAction.ADD_ATTR, { kind: 'http', name: 'request', id: 'req-1', details: attribute } );
|
|
153
197
|
|
|
154
|
-
expect( addAttributeMock ).toHaveBeenCalledTimes( 1 );
|
|
155
|
-
expect( addAttributeMock ).toHaveBeenCalledWith( attribute );
|
|
156
198
|
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
157
199
|
expect( localExecMock.mock.calls[0][0] ).toEqual( {
|
|
158
200
|
traceInfo,
|
|
@@ -170,11 +212,9 @@ describe( 'tracing/trace_engine', () => {
|
|
|
170
212
|
|
|
171
213
|
it( 'addEventActionWithContext() throws on invalid ADD_ATTR signal payloads', async () => {
|
|
172
214
|
process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
|
|
173
|
-
const addAttributeMock = vi.fn();
|
|
174
215
|
storageLoadMock.mockReturnValue( {
|
|
175
216
|
parentId: 'ctx-p',
|
|
176
|
-
traceInfo
|
|
177
|
-
addAttribute: addAttributeMock
|
|
217
|
+
traceInfo
|
|
178
218
|
} );
|
|
179
219
|
const { init, addEventActionWithContext } = await loadTraceEngine();
|
|
180
220
|
const { EventAction } = await import( './trace_consts.js' );
|
|
@@ -186,7 +226,6 @@ describe( 'tracing/trace_engine', () => {
|
|
|
186
226
|
{ kind: 'http', name: 'request', id: 'req-1', details: invalidAttribute }
|
|
187
227
|
) ).toThrow( /not a BaseAttribute instance/ );
|
|
188
228
|
|
|
189
|
-
expect( addAttributeMock ).not.toHaveBeenCalled();
|
|
190
229
|
expect( localExecMock ).not.toHaveBeenCalled();
|
|
191
230
|
} );
|
|
192
231
|
|
|
@@ -38,7 +38,7 @@ export class CatalogJob {
|
|
|
38
38
|
throw e;
|
|
39
39
|
}
|
|
40
40
|
if ( !( e instanceof WorkflowNotFoundError ) ) {
|
|
41
|
-
log.warn( 'Error describing catalog workflow', { error: e } );
|
|
41
|
+
log.warn( 'Error describing catalog workflow', { error: e.message } );
|
|
42
42
|
}
|
|
43
43
|
return false;
|
|
44
44
|
} );
|
|
@@ -52,7 +52,7 @@ export class CatalogJob {
|
|
|
52
52
|
throw e;
|
|
53
53
|
}
|
|
54
54
|
if ( !( e instanceof WorkflowNotFoundError ) ) {
|
|
55
|
-
log.warn( 'Error describing catalog workflow', { error: e } );
|
|
55
|
+
log.warn( 'Error describing catalog workflow', { error: e.message } );
|
|
56
56
|
}
|
|
57
57
|
return false;
|
|
58
58
|
} );
|
|
@@ -66,7 +66,7 @@ export class CatalogJob {
|
|
|
66
66
|
throw e;
|
|
67
67
|
}
|
|
68
68
|
if ( !( e instanceof WorkflowNotFoundError ) ) {
|
|
69
|
-
log.warn( 'Error completing previous catalog workflow', { error: e } );
|
|
69
|
+
log.warn( 'Error completing previous catalog workflow', { error: e.message } );
|
|
70
70
|
}
|
|
71
71
|
} );
|
|
72
72
|
};
|
|
@@ -121,7 +121,7 @@ describe( 'CatalogJob', () => {
|
|
|
121
121
|
|
|
122
122
|
await job.run();
|
|
123
123
|
|
|
124
|
-
expect( mockLog.warn ).toHaveBeenCalledWith( 'Error completing previous catalog workflow', { error: completeError } );
|
|
124
|
+
expect( mockLog.warn ).toHaveBeenCalledWith( 'Error completing previous catalog workflow', { error: completeError.message } );
|
|
125
125
|
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', expect.any( Object ) );
|
|
126
126
|
expect( job.error ).toBeNull();
|
|
127
127
|
} );
|
package/src/worker/index.js
CHANGED
|
@@ -18,7 +18,10 @@ import { BusEventType } from '#consts';
|
|
|
18
18
|
import { setupTelemetry } from './telemetry.js';
|
|
19
19
|
import { TemporalConnectionMonitor } from './connection_monitor.js';
|
|
20
20
|
import { bindGlobalFunctions } from './global_functions.js';
|
|
21
|
+
import { setupClientConfig } from '#temporal/client';
|
|
21
22
|
import { runOnce } from '#helpers/function';
|
|
23
|
+
import { serializeError } from '#helpers/error_serializer';
|
|
24
|
+
import { setupTemporalLogger } from './temporal_logger.js';
|
|
22
25
|
|
|
23
26
|
import './log_hooks.js';
|
|
24
27
|
|
|
@@ -51,6 +54,9 @@ const state = {
|
|
|
51
54
|
const callerDir = process.argv[2];
|
|
52
55
|
|
|
53
56
|
const execute = async () => {
|
|
57
|
+
log.info( 'Setup Temporal Logger' );
|
|
58
|
+
setupTemporalLogger();
|
|
59
|
+
|
|
54
60
|
log.info( 'Loading config...', { callerDir } );
|
|
55
61
|
await loadHooks( callerDir );
|
|
56
62
|
|
|
@@ -81,6 +87,9 @@ const execute = async () => {
|
|
|
81
87
|
|
|
82
88
|
state.connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
|
|
83
89
|
|
|
90
|
+
// add configs for the /temporal endpoint
|
|
91
|
+
setupClientConfig( { connection: state.connection, namespace } );
|
|
92
|
+
|
|
84
93
|
log.info( 'Creating connection monitor...' );
|
|
85
94
|
state.connectionMonitor = new TemporalConnectionMonitor( state.connection );
|
|
86
95
|
|
|
@@ -219,7 +228,7 @@ execute()
|
|
|
219
228
|
log.info( 'Bye' );
|
|
220
229
|
} )
|
|
221
230
|
.catch( async error => {
|
|
222
|
-
log.error( 'Fatal error', { error: error
|
|
231
|
+
log.error( 'Fatal error', { error: serializeError( error ) } );
|
|
223
232
|
|
|
224
233
|
mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error } );
|
|
225
234
|
|
package/src/worker/index.spec.js
CHANGED
|
@@ -14,7 +14,10 @@ const {
|
|
|
14
14
|
mockWorker,
|
|
15
15
|
promises,
|
|
16
16
|
resetPromises,
|
|
17
|
+
serializeErrorMock,
|
|
18
|
+
setupClientConfigMock,
|
|
17
19
|
setupInterruptionHandlerMock,
|
|
20
|
+
setupTemporalLoggerMock,
|
|
18
21
|
setupTelemetryMock
|
|
19
22
|
} = vi.hoisted( () => {
|
|
20
23
|
const createDeferred = () => {
|
|
@@ -117,16 +120,20 @@ const {
|
|
|
117
120
|
mockWorker,
|
|
118
121
|
promises,
|
|
119
122
|
resetPromises,
|
|
123
|
+
serializeErrorMock: vi.fn( error => ( {
|
|
124
|
+
name: error.name,
|
|
125
|
+
message: error.message,
|
|
126
|
+
stack: error.stack
|
|
127
|
+
} ) ),
|
|
128
|
+
setupClientConfigMock: vi.fn(),
|
|
120
129
|
setupInterruptionHandlerMock: vi.fn(),
|
|
130
|
+
setupTemporalLoggerMock: vi.fn(),
|
|
121
131
|
setupTelemetryMock: vi.fn()
|
|
122
132
|
};
|
|
123
133
|
} );
|
|
124
134
|
|
|
125
135
|
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
126
|
-
vi.mock( '#
|
|
127
|
-
const actual = await importOriginal();
|
|
128
|
-
return { ...actual };
|
|
129
|
-
} );
|
|
136
|
+
vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
|
|
130
137
|
const initTracing = vi.fn().mockResolvedValue( undefined );
|
|
131
138
|
vi.mock( '#tracing', () => ( { init: initTracing } ) );
|
|
132
139
|
vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
|
|
@@ -161,6 +168,8 @@ vi.mock( './catalog_workflow/catalog_job.js', () => ( {
|
|
|
161
168
|
} )
|
|
162
169
|
} ) );
|
|
163
170
|
vi.mock( './log_hooks.js', () => ( {} ) );
|
|
171
|
+
vi.mock( './temporal_logger.js', () => ( { setupTemporalLogger: setupTemporalLoggerMock } ) );
|
|
172
|
+
vi.mock( '#temporal/client', () => ( { setupClientConfig: setupClientConfigMock } ) );
|
|
164
173
|
vi.mock( '@temporalio/worker', () => ( {
|
|
165
174
|
NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) },
|
|
166
175
|
Worker: { create: vi.fn().mockResolvedValue( mockWorker ) }
|
|
@@ -217,6 +226,7 @@ describe( 'worker/index', () => {
|
|
|
217
226
|
await vi.waitFor( () => expect( Worker.create ).toHaveBeenCalled() );
|
|
218
227
|
|
|
219
228
|
expect( loadHooksMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
229
|
+
expect( setupTemporalLoggerMock ).toHaveBeenCalledOnce();
|
|
220
230
|
expect( loadWorkflowsMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
221
231
|
expect( loadActivitiesMock ).toHaveBeenCalledWith( '/test/caller/dir', [] );
|
|
222
232
|
expect( initTracing ).toHaveBeenCalled();
|
|
@@ -228,6 +238,10 @@ describe( 'worker/index', () => {
|
|
|
228
238
|
apiKey: undefined,
|
|
229
239
|
proxy: undefined
|
|
230
240
|
} );
|
|
241
|
+
expect( setupClientConfigMock ).toHaveBeenCalledWith( {
|
|
242
|
+
connection: mockConnection,
|
|
243
|
+
namespace: configValues.namespace
|
|
244
|
+
} );
|
|
231
245
|
expect( bindGlobalFunctionsMock ).toHaveBeenCalledTimes( 1 );
|
|
232
246
|
expect( bindGlobalFunctionsMock.mock.invocationCallOrder[0] ).toBeLessThan(
|
|
233
247
|
NativeConnection.connect.mock.invocationCallOrder[0]
|
|
@@ -356,9 +370,10 @@ describe( 'worker/index', () => {
|
|
|
356
370
|
|
|
357
371
|
await vi.waitFor( () => {
|
|
358
372
|
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
359
|
-
error: 'Big Failure'
|
|
373
|
+
error: expect.objectContaining( { message: 'Big Failure' } )
|
|
360
374
|
} ) );
|
|
361
375
|
} );
|
|
376
|
+
expect( serializeErrorMock ).toHaveBeenCalledWith( error );
|
|
362
377
|
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
363
378
|
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
|
|
364
379
|
expect( flushPendingHooksMock ).toHaveBeenCalledOnce();
|
|
@@ -378,7 +393,7 @@ describe( 'worker/index', () => {
|
|
|
378
393
|
|
|
379
394
|
await vi.waitFor( () => {
|
|
380
395
|
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
381
|
-
error: 'connection lost'
|
|
396
|
+
error: expect.objectContaining( { message: 'connection lost' } )
|
|
382
397
|
} ) );
|
|
383
398
|
} );
|
|
384
399
|
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
@@ -398,7 +413,7 @@ describe( 'worker/index', () => {
|
|
|
398
413
|
|
|
399
414
|
await vi.waitFor( () => {
|
|
400
415
|
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
401
|
-
error: 'catalog failed'
|
|
416
|
+
error: expect.objectContaining( { message: 'catalog failed' } )
|
|
402
417
|
} ) );
|
|
403
418
|
} );
|
|
404
419
|
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
@@ -418,7 +433,7 @@ describe( 'worker/index', () => {
|
|
|
418
433
|
expect( catalogJobInstance.interrupt ).not.toHaveBeenCalled();
|
|
419
434
|
await vi.waitFor( () => {
|
|
420
435
|
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
421
|
-
error: 'worker create failed'
|
|
436
|
+
error: expect.objectContaining( { message: 'worker create failed' } )
|
|
422
437
|
} ) );
|
|
423
438
|
} );
|
|
424
439
|
} );
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { Context, activityInfo as activityInfoFn } from '@temporalio/activity';
|
|
1
|
+
import { Context, activityInfo as activityInfoFn, ApplicationFailure } from '@temporalio/activity';
|
|
2
|
+
import { CompleteAsyncError, TemporalFailure } from '@temporalio/common';
|
|
2
3
|
import { Storage } from '#async_storage';
|
|
3
4
|
import * as Tracing from '#tracing';
|
|
4
5
|
import { headersToObject } from './headers.js';
|
|
5
|
-
import {
|
|
6
|
+
import { ActivitySpecialOutput, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
7
|
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
|
|
7
8
|
import { mainEventBus } from '#bus';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { inheritsFromAnyNamedType } from '#helpers/errors';
|
|
10
|
+
import { serializeError } from '#helpers/error_serializer';
|
|
11
|
+
import { FatalError, TransparentFatalError } from '#errors';
|
|
10
12
|
|
|
11
13
|
/*
|
|
12
14
|
This interceptor wraps every activity execution with cross-cutting concerns:
|
|
@@ -51,12 +53,9 @@ export class ActivityExecutionInterceptor {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
const state = {
|
|
54
|
-
heartbeat: null
|
|
55
|
-
attributes: []
|
|
56
|
+
heartbeat: null
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
const addAttribute = attribute => state.attributes.push( attribute );
|
|
59
|
-
|
|
60
59
|
// Adds context accessible information
|
|
61
60
|
const storageContext = {
|
|
62
61
|
parentId: activityId,
|
|
@@ -64,8 +63,7 @@ export class ActivityExecutionInterceptor {
|
|
|
64
63
|
activityInfo,
|
|
65
64
|
workflowDetails,
|
|
66
65
|
workflowFilename,
|
|
67
|
-
traceInfo
|
|
68
|
-
addAttribute
|
|
66
|
+
traceInfo
|
|
69
67
|
};
|
|
70
68
|
|
|
71
69
|
mainEventBus.emit( BusEventType.ACTIVITY_START, { activityInfo, workflowDetails, outputActivityKind } );
|
|
@@ -77,19 +75,34 @@ export class ActivityExecutionInterceptor {
|
|
|
77
75
|
|
|
78
76
|
const output = await Storage.runWithContext( async _ => next( input ), storageContext );
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
mainEventBus.emit( BusEventType.ACTIVITY_END, { activityInfo, aggregations, workflowDetails, outputActivityKind } );
|
|
78
|
+
mainEventBus.emit( BusEventType.ACTIVITY_END, { activityInfo, workflowDetails, outputActivityKind } );
|
|
83
79
|
Tracing.addEventEnd( { id: activityId, details: output, traceInfo } );
|
|
84
80
|
|
|
85
|
-
return
|
|
81
|
+
return output;
|
|
86
82
|
|
|
87
83
|
} catch ( error ) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
// Record async completion handoff as a trace end without emitting a bus event.
|
|
85
|
+
if ( error instanceof CompleteAsyncError ) {
|
|
86
|
+
Tracing.addEventEnd( { id: activityId, details: ActivitySpecialOutput.ASYNC_HANDOFF, traceInfo } );
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const unwrappedError = error instanceof TransparentFatalError ? error.cause : error;
|
|
91
|
+
|
|
92
|
+
Tracing.addEventError( { id: activityId, details: unwrappedError, traceInfo } );
|
|
93
|
+
mainEventBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, workflowDetails, outputActivityKind, error: unwrappedError } );
|
|
94
|
+
|
|
95
|
+
// Native Temporal errors are just re-thrown
|
|
96
|
+
if ( error instanceof TemporalFailure ) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nonRetryable = error instanceof FatalError || inheritsFromAnyNamedType( error, activityInfo.retryPolicy?.nonRetryableErrorTypes ?? [] );
|
|
101
|
+
throw ApplicationFailure.fromError( unwrappedError, {
|
|
102
|
+
nonRetryable,
|
|
103
|
+
cause: unwrappedError,
|
|
104
|
+
details: [ { error: serializeError( unwrappedError, { dropKeys: [ 'stack' ] } ) } ]
|
|
105
|
+
} );
|
|
93
106
|
} finally {
|
|
94
107
|
clearInterval( state.heartbeat );
|
|
95
108
|
}
|