@outputai/core 0.1.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/LICENSE +201 -0
- package/README.md +11 -0
- package/bin/healthcheck.mjs +36 -0
- package/bin/healthcheck.spec.js +90 -0
- package/bin/worker.sh +26 -0
- package/package.json +67 -0
- package/src/activity_integration/context.d.ts +27 -0
- package/src/activity_integration/context.js +17 -0
- package/src/activity_integration/context.spec.js +42 -0
- package/src/activity_integration/events.d.ts +7 -0
- package/src/activity_integration/events.js +10 -0
- package/src/activity_integration/index.d.ts +9 -0
- package/src/activity_integration/index.js +3 -0
- package/src/activity_integration/tracing.d.ts +32 -0
- package/src/activity_integration/tracing.js +37 -0
- package/src/async_storage.js +19 -0
- package/src/bus.js +3 -0
- package/src/consts.js +32 -0
- package/src/errors.d.ts +15 -0
- package/src/errors.js +14 -0
- package/src/hooks/index.d.ts +28 -0
- package/src/hooks/index.js +32 -0
- package/src/index.d.ts +49 -0
- package/src/index.js +4 -0
- package/src/interface/evaluation_result.d.ts +173 -0
- package/src/interface/evaluation_result.js +215 -0
- package/src/interface/evaluator.d.ts +70 -0
- package/src/interface/evaluator.js +34 -0
- package/src/interface/evaluator.spec.js +565 -0
- package/src/interface/index.d.ts +9 -0
- package/src/interface/index.js +26 -0
- package/src/interface/step.d.ts +138 -0
- package/src/interface/step.js +22 -0
- package/src/interface/types.d.ts +27 -0
- package/src/interface/validations/runtime.js +20 -0
- package/src/interface/validations/runtime.spec.js +29 -0
- package/src/interface/validations/schema_utils.js +8 -0
- package/src/interface/validations/schema_utils.spec.js +67 -0
- package/src/interface/validations/static.js +136 -0
- package/src/interface/validations/static.spec.js +366 -0
- package/src/interface/webhook.d.ts +84 -0
- package/src/interface/webhook.js +64 -0
- package/src/interface/webhook.spec.js +122 -0
- package/src/interface/workflow.d.ts +273 -0
- package/src/interface/workflow.js +128 -0
- package/src/interface/workflow.spec.js +467 -0
- package/src/interface/workflow_context.js +31 -0
- package/src/interface/workflow_utils.d.ts +76 -0
- package/src/interface/workflow_utils.js +50 -0
- package/src/interface/workflow_utils.spec.js +190 -0
- package/src/interface/zod_integration.spec.js +646 -0
- package/src/internal_activities/index.js +66 -0
- package/src/internal_activities/index.spec.js +102 -0
- package/src/logger.js +73 -0
- package/src/tracing/internal_interface.js +71 -0
- package/src/tracing/processors/local/index.js +111 -0
- package/src/tracing/processors/local/index.spec.js +149 -0
- package/src/tracing/processors/s3/configs.js +31 -0
- package/src/tracing/processors/s3/configs.spec.js +64 -0
- package/src/tracing/processors/s3/index.js +114 -0
- package/src/tracing/processors/s3/index.spec.js +153 -0
- package/src/tracing/processors/s3/redis_client.js +62 -0
- package/src/tracing/processors/s3/redis_client.spec.js +185 -0
- package/src/tracing/processors/s3/s3_client.js +27 -0
- package/src/tracing/processors/s3/s3_client.spec.js +62 -0
- package/src/tracing/tools/build_trace_tree.js +83 -0
- package/src/tracing/tools/build_trace_tree.spec.js +135 -0
- package/src/tracing/tools/utils.js +21 -0
- package/src/tracing/tools/utils.spec.js +14 -0
- package/src/tracing/trace_engine.js +97 -0
- package/src/tracing/trace_engine.spec.js +199 -0
- package/src/utils/index.d.ts +134 -0
- package/src/utils/index.js +2 -0
- package/src/utils/resolve_invocation_dir.js +34 -0
- package/src/utils/resolve_invocation_dir.spec.js +102 -0
- package/src/utils/utils.js +211 -0
- package/src/utils/utils.spec.js +448 -0
- package/src/worker/bundler_options.js +43 -0
- package/src/worker/catalog_workflow/catalog.js +114 -0
- package/src/worker/catalog_workflow/index.js +54 -0
- package/src/worker/catalog_workflow/index.spec.js +196 -0
- package/src/worker/catalog_workflow/workflow.js +24 -0
- package/src/worker/configs.js +49 -0
- package/src/worker/configs.spec.js +130 -0
- package/src/worker/index.js +89 -0
- package/src/worker/index.spec.js +177 -0
- package/src/worker/interceptors/activity.js +62 -0
- package/src/worker/interceptors/activity.spec.js +212 -0
- package/src/worker/interceptors/workflow.js +70 -0
- package/src/worker/interceptors/workflow.spec.js +167 -0
- package/src/worker/interceptors.js +10 -0
- package/src/worker/loader.js +151 -0
- package/src/worker/loader.spec.js +236 -0
- package/src/worker/loader_tools.js +132 -0
- package/src/worker/loader_tools.spec.js +156 -0
- package/src/worker/log_hooks.js +95 -0
- package/src/worker/log_hooks.spec.js +217 -0
- package/src/worker/sandboxed_utils.js +18 -0
- package/src/worker/shutdown.js +26 -0
- package/src/worker/shutdown.spec.js +82 -0
- package/src/worker/sinks.js +74 -0
- package/src/worker/start_catalog.js +36 -0
- package/src/worker/start_catalog.spec.js +118 -0
- package/src/worker/webpack_loaders/consts.js +9 -0
- package/src/worker/webpack_loaders/tools.js +548 -0
- package/src/worker/webpack_loaders/tools.spec.js +330 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +221 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +336 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +61 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +216 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +196 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +123 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +205 -0
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +613 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
BusEventType,
|
|
4
|
+
ComponentType,
|
|
5
|
+
LifecycleEvent,
|
|
6
|
+
WORKFLOW_CATALOG
|
|
7
|
+
} from '#consts';
|
|
8
|
+
|
|
9
|
+
const activityLogMock = vi.hoisted( () => ( { info: vi.fn(), error: vi.fn() } ) );
|
|
10
|
+
const workflowLogMock = vi.hoisted( () => ( { info: vi.fn(), error: vi.fn() } ) );
|
|
11
|
+
const createChildLoggerMock = vi.hoisted( () =>
|
|
12
|
+
vi.fn( name => ( name === 'Activity' ? activityLogMock : workflowLogMock ) )
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const onHandlers = vi.hoisted( () => ( {} ) );
|
|
16
|
+
const messageBusMock = vi.hoisted( () => ( {
|
|
17
|
+
on: vi.fn( ( eventType, handler ) => {
|
|
18
|
+
onHandlers[eventType] = handler;
|
|
19
|
+
} )
|
|
20
|
+
} ) );
|
|
21
|
+
|
|
22
|
+
vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
|
|
23
|
+
vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
|
|
24
|
+
|
|
25
|
+
import './log_hooks.js';
|
|
26
|
+
|
|
27
|
+
describe( 'log_hooks', () => {
|
|
28
|
+
beforeEach( () => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
describe( 'activity events', () => {
|
|
33
|
+
const basePayload = {
|
|
34
|
+
id: 'act-1',
|
|
35
|
+
name: 'myWorkflow#myStep',
|
|
36
|
+
kind: 'step',
|
|
37
|
+
workflowId: 'wf-1',
|
|
38
|
+
workflowName: 'myWorkflow'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
it( 'ACTIVITY_START logs full message and second arg', () => {
|
|
42
|
+
onHandlers[BusEventType.ACTIVITY_START]( basePayload );
|
|
43
|
+
|
|
44
|
+
expect( activityLogMock.info ).toHaveBeenCalledTimes( 1 );
|
|
45
|
+
expect( activityLogMock.info ).toHaveBeenCalledWith(
|
|
46
|
+
'Started myWorkflow#myStep step',
|
|
47
|
+
{
|
|
48
|
+
event: LifecycleEvent.START,
|
|
49
|
+
activityId: 'act-1',
|
|
50
|
+
activityName: 'myWorkflow#myStep',
|
|
51
|
+
activityKind: 'step',
|
|
52
|
+
workflowId: 'wf-1',
|
|
53
|
+
workflowName: 'myWorkflow'
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'ACTIVITY_START does not log when kind is INTERNAL_STEP', () => {
|
|
59
|
+
onHandlers[BusEventType.ACTIVITY_START]( {
|
|
60
|
+
...basePayload,
|
|
61
|
+
kind: ComponentType.INTERNAL_STEP
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
expect( activityLogMock.info ).not.toHaveBeenCalled();
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
it( 'ACTIVITY_END logs full message and second arg', () => {
|
|
68
|
+
onHandlers[BusEventType.ACTIVITY_END]( { ...basePayload, duration: 42 } );
|
|
69
|
+
|
|
70
|
+
expect( activityLogMock.info ).toHaveBeenCalledTimes( 1 );
|
|
71
|
+
expect( activityLogMock.info ).toHaveBeenCalledWith(
|
|
72
|
+
'Ended myWorkflow#myStep step',
|
|
73
|
+
{
|
|
74
|
+
event: LifecycleEvent.END,
|
|
75
|
+
activityId: 'act-1',
|
|
76
|
+
activityName: 'myWorkflow#myStep',
|
|
77
|
+
activityKind: 'step',
|
|
78
|
+
workflowId: 'wf-1',
|
|
79
|
+
workflowName: 'myWorkflow',
|
|
80
|
+
durationMs: 42
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'ACTIVITY_END does not log when kind is INTERNAL_STEP', () => {
|
|
86
|
+
onHandlers[BusEventType.ACTIVITY_END]( {
|
|
87
|
+
...basePayload,
|
|
88
|
+
kind: ComponentType.INTERNAL_STEP,
|
|
89
|
+
duration: 10
|
|
90
|
+
} );
|
|
91
|
+
|
|
92
|
+
expect( activityLogMock.info ).not.toHaveBeenCalled();
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'ACTIVITY_ERROR logs full message and second arg', () => {
|
|
96
|
+
const err = new Error( 'step failed' );
|
|
97
|
+
onHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
98
|
+
...basePayload,
|
|
99
|
+
duration: 100,
|
|
100
|
+
error: err
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
expect( activityLogMock.error ).toHaveBeenCalledTimes( 1 );
|
|
104
|
+
expect( activityLogMock.error ).toHaveBeenCalledWith(
|
|
105
|
+
'Error myWorkflow#myStep step: Error',
|
|
106
|
+
{
|
|
107
|
+
event: LifecycleEvent.ERROR,
|
|
108
|
+
activityId: 'act-1',
|
|
109
|
+
activityName: 'myWorkflow#myStep',
|
|
110
|
+
activityKind: 'step',
|
|
111
|
+
workflowId: 'wf-1',
|
|
112
|
+
workflowName: 'myWorkflow',
|
|
113
|
+
durationMs: 100,
|
|
114
|
+
error: 'step failed'
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'ACTIVITY_ERROR does not log when kind is INTERNAL_STEP', () => {
|
|
120
|
+
onHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
121
|
+
...basePayload,
|
|
122
|
+
kind: ComponentType.INTERNAL_STEP,
|
|
123
|
+
duration: 5,
|
|
124
|
+
error: new Error( 'x' )
|
|
125
|
+
} );
|
|
126
|
+
|
|
127
|
+
expect( activityLogMock.error ).not.toHaveBeenCalled();
|
|
128
|
+
} );
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
describe( 'workflow events', () => {
|
|
132
|
+
const basePayload = { id: 'wf-1', name: 'myWorkflow' };
|
|
133
|
+
|
|
134
|
+
it( 'WORKFLOW_START logs full message and second arg', () => {
|
|
135
|
+
onHandlers[BusEventType.WORKFLOW_START]( basePayload );
|
|
136
|
+
|
|
137
|
+
expect( workflowLogMock.info ).toHaveBeenCalledTimes( 1 );
|
|
138
|
+
expect( workflowLogMock.info ).toHaveBeenCalledWith(
|
|
139
|
+
'Started myWorkflow workflow',
|
|
140
|
+
{
|
|
141
|
+
event: LifecycleEvent.START,
|
|
142
|
+
workflowId: 'wf-1',
|
|
143
|
+
workflowName: 'myWorkflow'
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
} );
|
|
147
|
+
|
|
148
|
+
it( 'WORKFLOW_START does not log when name is WORKFLOW_CATALOG', () => {
|
|
149
|
+
onHandlers[BusEventType.WORKFLOW_START]( {
|
|
150
|
+
id: 'cat-1',
|
|
151
|
+
name: WORKFLOW_CATALOG
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
expect( workflowLogMock.info ).not.toHaveBeenCalled();
|
|
155
|
+
} );
|
|
156
|
+
|
|
157
|
+
it( 'WORKFLOW_END logs full message and second arg', () => {
|
|
158
|
+
onHandlers[BusEventType.WORKFLOW_END]( {
|
|
159
|
+
...basePayload,
|
|
160
|
+
duration: 200
|
|
161
|
+
} );
|
|
162
|
+
|
|
163
|
+
expect( workflowLogMock.info ).toHaveBeenCalledTimes( 1 );
|
|
164
|
+
expect( workflowLogMock.info ).toHaveBeenCalledWith(
|
|
165
|
+
'Ended myWorkflow workflow',
|
|
166
|
+
{
|
|
167
|
+
event: LifecycleEvent.END,
|
|
168
|
+
workflowId: 'wf-1',
|
|
169
|
+
workflowName: 'myWorkflow',
|
|
170
|
+
durationMs: 200
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
it( 'WORKFLOW_END does not log when name is WORKFLOW_CATALOG', () => {
|
|
176
|
+
onHandlers[BusEventType.WORKFLOW_END]( {
|
|
177
|
+
id: 'cat-1',
|
|
178
|
+
name: WORKFLOW_CATALOG,
|
|
179
|
+
duration: 50
|
|
180
|
+
} );
|
|
181
|
+
|
|
182
|
+
expect( workflowLogMock.info ).not.toHaveBeenCalled();
|
|
183
|
+
} );
|
|
184
|
+
|
|
185
|
+
it( 'WORKFLOW_ERROR logs full message and second arg', () => {
|
|
186
|
+
const err = new TypeError( 'workflow boom' );
|
|
187
|
+
onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
188
|
+
...basePayload,
|
|
189
|
+
duration: 150,
|
|
190
|
+
error: err
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
expect( workflowLogMock.error ).toHaveBeenCalledTimes( 1 );
|
|
194
|
+
expect( workflowLogMock.error ).toHaveBeenCalledWith(
|
|
195
|
+
'Error myWorkflow workflow: TypeError',
|
|
196
|
+
{
|
|
197
|
+
event: LifecycleEvent.ERROR,
|
|
198
|
+
workflowId: 'wf-1',
|
|
199
|
+
workflowName: 'myWorkflow',
|
|
200
|
+
durationMs: 150,
|
|
201
|
+
error: 'workflow boom'
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
it( 'WORKFLOW_ERROR does not log when name is WORKFLOW_CATALOG', () => {
|
|
207
|
+
onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
208
|
+
id: 'cat-1',
|
|
209
|
+
name: WORKFLOW_CATALOG,
|
|
210
|
+
duration: 1,
|
|
211
|
+
error: new Error( 'x' )
|
|
212
|
+
} );
|
|
213
|
+
|
|
214
|
+
expect( workflowLogMock.error ).not.toHaveBeenCalled();
|
|
215
|
+
} );
|
|
216
|
+
} );
|
|
217
|
+
} );
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// THIS IS SAFE TO RUN AT TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
|
+
import { defaultPayloadConverter } from '@temporalio/common';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
@important: They plain JS values need to be converted to "payload":
|
|
6
|
+
- https://typescript.temporal.io/api/namespaces/common/#headers
|
|
7
|
+
- https://community.temporal.io/t/specify-temporal-headers-when-starting-workflow/6712
|
|
8
|
+
*/
|
|
9
|
+
export const memoToHeaders = memo =>
|
|
10
|
+
Object.fromEntries(
|
|
11
|
+
Object.entries( memo ?? {} ).map( ( [ k, v ] ) => [ k, defaultPayloadConverter.toPayload( v ) ] )
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// And the opposite of the function above
|
|
15
|
+
export const headersToObject = headers =>
|
|
16
|
+
Object.fromEntries(
|
|
17
|
+
Object.entries( headers ?? {} ).map( ( [ k, v ] ) => [ k, defaultPayloadConverter.fromPayload( v ) ] )
|
|
18
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const FORCE_QUIT_GRACE_MS = 1000;
|
|
2
|
+
|
|
3
|
+
export const registerShutdown = ( { worker, log } ) => {
|
|
4
|
+
const state = { isShuttingDown: false, shutdownStartedAt: null };
|
|
5
|
+
|
|
6
|
+
const shutdown = signal => {
|
|
7
|
+
if ( state.isShuttingDown ) {
|
|
8
|
+
const elapsed = Date.now() - state.shutdownStartedAt;
|
|
9
|
+
|
|
10
|
+
// If running with npx, 2 kill signals are received in rapid succession,
|
|
11
|
+
// this ignores the second interruption when it is right after the first.
|
|
12
|
+
if ( elapsed < FORCE_QUIT_GRACE_MS ) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
log.warn( 'Force quitting...' );
|
|
16
|
+
process.exit( 1 );
|
|
17
|
+
}
|
|
18
|
+
state.isShuttingDown = true;
|
|
19
|
+
state.shutdownStartedAt = Date.now();
|
|
20
|
+
log.info( 'Shutting down...', { signal } );
|
|
21
|
+
worker.shutdown();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
process.on( 'SIGTERM', () => shutdown( 'SIGTERM' ) );
|
|
25
|
+
process.on( 'SIGINT', () => shutdown( 'SIGINT' ) );
|
|
26
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { registerShutdown } from './shutdown.js';
|
|
3
|
+
|
|
4
|
+
describe( 'worker/shutdown', () => {
|
|
5
|
+
const mockLog = { info: vi.fn(), warn: vi.fn() };
|
|
6
|
+
const shutdownMock = vi.fn();
|
|
7
|
+
const mockWorker = { shutdown: shutdownMock };
|
|
8
|
+
const onHandlers = {};
|
|
9
|
+
const exitMock = vi.fn();
|
|
10
|
+
const originalOn = process.on;
|
|
11
|
+
const originalExit = process.exit;
|
|
12
|
+
|
|
13
|
+
beforeEach( () => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
Object.keys( onHandlers ).forEach( k => delete onHandlers[k] );
|
|
16
|
+
process.on = vi.fn( ( event, handler ) => {
|
|
17
|
+
onHandlers[event] = handler;
|
|
18
|
+
} );
|
|
19
|
+
process.exit = exitMock;
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
afterEach( () => {
|
|
23
|
+
process.on = originalOn;
|
|
24
|
+
process.exit = originalExit;
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'registers SIGTERM and SIGINT handlers', () => {
|
|
28
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
29
|
+
|
|
30
|
+
expect( process.on ).toHaveBeenCalledWith( 'SIGTERM', expect.any( Function ) );
|
|
31
|
+
expect( process.on ).toHaveBeenCalledWith( 'SIGINT', expect.any( Function ) );
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'on first signal: logs, calls worker.shutdown(), does not exit', () => {
|
|
35
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
36
|
+
|
|
37
|
+
onHandlers.SIGTERM();
|
|
38
|
+
|
|
39
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Shutting down...', { signal: 'SIGTERM' } );
|
|
40
|
+
expect( shutdownMock ).toHaveBeenCalledTimes( 1 );
|
|
41
|
+
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
42
|
+
expect( exitMock ).not.toHaveBeenCalled();
|
|
43
|
+
} );
|
|
44
|
+
|
|
45
|
+
it( 'on first SIGINT: logs with SIGINT', () => {
|
|
46
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
47
|
+
|
|
48
|
+
onHandlers.SIGINT();
|
|
49
|
+
|
|
50
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Shutting down...', { signal: 'SIGINT' } );
|
|
51
|
+
expect( shutdownMock ).toHaveBeenCalledTimes( 1 );
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'on second signal within grace period: ignores (no force quit)', () => {
|
|
55
|
+
vi.useFakeTimers();
|
|
56
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
57
|
+
|
|
58
|
+
onHandlers.SIGTERM();
|
|
59
|
+
onHandlers.SIGINT();
|
|
60
|
+
|
|
61
|
+
expect( mockLog.info ).toHaveBeenCalledTimes( 1 );
|
|
62
|
+
expect( shutdownMock ).toHaveBeenCalledTimes( 1 );
|
|
63
|
+
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
64
|
+
expect( exitMock ).not.toHaveBeenCalled();
|
|
65
|
+
|
|
66
|
+
vi.useRealTimers();
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
it( 'on second signal after grace period: logs force quit and exits with 1', () => {
|
|
70
|
+
vi.useFakeTimers();
|
|
71
|
+
registerShutdown( { worker: mockWorker, log: mockLog } );
|
|
72
|
+
|
|
73
|
+
onHandlers.SIGTERM();
|
|
74
|
+
vi.advanceTimersByTime( 1001 );
|
|
75
|
+
onHandlers.SIGINT();
|
|
76
|
+
|
|
77
|
+
expect( mockLog.warn ).toHaveBeenCalledWith( 'Force quitting...' );
|
|
78
|
+
expect( exitMock ).toHaveBeenCalledWith( 1 );
|
|
79
|
+
|
|
80
|
+
vi.useRealTimers();
|
|
81
|
+
} );
|
|
82
|
+
} );
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { BusEventType, ComponentType } from '#consts';
|
|
2
|
+
import * as Tracing from '#tracing';
|
|
3
|
+
import { messageBus } from '#bus';
|
|
4
|
+
|
|
5
|
+
// This sink allow for sandbox Temporal environment to send trace logs back to the main thread.
|
|
6
|
+
export const sinks = {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Workflow lifecycle sinks
|
|
10
|
+
*/
|
|
11
|
+
workflow: {
|
|
12
|
+
start: {
|
|
13
|
+
fn: ( workflowInfo, input ) => {
|
|
14
|
+
const { workflowId: id, workflowType: name, memo: { parentId, executionContext } } = workflowInfo;
|
|
15
|
+
messageBus.emit( BusEventType.WORKFLOW_START, { id, name } );
|
|
16
|
+
if ( executionContext ) { // filters out internal workflows
|
|
17
|
+
Tracing.addEventStart( { id, kind: ComponentType.WORKFLOW, name, details: input, parentId, executionContext } );
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
callDuringReplay: false
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
end: {
|
|
24
|
+
fn: ( workflowInfo, output ) => {
|
|
25
|
+
const { workflowId: id, workflowType: name, startTime, memo: { executionContext } } = workflowInfo;
|
|
26
|
+
messageBus.emit( BusEventType.WORKFLOW_END, { id, name, duration: Date.now() - startTime.getTime() } );
|
|
27
|
+
if ( executionContext ) { // filters out internal workflows
|
|
28
|
+
Tracing.addEventEnd( { id, details: output, executionContext } );
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
callDuringReplay: false
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
error: {
|
|
35
|
+
fn: ( workflowInfo, error ) => {
|
|
36
|
+
const { workflowId: id, workflowType: name, startTime, memo: { executionContext } } = workflowInfo;
|
|
37
|
+
messageBus.emit( BusEventType.WORKFLOW_ERROR, { id, name, error, duration: Date.now() - startTime.getTime() } );
|
|
38
|
+
if ( executionContext ) { // filters out internal workflows
|
|
39
|
+
Tracing.addEventError( { id, details: error, executionContext } );
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
callDuringReplay: false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generic trace sinks
|
|
48
|
+
*/
|
|
49
|
+
trace: {
|
|
50
|
+
start: {
|
|
51
|
+
fn: ( workflowInfo, { id, name, kind, details } ) => {
|
|
52
|
+
const { memo: { executionContext, parentId } } = workflowInfo;
|
|
53
|
+
Tracing.addEventStart( { id, kind, name, details, parentId, executionContext } );
|
|
54
|
+
},
|
|
55
|
+
callDuringReplay: false
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
end: {
|
|
59
|
+
fn: ( workflowInfo, { id, details } ) => {
|
|
60
|
+
const { memo: { executionContext } } = workflowInfo;
|
|
61
|
+
Tracing.addEventEnd( { id, details, executionContext } );
|
|
62
|
+
},
|
|
63
|
+
callDuringReplay: false
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
error: {
|
|
67
|
+
fn: ( workflowInfo, { id, details } ) => {
|
|
68
|
+
const { memo: { executionContext } } = workflowInfo;
|
|
69
|
+
Tracing.addEventError( { id, details, executionContext } );
|
|
70
|
+
},
|
|
71
|
+
callDuringReplay: false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Client, WorkflowNotFoundError } from '@temporalio/client';
|
|
2
|
+
import { WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
3
|
+
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
|
+
import { catalogId, taskQueue } from './configs.js';
|
|
5
|
+
import { createChildLogger } from '#logger';
|
|
6
|
+
|
|
7
|
+
const log = createChildLogger( 'Catalog' );
|
|
8
|
+
|
|
9
|
+
export const startCatalog = async ( { connection, namespace, catalog } ) => {
|
|
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
|
+
|
|
29
|
+
log.info( 'Starting catalog workflow...' );
|
|
30
|
+
await client.workflow.start( WORKFLOW_CATALOG, {
|
|
31
|
+
taskQueue,
|
|
32
|
+
workflowId: catalogId, // use the name of the task queue as the catalog name, ensuring uniqueness
|
|
33
|
+
workflowIdConflictPolicy: WorkflowIdConflictPolicy.FAIL,
|
|
34
|
+
args: [ catalog ]
|
|
35
|
+
} );
|
|
36
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { WorkflowNotFoundError } from '@temporalio/client';
|
|
3
|
+
|
|
4
|
+
const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
5
|
+
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
6
|
+
|
|
7
|
+
vi.mock( '#consts', () => ( { WORKFLOW_CATALOG: 'catalog' } ) );
|
|
8
|
+
|
|
9
|
+
const catalogId = 'test-catalog';
|
|
10
|
+
const taskQueue = 'test-queue';
|
|
11
|
+
vi.mock( './configs.js', () => ( { catalogId, taskQueue } ) );
|
|
12
|
+
|
|
13
|
+
const describeMock = vi.fn();
|
|
14
|
+
const executeUpdateMock = vi.fn();
|
|
15
|
+
const workflowStartMock = vi.fn().mockResolvedValue( undefined );
|
|
16
|
+
vi.mock( '@temporalio/client', async importOriginal => {
|
|
17
|
+
const actual = await importOriginal();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
Client: vi.fn().mockImplementation( function () {
|
|
21
|
+
return {
|
|
22
|
+
workflow: {
|
|
23
|
+
start: workflowStartMock,
|
|
24
|
+
getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
} )
|
|
28
|
+
};
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
vi.mock( '@temporalio/common', () => ( { WorkflowIdConflictPolicy: { FAIL: 'FAIL' } } ) );
|
|
32
|
+
|
|
33
|
+
describe( 'worker/start_catalog', () => {
|
|
34
|
+
const mockConnection = {};
|
|
35
|
+
const namespace = 'default';
|
|
36
|
+
const catalog = { workflows: [], activities: {} };
|
|
37
|
+
|
|
38
|
+
beforeEach( () => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
workflowStartMock.mockResolvedValue( undefined );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
it( 'when previous catalog still running: completes it then starts catalog workflow', async () => {
|
|
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
|
+
|
|
83
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
84
|
+
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
85
|
+
|
|
86
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
87
|
+
expect( mockLog.info ).not.toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
88
|
+
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
89
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
90
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
91
|
+
taskQueue,
|
|
92
|
+
workflowId: catalogId,
|
|
93
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
94
|
+
args: [ catalog ]
|
|
95
|
+
} );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
it( 'when describe or complete fails with other error: logs warn and still starts catalog workflow', async () => {
|
|
99
|
+
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
100
|
+
executeUpdateMock.mockRejectedValue( new Error( 'Connection refused' ) );
|
|
101
|
+
|
|
102
|
+
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
|
+
} );
|
|
117
|
+
} );
|
|
118
|
+
} );
|