@outputai/core 0.8.1-next.e92f632.0 → 0.8.2-dev.e78f6b4.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 +9 -10
- package/src/bus.js +1 -1
- package/src/consts.js +5 -2
- package/src/helpers/component.js +12 -0
- package/src/helpers/component.spec.js +54 -0
- package/src/helpers/fetch.js +105 -0
- package/src/helpers/fetch.spec.js +203 -0
- package/src/helpers/function.js +15 -0
- package/src/helpers/function.spec.js +48 -0
- package/src/helpers/object.js +98 -0
- package/src/helpers/object.spec.js +377 -0
- package/src/helpers/promise.js +29 -0
- package/src/helpers/promise.spec.js +35 -0
- package/src/helpers/string.js +30 -0
- package/src/helpers/string.spec.js +64 -0
- package/src/interface/evaluator.js +14 -12
- package/src/interface/evaluator.spec.js +10 -6
- package/src/interface/index.d.ts +7 -6
- package/src/interface/index.js +2 -0
- package/src/interface/logger.d.ts +53 -0
- package/src/interface/logger.js +68 -0
- package/src/interface/logger.spec.js +138 -0
- package/src/interface/step.js +15 -12
- package/src/interface/step.spec.js +10 -6
- package/src/interface/webhook.d.ts +21 -2
- package/src/interface/workflow.js +85 -78
- package/src/interface/workflow.spec.js +11 -4
- package/src/internal_activities/index.js +38 -35
- package/src/internal_activities/index.spec.js +27 -4
- package/src/logger/development.js +2 -2
- package/src/logger/development.spec.js +19 -2
- package/src/logger/production.js +1 -1
- package/src/logger/production.spec.js +24 -5
- package/src/sdk/README.md +47 -0
- package/src/sdk/helpers/component_metadata.d.ts +17 -0
- package/src/sdk/helpers/component_metadata.js +6 -0
- package/src/sdk/helpers/component_metadata.spec.js +30 -0
- package/src/sdk/helpers/index.d.ts +12 -0
- package/src/sdk/helpers/index.js +3 -0
- package/src/sdk/helpers/objects.d.ts +51 -0
- package/src/sdk/helpers/objects.js +8 -0
- package/src/sdk/helpers/objects.spec.js +16 -0
- package/src/sdk/helpers/path.d.ts +11 -0
- package/src/sdk/helpers/path.js +32 -0
- package/src/{utils/resolve_invocation_dir.spec.js → sdk/helpers/path.spec.js} +9 -9
- package/src/sdk/runtime/context.d.ts +30 -0
- package/src/sdk/runtime/context.js +15 -0
- package/src/{activity_integration → sdk/runtime}/context.spec.js +5 -5
- package/src/sdk/runtime/events.d.ts +15 -0
- package/src/sdk/runtime/events.js +18 -0
- package/src/{activity_integration → sdk/runtime}/events.spec.js +8 -9
- package/src/sdk/runtime/index.d.ts +12 -0
- package/src/sdk/runtime/index.js +3 -0
- package/src/sdk/runtime/tracing.d.ts +46 -0
- package/src/sdk/runtime/tracing.js +11 -0
- package/src/tracing/processors/s3/redis_client.spec.js +0 -6
- package/src/tracing/processors/s3/s3_client.spec.js +0 -6
- package/src/tracing/trace_engine.js +1 -1
- package/src/worker/catalog_workflow/catalog_job.js +1 -1
- package/src/worker/catalog_workflow/index.spec.js +8 -11
- package/src/worker/configs.js +14 -1
- package/src/worker/configs.spec.js +34 -1
- package/src/worker/connection_monitor.js +3 -14
- package/src/worker/connection_monitor.spec.js +4 -21
- package/src/worker/global_functions.js +14 -0
- package/src/worker/global_functions.spec.js +55 -0
- package/src/worker/index.js +11 -3
- package/src/worker/index.spec.js +29 -1
- package/src/worker/interceptors/activity.js +2 -2
- package/src/worker/interceptors/workflow.js +3 -3
- package/src/worker/interceptors/workflow.spec.js +1 -1
- package/src/worker/loader/matchers.js +1 -1
- package/src/worker/loader/tools.js +1 -1
- package/src/worker/loader/tools.spec.js +1 -1
- package/src/worker/log_hooks.js +14 -0
- package/src/worker/log_hooks.spec.js +83 -2
- package/src/worker/sinks.js +7 -1
- package/src/worker/sinks.spec.js +203 -0
- package/src/activity_integration/context.d.ts +0 -23
- package/src/activity_integration/context.js +0 -18
- package/src/activity_integration/event_id_integration.spec.js +0 -52
- package/src/activity_integration/events.d.ts +0 -10
- package/src/activity_integration/events.js +0 -15
- package/src/activity_integration/index.d.ts +0 -9
- package/src/activity_integration/index.js +0 -3
- package/src/activity_integration/tracing.d.ts +0 -40
- package/src/activity_integration/tracing.js +0 -48
- package/src/utils/index.d.ts +0 -180
- package/src/utils/index.js +0 -2
- package/src/utils/resolve_invocation_dir.js +0 -34
- package/src/utils/utils.js +0 -334
- package/src/utils/utils.spec.js +0 -723
- /package/src/{internal_utils → helpers}/aggregations.js +0 -0
- /package/src/{internal_utils → helpers}/aggregations.spec.js +0 -0
- /package/src/{internal_utils → helpers}/errors.js +0 -0
- /package/src/{internal_utils → helpers}/errors.spec.js +0 -0
- /package/src/{internal_utils → helpers}/temporal_context.js +0 -0
- /package/src/{internal_utils → helpers}/temporal_context.spec.ts +0 -0
- /package/src/{internal_utils → helpers}/trace_info.js +0 -0
- /package/src/{internal_utils → helpers}/trace_info.spec.js +0 -0
- /package/src/{internal_utils → helpers}/workflow_context.js +0 -0
- /package/src/{internal_utils → helpers}/workflow_context.spec.js +0 -0
|
@@ -5,7 +5,7 @@ vi.mock( '#async_storage', () => ( {
|
|
|
5
5
|
Storage: { load: loadMock }
|
|
6
6
|
} ) );
|
|
7
7
|
|
|
8
|
-
describe( '
|
|
8
|
+
describe( 'Context.getActivityContext', () => {
|
|
9
9
|
beforeEach( () => {
|
|
10
10
|
vi.clearAllMocks();
|
|
11
11
|
vi.resetModules();
|
|
@@ -13,8 +13,8 @@ describe( 'getExecutionContext', () => {
|
|
|
13
13
|
|
|
14
14
|
it( 'returns null when no context is stored', async () => {
|
|
15
15
|
loadMock.mockReturnValue( undefined );
|
|
16
|
-
const {
|
|
17
|
-
expect(
|
|
16
|
+
const { Context } = await import( './index.js' );
|
|
17
|
+
expect( Context.getActivityContext() ).toBeNull();
|
|
18
18
|
} );
|
|
19
19
|
|
|
20
20
|
it( 'returns activity execution context from storage', async () => {
|
|
@@ -28,8 +28,8 @@ describe( 'getExecutionContext', () => {
|
|
|
28
28
|
activityInfo,
|
|
29
29
|
workflowFilename: '/workflows/myWorkflow.js'
|
|
30
30
|
} );
|
|
31
|
-
const {
|
|
32
|
-
expect(
|
|
31
|
+
const { Context } = await import( './index.js' );
|
|
32
|
+
expect( Context.getActivityContext() ).toEqual( {
|
|
33
33
|
activityInfo,
|
|
34
34
|
workflowFilename: '/workflows/myWorkflow.js'
|
|
35
35
|
} );
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools to interact with Events
|
|
3
|
+
*/
|
|
4
|
+
export declare const Event: {
|
|
5
|
+
/**
|
|
6
|
+
* Emits a custom event on the in-process message bus.
|
|
7
|
+
*
|
|
8
|
+
* When called inside an Output activity context, the framework automatically
|
|
9
|
+
* attaches `activityInfo`, `workflowDetails`, and `outputActivityKind` onto the emitted payload.
|
|
10
|
+
*
|
|
11
|
+
* @param eventName - The name of the event to emit
|
|
12
|
+
* @param payload - An optional payload to send to the event
|
|
13
|
+
*/
|
|
14
|
+
emit( eventName: string, payload?: object ): void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { messageBus } from '#bus';
|
|
2
|
+
import { Storage } from '#async_storage';
|
|
3
|
+
|
|
4
|
+
export const Event = {
|
|
5
|
+
emit: ( eventName, payload ) => {
|
|
6
|
+
const ctx = Storage.load();
|
|
7
|
+
|
|
8
|
+
messageBus.emit( `external:${eventName}`, {
|
|
9
|
+
...payload ?? {},
|
|
10
|
+
...( ctx && {
|
|
11
|
+
activityInfo: ctx.activityInfo,
|
|
12
|
+
workflowDetails: ctx.workflowDetails,
|
|
13
|
+
outputActivityKind: ctx.outputActivityKind
|
|
14
|
+
} )
|
|
15
|
+
} );
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
@@ -11,12 +11,11 @@ vi.mock( '#bus', () => ( {
|
|
|
11
11
|
messageBus: { emit: emitMock }
|
|
12
12
|
} ) );
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { Event } from './events.js';
|
|
15
15
|
|
|
16
|
-
// `eventId` stamping is the bus layer's responsibility (see bus.spec.js
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
describe( 'emitEvent', () => {
|
|
16
|
+
// `eventId` stamping is the bus layer's responsibility (see bus.spec.js).
|
|
17
|
+
// Assertions here use `objectContaining` so they don't have to know about that enrichment.
|
|
18
|
+
describe( 'Event.emit', () => {
|
|
20
19
|
beforeEach( () => {
|
|
21
20
|
vi.clearAllMocks();
|
|
22
21
|
} );
|
|
@@ -39,7 +38,7 @@ describe( 'emitEvent', () => {
|
|
|
39
38
|
outputActivityKind: 'step'
|
|
40
39
|
} );
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
Event.emit( 'cost:llm:request', { modelId: 'gpt-4o' } );
|
|
43
42
|
|
|
44
43
|
expect( emitMock ).toHaveBeenCalledWith( 'external:cost:llm:request', expect.objectContaining( {
|
|
45
44
|
activityInfo,
|
|
@@ -52,7 +51,7 @@ describe( 'emitEvent', () => {
|
|
|
52
51
|
it( 'emits payload without context when storage is missing', () => {
|
|
53
52
|
loadMock.mockReturnValue( undefined );
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
Event.emit( 'foo:bar', { x: 1 } );
|
|
56
55
|
|
|
57
56
|
expect( emitMock ).toHaveBeenCalledWith( 'external:foo:bar', { x: 1 } );
|
|
58
57
|
} );
|
|
@@ -75,7 +74,7 @@ describe( 'emitEvent', () => {
|
|
|
75
74
|
outputActivityKind: 'step'
|
|
76
75
|
} );
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
Event.emit( 'lifecycle:start' );
|
|
79
78
|
|
|
80
79
|
expect( emitMock ).toHaveBeenCalledWith( 'external:lifecycle:start', expect.objectContaining( {
|
|
81
80
|
activityInfo,
|
|
@@ -102,7 +101,7 @@ describe( 'emitEvent', () => {
|
|
|
102
101
|
outputActivityKind: 'step'
|
|
103
102
|
} );
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
Event.emit( 'cost:http:request', {
|
|
106
105
|
activityInfo: { activityId: 'should-be-overridden' },
|
|
107
106
|
workflowDetails: { workflowId: 'should-be-overridden' },
|
|
108
107
|
outputActivityKind: 'should-be-overridden',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* > [!WARNING]
|
|
3
|
+
* > **Internal use only.** Not part of the public API; may change without notice.
|
|
4
|
+
*
|
|
5
|
+
* These are helpers for other SDK modules integration.
|
|
6
|
+
* These need Temporal activity runtime to work, as they access state, emit events and use node:* modules.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export * from './context.js';
|
|
11
|
+
export * from './events.js';
|
|
12
|
+
export * from './tracing.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Attribute } from '#trace_attribute';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tools to interact with Tracing
|
|
5
|
+
*/
|
|
6
|
+
export declare const Tracing: {
|
|
7
|
+
Attribute: typeof Attribute;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new event.
|
|
11
|
+
*
|
|
12
|
+
* @param args
|
|
13
|
+
* @param args.id - A unique id for the Event.
|
|
14
|
+
* @param args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
|
|
15
|
+
* @param args.name - The human-friendly name of the Event: query, request, create.
|
|
16
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "input" field.
|
|
17
|
+
*/
|
|
18
|
+
addEventStart( args: { id: string; kind: string; name: string; details: unknown } ): void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Concludes an event.
|
|
22
|
+
*
|
|
23
|
+
* @param args
|
|
24
|
+
* @param args.id - The id of the event to conclude.
|
|
25
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "output" field.
|
|
26
|
+
*/
|
|
27
|
+
addEventEnd( args: { id: string; details: unknown } ): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Concludes an event with an error.
|
|
31
|
+
*
|
|
32
|
+
* @param args
|
|
33
|
+
* @param args.id - The id of the event to conclude.
|
|
34
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "error" field.
|
|
35
|
+
*/
|
|
36
|
+
addEventError( args: { id: string; details: unknown } ): void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adds an attribute to an event.
|
|
40
|
+
*
|
|
41
|
+
* @param args
|
|
42
|
+
* @param args.eventId - The id of the event to attach the attribute to.
|
|
43
|
+
* @param args.attribute - The attribute to attach to the event.
|
|
44
|
+
*/
|
|
45
|
+
addEventAttribute( args: { eventId: string; attribute: Attribute.Instance } ): void;
|
|
46
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { addEventActionWithContext as send, EventAction } from '#tracing';
|
|
2
|
+
|
|
3
|
+
import { Attribute } from '#trace_attribute';
|
|
4
|
+
|
|
5
|
+
export const Tracing = {
|
|
6
|
+
Attribute,
|
|
7
|
+
addEventStart: ( { id, kind, name, details } ) => send( EventAction.START, { kind, name, details, id } ),
|
|
8
|
+
addEventEnd: ( { id, details } ) => send( EventAction.END, { id, details } ),
|
|
9
|
+
addEventError: ( { id, details } ) => send( EventAction.ERROR, { id, details } ),
|
|
10
|
+
addEventAttribute: ( { eventId, attribute } ) => send( EventAction.ADD_ATTR, { id: eventId, details: attribute } )
|
|
11
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Storage } from '#async_storage';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { serializeError } from './tools/utils.js';
|
|
4
|
-
import { isStringboolTrue } from '#
|
|
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 { ComponentType } from '#consts';
|
|
@@ -3,7 +3,7 @@ import { WorkflowExecutionAlreadyStartedError, WorkflowIdConflictPolicy } from '
|
|
|
3
3
|
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
4
|
import { catalogId, taskQueue } from '../configs.js';
|
|
5
5
|
import { createChildLogger } from '#logger';
|
|
6
|
-
import { CancellablePromise } from '#
|
|
6
|
+
import { CancellablePromise } from '#helpers/promise';
|
|
7
7
|
|
|
8
8
|
const log = createChildLogger( 'Catalog' );
|
|
9
9
|
|
|
@@ -7,9 +7,6 @@ vi.mock( '#consts', () => ( {
|
|
|
7
7
|
METADATA_ACCESS_SYMBOL
|
|
8
8
|
} ) );
|
|
9
9
|
|
|
10
|
-
const setMetadata = ( target, values ) =>
|
|
11
|
-
Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
|
|
12
|
-
|
|
13
10
|
describe( 'createCatalog', () => {
|
|
14
11
|
it( 'builds catalog with activities grouped by workflow path and returns Catalog with CatalogWorkflow entries', async () => {
|
|
15
12
|
const { createCatalog } = await import( './index.js' );
|
|
@@ -32,40 +29,40 @@ describe( 'createCatalog', () => {
|
|
|
32
29
|
];
|
|
33
30
|
|
|
34
31
|
const activity1 = () => {};
|
|
35
|
-
|
|
32
|
+
activity1[METADATA_ACCESS_SYMBOL] = {
|
|
36
33
|
name: 'A1',
|
|
37
34
|
path: '/flows/flow1#A1',
|
|
38
35
|
description: 'desc-a1',
|
|
39
36
|
inputSchema: z.object( { in: z.literal( 'a1' ) } ),
|
|
40
37
|
outputSchema: z.object( { out: z.literal( 'a1' ) } )
|
|
41
|
-
}
|
|
38
|
+
};
|
|
42
39
|
|
|
43
40
|
const activity2 = () => {};
|
|
44
|
-
|
|
41
|
+
activity2[METADATA_ACCESS_SYMBOL] = {
|
|
45
42
|
name: 'A2',
|
|
46
43
|
path: '/flows/flow1#A2',
|
|
47
44
|
description: 'desc-a2',
|
|
48
45
|
inputSchema: z.object( { in: z.literal( 'a2' ) } ),
|
|
49
46
|
outputSchema: z.object( { out: z.literal( 'a2' ) } )
|
|
50
|
-
}
|
|
47
|
+
};
|
|
51
48
|
|
|
52
49
|
const activity3 = () => {};
|
|
53
|
-
|
|
50
|
+
activity3[METADATA_ACCESS_SYMBOL] = {
|
|
54
51
|
name: 'B1',
|
|
55
52
|
path: '/flows/flow2#B1',
|
|
56
53
|
description: 'desc-b1',
|
|
57
54
|
inputSchema: z.object( { in: z.literal( 'b1' ) } ),
|
|
58
55
|
outputSchema: z.object( { out: z.literal( 'b1' ) } )
|
|
59
|
-
}
|
|
56
|
+
};
|
|
60
57
|
|
|
61
58
|
const activity4 = () => {};
|
|
62
|
-
|
|
59
|
+
activity4[METADATA_ACCESS_SYMBOL] = {
|
|
63
60
|
name: 'X',
|
|
64
61
|
path: '/other#X',
|
|
65
62
|
description: 'desc-x',
|
|
66
63
|
inputSchema: z.object( { in: z.literal( 'x' ) } ),
|
|
67
64
|
outputSchema: z.object( { out: z.literal( 'x' ) } )
|
|
68
|
-
}
|
|
65
|
+
};
|
|
69
66
|
|
|
70
67
|
const activities = {
|
|
71
68
|
'/flows/flow1#A1': activity1,
|
package/src/worker/configs.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
|
-
import { isStringboolTrue } from '#
|
|
2
|
+
import { isStringboolTrue } from '#helpers/string';
|
|
3
3
|
|
|
4
4
|
class InvalidEnvVarsErrors extends Error { }
|
|
5
5
|
|
|
6
6
|
const coalesceEmptyString = v => v === '' ? undefined : v;
|
|
7
7
|
|
|
8
|
+
const durationSchema = z.preprocess(
|
|
9
|
+
coalesceEmptyString,
|
|
10
|
+
z.string()
|
|
11
|
+
.regex( /^\d+$|^\d+(\.\d+)?\s?(ms|s|m|h|d)$/i )
|
|
12
|
+
.optional()
|
|
13
|
+
);
|
|
14
|
+
|
|
8
15
|
const envVarSchema = z.object( {
|
|
9
16
|
OUTPUT_CATALOG_ID: z.string().regex( /^[a-z0-9_.@-]+$/i ),
|
|
10
17
|
OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().nonnegative().default( 0 ) ),
|
|
@@ -28,6 +35,10 @@ const envVarSchema = z.object( {
|
|
|
28
35
|
OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: z.transform( v => v === undefined ? true : isStringboolTrue( v ) ),
|
|
29
36
|
// Time to allow for hooks to flush before shutdown
|
|
30
37
|
OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 3000 ) ),
|
|
38
|
+
// Set temporal worker shutdown force time
|
|
39
|
+
TEMPORAL_SHUTDOWN_FORCE_TIME: durationSchema,
|
|
40
|
+
// Set temporal worker shutdown grace time
|
|
41
|
+
TEMPORAL_SHUTDOWN_GRACE_TIME: durationSchema,
|
|
31
42
|
// HTTP CONNECT proxy for Temporal gRPC connections (e.g. "proxy-host:8080").
|
|
32
43
|
// Must be a bare host:port — no scheme (Temporal's native HTTP CONNECT
|
|
33
44
|
// option is not a URL).
|
|
@@ -56,4 +67,6 @@ export const workerTelemetryIntervalMs = envVars.OUTPUT_WORKER_TELEMETRY_INTERVA
|
|
|
56
67
|
export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
|
|
57
68
|
export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
|
|
58
69
|
export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
|
|
70
|
+
export const shutdownForceTime = envVars.TEMPORAL_SHUTDOWN_FORCE_TIME;
|
|
71
|
+
export const shutdownGraceTime = envVars.TEMPORAL_SHUTDOWN_GRACE_TIME;
|
|
59
72
|
export const grpcProxy = envVars.TEMPORAL_GRPC_PROXY;
|
|
@@ -12,7 +12,9 @@ const CONFIG_KEYS = [
|
|
|
12
12
|
'TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS',
|
|
13
13
|
'OUTPUT_WORKER_TELEMETRY_INTERVAL_MS',
|
|
14
14
|
'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS',
|
|
15
|
-
'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED'
|
|
15
|
+
'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED',
|
|
16
|
+
'TEMPORAL_SHUTDOWN_FORCE_TIME',
|
|
17
|
+
'TEMPORAL_SHUTDOWN_GRACE_TIME'
|
|
16
18
|
];
|
|
17
19
|
|
|
18
20
|
const setEnv = ( overrides = {} ) => {
|
|
@@ -65,6 +67,8 @@ describe( 'worker/configs', () => {
|
|
|
65
67
|
expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
|
|
66
68
|
expect( configs.activityHeartbeatIntervalMs ).toBe( 2 * 60 * 1000 );
|
|
67
69
|
expect( configs.activityHeartbeatEnabled ).toBe( true );
|
|
70
|
+
expect( configs.shutdownForceTime ).toBeUndefined();
|
|
71
|
+
expect( configs.shutdownGraceTime ).toBeUndefined();
|
|
68
72
|
expect( configs.taskQueue ).toBe( 'test-catalog' );
|
|
69
73
|
expect( configs.catalogId ).toBe( 'test-catalog' );
|
|
70
74
|
} );
|
|
@@ -100,6 +104,35 @@ describe( 'worker/configs', () => {
|
|
|
100
104
|
expect( configs.activityHeartbeatIntervalMs ).toBe( 60000 );
|
|
101
105
|
} );
|
|
102
106
|
|
|
107
|
+
it( 'parses Temporal shutdown durations', async () => {
|
|
108
|
+
setEnv( {
|
|
109
|
+
TEMPORAL_SHUTDOWN_FORCE_TIME: '15000',
|
|
110
|
+
TEMPORAL_SHUTDOWN_GRACE_TIME: '15s'
|
|
111
|
+
} );
|
|
112
|
+
const configs = await loadConfigs();
|
|
113
|
+
|
|
114
|
+
expect( configs.shutdownForceTime ).toBe( '15000' );
|
|
115
|
+
expect( configs.shutdownGraceTime ).toBe( '15s' );
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'treats empty Temporal shutdown durations as unset', async () => {
|
|
119
|
+
setEnv( {
|
|
120
|
+
TEMPORAL_SHUTDOWN_FORCE_TIME: '',
|
|
121
|
+
TEMPORAL_SHUTDOWN_GRACE_TIME: ''
|
|
122
|
+
} );
|
|
123
|
+
const configs = await loadConfigs();
|
|
124
|
+
|
|
125
|
+
expect( configs.shutdownForceTime ).toBeUndefined();
|
|
126
|
+
expect( configs.shutdownGraceTime ).toBeUndefined();
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
it( 'throws when Temporal shutdown durations are invalid', async () => {
|
|
130
|
+
setEnv( { TEMPORAL_SHUTDOWN_FORCE_TIME: 'soon' } );
|
|
131
|
+
vi.resetModules();
|
|
132
|
+
|
|
133
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
134
|
+
} );
|
|
135
|
+
|
|
103
136
|
it( 'allows zero for worker telemetry interval', async () => {
|
|
104
137
|
setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '0' } );
|
|
105
138
|
const configs = await loadConfigs();
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { createChildLogger } from '#logger';
|
|
2
2
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
-
import { CancellablePromise } from '#
|
|
4
|
-
|
|
5
|
-
const ServingStatus = {
|
|
6
|
-
UNKNOWN: 0,
|
|
7
|
-
SERVING: 1,
|
|
8
|
-
NOT_SERVING: 2,
|
|
9
|
-
SERVICE_UNKNOWN: 3
|
|
10
|
-
};
|
|
3
|
+
import { CancellablePromise } from '#helpers/promise';
|
|
11
4
|
|
|
12
5
|
const log = createChildLogger( 'Connection' );
|
|
13
6
|
|
|
@@ -28,24 +21,20 @@ export class TemporalConnectionMonitor {
|
|
|
28
21
|
throw new Error( 'Connection health check timed out' );
|
|
29
22
|
} );
|
|
30
23
|
|
|
31
|
-
#healthcheck = async () => this.#connection.
|
|
24
|
+
#healthcheck = async () => this.#connection.workflowService.getSystemInfo( {} );
|
|
32
25
|
|
|
33
26
|
#sleep = async () => delay( this.#CHECK_INTERVAL_MS, 0, { ref: false } );
|
|
34
27
|
|
|
35
28
|
#watch = async () => {
|
|
36
29
|
while ( !this.#cancellation.completed ) {
|
|
37
30
|
try {
|
|
38
|
-
|
|
31
|
+
await Promise.race( [ this.#healthcheck(), this.#getTimeout(), this.#cancellation.promise ] );
|
|
39
32
|
|
|
40
33
|
// cancellation won the race
|
|
41
34
|
if ( this.#cancellation.completed ) {
|
|
42
35
|
break;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
if ( health?.status !== ServingStatus.SERVING ) {
|
|
46
|
-
throw new Error( `Connection not serving (status ${health?.status})` );
|
|
47
|
-
}
|
|
48
|
-
|
|
49
38
|
log.info( this.#failures === 0 ? 'Healthy' : 'Recovered' );
|
|
50
39
|
this.#failures = 0;
|
|
51
40
|
} catch ( error ) {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { TemporalConnectionMonitor } from './connection_monitor.js';
|
|
3
3
|
|
|
4
|
-
const SERVING = 1;
|
|
5
|
-
const NOT_SERVING = 2;
|
|
6
4
|
const CHECK_TIMEOUT_MS = 50;
|
|
7
5
|
const CHECK_INTERVAL_MS = 100;
|
|
8
6
|
|
|
@@ -26,7 +24,7 @@ vi.mock( 'node:timers/promises', () => ( { setTimeout: delayMock } ) );
|
|
|
26
24
|
vi.mock( '#logger', () => ( { createChildLogger: vi.fn( () => mockLogger ) } ) );
|
|
27
25
|
|
|
28
26
|
const createConnection = check => ( {
|
|
29
|
-
|
|
27
|
+
workflowService: { getSystemInfo: check }
|
|
30
28
|
} );
|
|
31
29
|
|
|
32
30
|
const createMonitor = ( check, overrides = {} ) => new TemporalConnectionMonitor( createConnection( check ), {
|
|
@@ -52,8 +50,8 @@ describe( 'TemporalConnectionMonitor', () => {
|
|
|
52
50
|
scheduledDelays.length = 0;
|
|
53
51
|
} );
|
|
54
52
|
|
|
55
|
-
it( 'logs healthy when the
|
|
56
|
-
const check = vi.fn().mockResolvedValue( {
|
|
53
|
+
it( 'logs healthy when the workflow service is reachable', async () => {
|
|
54
|
+
const check = vi.fn().mockResolvedValue( {} );
|
|
57
55
|
const monitor = createMonitor( check );
|
|
58
56
|
|
|
59
57
|
const run = monitor.start();
|
|
@@ -90,7 +88,7 @@ describe( 'TemporalConnectionMonitor', () => {
|
|
|
90
88
|
it( 'logs recovered after a transient failure succeeds', async () => {
|
|
91
89
|
const check = vi.fn()
|
|
92
90
|
.mockRejectedValueOnce( new Error( 'temporary outage' ) )
|
|
93
|
-
.mockResolvedValueOnce( {
|
|
91
|
+
.mockResolvedValueOnce( {} );
|
|
94
92
|
const monitor = createMonitor( check );
|
|
95
93
|
|
|
96
94
|
monitor.start();
|
|
@@ -134,21 +132,6 @@ describe( 'TemporalConnectionMonitor', () => {
|
|
|
134
132
|
expect( monitor.running ).toBe( false );
|
|
135
133
|
} );
|
|
136
134
|
|
|
137
|
-
it( 'treats non-serving health status as a failure', async () => {
|
|
138
|
-
const check = vi.fn().mockResolvedValue( { status: NOT_SERVING } );
|
|
139
|
-
const monitor = createMonitor( check );
|
|
140
|
-
|
|
141
|
-
monitor.start();
|
|
142
|
-
await flushPromises();
|
|
143
|
-
|
|
144
|
-
expect( mockLogger.warn ).toHaveBeenCalledWith( 'Connection unhealthy', {
|
|
145
|
-
error: `Connection not serving (status ${NOT_SERVING})`,
|
|
146
|
-
failures: 1
|
|
147
|
-
} );
|
|
148
|
-
|
|
149
|
-
await monitor.stop();
|
|
150
|
-
} );
|
|
151
|
-
|
|
152
135
|
it( 'returns the same lifecycle promise when started more than once', async () => {
|
|
153
136
|
const check = vi.fn().mockReturnValue( new Promise( () => {} ) );
|
|
154
137
|
const monitor = createMonitor( check );
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { messageBus } from '#bus';
|
|
2
|
+
import { ACTIVITY_LOGGER_SYMBOL, BusEventType } from '#consts';
|
|
3
|
+
import { activityInfo as activityInfoFn } from '@temporalio/activity';
|
|
4
|
+
import { assignImmutableProperty } from '#helpers/object';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sets global functions on globalThis
|
|
8
|
+
*/
|
|
9
|
+
export const bindGlobalFunctions = () => {
|
|
10
|
+
/** Defines the activity logger function, accessible in activity context via logger interface */
|
|
11
|
+
assignImmutableProperty( globalThis, ACTIVITY_LOGGER_SYMBOL, ( { level, message, metadata } ) =>
|
|
12
|
+
messageBus.emit( BusEventType.ACTIVITY_LOG, { level, message, metadata, activityInfo: activityInfoFn() } )
|
|
13
|
+
);
|
|
14
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ACTIVITY_LOGGER_SYMBOL,
|
|
5
|
+
BusEventType
|
|
6
|
+
} = vi.hoisted( () => ( {
|
|
7
|
+
ACTIVITY_LOGGER_SYMBOL: Symbol( 'activity_logger' ),
|
|
8
|
+
BusEventType: {
|
|
9
|
+
ACTIVITY_LOG: 'activity:log'
|
|
10
|
+
}
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
const messageBusMock = vi.hoisted( () => ( {
|
|
14
|
+
emit: vi.fn()
|
|
15
|
+
} ) );
|
|
16
|
+
const activityInfoMock = vi.hoisted( () => vi.fn( () => ( {
|
|
17
|
+
activityId: 'act-1',
|
|
18
|
+
activityType: 'myStep'
|
|
19
|
+
} ) ) );
|
|
20
|
+
|
|
21
|
+
vi.mock( '#consts', () => ( { ACTIVITY_LOGGER_SYMBOL, BusEventType } ) );
|
|
22
|
+
vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
|
|
23
|
+
vi.mock( '@temporalio/activity', () => ( {
|
|
24
|
+
activityInfo: activityInfoMock
|
|
25
|
+
} ) );
|
|
26
|
+
|
|
27
|
+
describe( 'worker/global_functions', () => {
|
|
28
|
+
beforeEach( () => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
delete globalThis[ACTIVITY_LOGGER_SYMBOL];
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
it( 'binds activity logger global function that emits activity log events with metadata', async () => {
|
|
34
|
+
const { bindGlobalFunctions } = await import( './global_functions.js' );
|
|
35
|
+
const metadata = { requestId: 'req-1' };
|
|
36
|
+
|
|
37
|
+
bindGlobalFunctions();
|
|
38
|
+
globalThis[ACTIVITY_LOGGER_SYMBOL]( {
|
|
39
|
+
level: 'debug',
|
|
40
|
+
message: 'activity detail',
|
|
41
|
+
metadata
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
expect( activityInfoMock ).toHaveBeenCalledTimes( 1 );
|
|
45
|
+
expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.ACTIVITY_LOG, {
|
|
46
|
+
level: 'debug',
|
|
47
|
+
message: 'activity detail',
|
|
48
|
+
metadata,
|
|
49
|
+
activityInfo: {
|
|
50
|
+
activityId: 'act-1',
|
|
51
|
+
activityType: 'myStep'
|
|
52
|
+
}
|
|
53
|
+
} );
|
|
54
|
+
} );
|
|
55
|
+
} );
|
package/src/worker/index.js
CHANGED
|
@@ -17,7 +17,8 @@ import { messageBus } from '#bus';
|
|
|
17
17
|
import { BusEventType } from '#consts';
|
|
18
18
|
import { setupTelemetry } from './telemetry.js';
|
|
19
19
|
import { TemporalConnectionMonitor } from './connection_monitor.js';
|
|
20
|
-
import {
|
|
20
|
+
import { bindGlobalFunctions } from './global_functions.js';
|
|
21
|
+
import { runOnce } from '#helpers/function';
|
|
21
22
|
|
|
22
23
|
import './log_hooks.js';
|
|
23
24
|
|
|
@@ -33,7 +34,9 @@ const {
|
|
|
33
34
|
maxConcurrentActivityTaskExecutions,
|
|
34
35
|
maxCachedWorkflows,
|
|
35
36
|
maxConcurrentActivityTaskPolls,
|
|
36
|
-
maxConcurrentWorkflowTaskPolls
|
|
37
|
+
maxConcurrentWorkflowTaskPolls,
|
|
38
|
+
shutdownForceTime,
|
|
39
|
+
shutdownGraceTime
|
|
37
40
|
} = configs;
|
|
38
41
|
|
|
39
42
|
const state = {
|
|
@@ -73,6 +76,9 @@ const execute = async () => {
|
|
|
73
76
|
if ( proxy ) {
|
|
74
77
|
log.info( 'Using gRPC proxy', { targetHost: grpcProxy } );
|
|
75
78
|
}
|
|
79
|
+
|
|
80
|
+
bindGlobalFunctions();
|
|
81
|
+
|
|
76
82
|
state.connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
|
|
77
83
|
|
|
78
84
|
log.info( 'Creating connection monitor...' );
|
|
@@ -95,7 +101,9 @@ const execute = async () => {
|
|
|
95
101
|
maxCachedWorkflows,
|
|
96
102
|
maxConcurrentActivityTaskPolls,
|
|
97
103
|
maxConcurrentWorkflowTaskPolls,
|
|
98
|
-
bundlerOptions: { webpackConfigHook }
|
|
104
|
+
bundlerOptions: { webpackConfigHook },
|
|
105
|
+
...( shutdownForceTime !== undefined && { shutdownForceTime } ),
|
|
106
|
+
...( shutdownGraceTime !== undefined && { shutdownGraceTime } )
|
|
99
107
|
} );
|
|
100
108
|
|
|
101
109
|
log.info( 'Setting up telemetry...' );
|