@outputai/core 0.9.3-next.c318502.0 → 0.10.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
CHANGED
package/src/worker/configs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import { isStringboolTrue } from '#helpers/string';
|
|
3
|
+
import { workerTunerEnvSchema } from './configs_tuner_schema.js';
|
|
3
4
|
|
|
4
5
|
class InvalidEnvVarsErrors extends Error { }
|
|
5
6
|
|
|
@@ -28,6 +29,8 @@ const envVarSchema = z.object( {
|
|
|
28
29
|
// How aggressively the worker pulls tasks from Temporal.
|
|
29
30
|
TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 5 ) ),
|
|
30
31
|
TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 5 ) ),
|
|
32
|
+
// JSON-encoded Temporal Worker tuner options.
|
|
33
|
+
TEMPORAL_WORKER_TUNER: workerTunerEnvSchema,
|
|
31
34
|
// Activity configs
|
|
32
35
|
// How often the worker sends a heartbeat to the Temporal Service during activity execution
|
|
33
36
|
OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 2 * 60 * 1000 ) ), // 2min
|
|
@@ -60,6 +63,7 @@ export const maxConcurrentWorkflowTaskExecutions = envVars.TEMPORAL_MAX_CONCURRE
|
|
|
60
63
|
export const maxCachedWorkflows = envVars.TEMPORAL_MAX_CACHED_WORKFLOWS;
|
|
61
64
|
export const maxConcurrentActivityTaskPolls = envVars.TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS;
|
|
62
65
|
export const maxConcurrentWorkflowTaskPolls = envVars.TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS;
|
|
66
|
+
export const workerTuner = envVars.TEMPORAL_WORKER_TUNER;
|
|
63
67
|
export const namespace = envVars.TEMPORAL_NAMESPACE;
|
|
64
68
|
export const taskQueue = envVars.OUTPUT_CATALOG_ID;
|
|
65
69
|
export const catalogId = envVars.OUTPUT_CATALOG_ID;
|
|
@@ -10,6 +10,7 @@ const CONFIG_KEYS = [
|
|
|
10
10
|
'TEMPORAL_MAX_CACHED_WORKFLOWS',
|
|
11
11
|
'TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS',
|
|
12
12
|
'TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS',
|
|
13
|
+
'TEMPORAL_WORKER_TUNER',
|
|
13
14
|
'OUTPUT_WORKER_TELEMETRY_INTERVAL_MS',
|
|
14
15
|
'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS',
|
|
15
16
|
'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED',
|
|
@@ -64,6 +65,7 @@ describe( 'worker/configs', () => {
|
|
|
64
65
|
expect( configs.maxCachedWorkflows ).toBe( 1000 );
|
|
65
66
|
expect( configs.maxConcurrentActivityTaskPolls ).toBe( 5 );
|
|
66
67
|
expect( configs.maxConcurrentWorkflowTaskPolls ).toBe( 5 );
|
|
68
|
+
expect( configs.workerTuner ).toBeUndefined();
|
|
67
69
|
expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
|
|
68
70
|
expect( configs.activityHeartbeatIntervalMs ).toBe( 2 * 60 * 1000 );
|
|
69
71
|
expect( configs.activityHeartbeatEnabled ).toBe( true );
|
|
@@ -87,6 +89,25 @@ describe( 'worker/configs', () => {
|
|
|
87
89
|
expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
|
|
88
90
|
} );
|
|
89
91
|
|
|
92
|
+
it( 'parses Temporal worker tuner JSON', async () => {
|
|
93
|
+
const workerTuner = {
|
|
94
|
+
tunerOptions: {
|
|
95
|
+
targetMemoryUsage: 0.8,
|
|
96
|
+
targetCpuUsage: 0.9
|
|
97
|
+
},
|
|
98
|
+
activityTaskSlotOptions: {
|
|
99
|
+
minimumSlots: 1,
|
|
100
|
+
maximumSlots: 100,
|
|
101
|
+
rampThrottle: '50ms'
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
setEnv( { TEMPORAL_WORKER_TUNER: JSON.stringify( workerTuner ) } );
|
|
106
|
+
const configs = await loadConfigs();
|
|
107
|
+
|
|
108
|
+
expect( configs.workerTuner ).toEqual( workerTuner );
|
|
109
|
+
} );
|
|
110
|
+
|
|
90
111
|
it( 'parses custom numeric env vars', async () => {
|
|
91
112
|
setEnv( {
|
|
92
113
|
TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: '10',
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
const coalesceEmptyString = v => v === '' ? undefined : v;
|
|
4
|
+
|
|
5
|
+
const durationSchema = z.preprocess(
|
|
6
|
+
coalesceEmptyString,
|
|
7
|
+
z.string()
|
|
8
|
+
.regex( /^\d+$|^\d+(\.\d+)?\s?(ms|s|m|h|d)$/i )
|
|
9
|
+
.optional()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const resourceBasedTunerOptionsSchema = z.strictObject( {
|
|
13
|
+
targetMemoryUsage: z.number().min( 0 ).max( 1 ),
|
|
14
|
+
targetCpuUsage: z.number().min( 0 ).max( 1 )
|
|
15
|
+
} );
|
|
16
|
+
|
|
17
|
+
const resourceBasedSlotOptionsSchema = z.strictObject( {
|
|
18
|
+
minimumSlots: z.number().int().positive().optional(),
|
|
19
|
+
maximumSlots: z.number().int().positive().optional(),
|
|
20
|
+
rampThrottle: durationSchema
|
|
21
|
+
} ).superRefine( ( value, ctx ) => {
|
|
22
|
+
if ( value.minimumSlots !== undefined && value.maximumSlots !== undefined && value.minimumSlots > value.maximumSlots ) {
|
|
23
|
+
ctx.addIssue( {
|
|
24
|
+
code: 'custom',
|
|
25
|
+
message: 'minimumSlots must be less than or equal to maximumSlots'
|
|
26
|
+
} );
|
|
27
|
+
}
|
|
28
|
+
} );
|
|
29
|
+
|
|
30
|
+
const resourceBasedTunerSchema = z.strictObject( {
|
|
31
|
+
tunerOptions: resourceBasedTunerOptionsSchema,
|
|
32
|
+
workflowTaskSlotOptions: resourceBasedSlotOptionsSchema.optional(),
|
|
33
|
+
activityTaskSlotOptions: resourceBasedSlotOptionsSchema.optional(),
|
|
34
|
+
localActivityTaskSlotOptions: resourceBasedSlotOptionsSchema.optional(),
|
|
35
|
+
nexusTaskSlotOptions: resourceBasedSlotOptionsSchema.optional()
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
const fixedSizeSlotSupplierSchema = z.strictObject( {
|
|
39
|
+
type: z.literal( 'fixed-size' ),
|
|
40
|
+
numSlots: z.number().int().positive()
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
const resourceBasedSlotSupplierSchema = resourceBasedSlotOptionsSchema.extend( {
|
|
44
|
+
type: z.literal( 'resource-based' ),
|
|
45
|
+
tunerOptions: resourceBasedTunerOptionsSchema
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
const slotSupplierSchema = z.union( [
|
|
49
|
+
fixedSizeSlotSupplierSchema,
|
|
50
|
+
resourceBasedSlotSupplierSchema
|
|
51
|
+
] );
|
|
52
|
+
|
|
53
|
+
const tunerHolderSchema = z.strictObject( {
|
|
54
|
+
workflowTaskSlotSupplier: slotSupplierSchema,
|
|
55
|
+
activityTaskSlotSupplier: slotSupplierSchema,
|
|
56
|
+
localActivityTaskSlotSupplier: slotSupplierSchema,
|
|
57
|
+
nexusTaskSlotSupplier: slotSupplierSchema
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
const workerTunerSchema = z.union( [
|
|
61
|
+
resourceBasedTunerSchema,
|
|
62
|
+
tunerHolderSchema
|
|
63
|
+
] );
|
|
64
|
+
|
|
65
|
+
export const workerTunerEnvSchema = z.preprocess(
|
|
66
|
+
coalesceEmptyString,
|
|
67
|
+
/* eslint-disable consistent-return */
|
|
68
|
+
z.string().optional().transform( ( value, ctx ) => {
|
|
69
|
+
if ( value === undefined ) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse( value );
|
|
75
|
+
} catch ( error ) {
|
|
76
|
+
ctx.addIssue( { code: 'custom', message: `Expected valid JSON: ${error.message}` } );
|
|
77
|
+
return z.NEVER;
|
|
78
|
+
}
|
|
79
|
+
} ).pipe( workerTunerSchema.optional() )
|
|
80
|
+
/* eslint-enable consistent-return */
|
|
81
|
+
);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { workerTunerEnvSchema } from './configs_tuner_schema.js';
|
|
3
|
+
|
|
4
|
+
const parseWorkerTuner = value => workerTunerEnvSchema.parse( value );
|
|
5
|
+
|
|
6
|
+
describe( 'worker/configs_tuner_schema', () => {
|
|
7
|
+
it( 'treats empty string as unset', () => {
|
|
8
|
+
expect( parseWorkerTuner( '' ) ).toBeUndefined();
|
|
9
|
+
} );
|
|
10
|
+
|
|
11
|
+
it( 'parses resource-based Temporal worker tuner JSON', () => {
|
|
12
|
+
const workerTuner = {
|
|
13
|
+
tunerOptions: {
|
|
14
|
+
targetMemoryUsage: 0.8,
|
|
15
|
+
targetCpuUsage: 0.9
|
|
16
|
+
},
|
|
17
|
+
activityTaskSlotOptions: {
|
|
18
|
+
minimumSlots: 1,
|
|
19
|
+
maximumSlots: 100,
|
|
20
|
+
rampThrottle: '50ms'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect( parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toEqual( workerTuner );
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'parses per-task Temporal worker tuner JSON', () => {
|
|
28
|
+
const resourceBasedSupplier = {
|
|
29
|
+
type: 'resource-based',
|
|
30
|
+
tunerOptions: {
|
|
31
|
+
targetMemoryUsage: 0.8,
|
|
32
|
+
targetCpuUsage: 0.9
|
|
33
|
+
},
|
|
34
|
+
minimumSlots: 1,
|
|
35
|
+
maximumSlots: 100,
|
|
36
|
+
rampThrottle: '50ms'
|
|
37
|
+
};
|
|
38
|
+
const workerTuner = {
|
|
39
|
+
workflowTaskSlotSupplier: {
|
|
40
|
+
type: 'fixed-size',
|
|
41
|
+
numSlots: 10
|
|
42
|
+
},
|
|
43
|
+
activityTaskSlotSupplier: resourceBasedSupplier,
|
|
44
|
+
localActivityTaskSlotSupplier: {
|
|
45
|
+
type: 'fixed-size',
|
|
46
|
+
numSlots: 10
|
|
47
|
+
},
|
|
48
|
+
nexusTaskSlotSupplier: {
|
|
49
|
+
type: 'fixed-size',
|
|
50
|
+
numSlots: 10
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
expect( parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toEqual( workerTuner );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
it( 'throws when tuner is not valid JSON', () => {
|
|
58
|
+
expect( () => parseWorkerTuner( '{invalid' ) ).toThrow();
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'throws when tuner is not a JSON object', () => {
|
|
62
|
+
expect( () => parseWorkerTuner( '[]' ) ).toThrow();
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
it( 'throws when tuner is missing required per-task suppliers', () => {
|
|
66
|
+
const workerTuner = {
|
|
67
|
+
workflowTaskSlotSupplier: {
|
|
68
|
+
type: 'fixed-size',
|
|
69
|
+
numSlots: 10
|
|
70
|
+
},
|
|
71
|
+
activityTaskSlotSupplier: {
|
|
72
|
+
type: 'fixed-size',
|
|
73
|
+
numSlots: 10
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
expect( () => parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toThrow();
|
|
78
|
+
} );
|
|
79
|
+
|
|
80
|
+
it( 'throws when tuner uses custom suppliers', () => {
|
|
81
|
+
const workerTuner = {
|
|
82
|
+
workflowTaskSlotSupplier: {
|
|
83
|
+
type: 'custom'
|
|
84
|
+
},
|
|
85
|
+
activityTaskSlotSupplier: {
|
|
86
|
+
type: 'fixed-size',
|
|
87
|
+
numSlots: 10
|
|
88
|
+
},
|
|
89
|
+
localActivityTaskSlotSupplier: {
|
|
90
|
+
type: 'fixed-size',
|
|
91
|
+
numSlots: 10
|
|
92
|
+
},
|
|
93
|
+
nexusTaskSlotSupplier: {
|
|
94
|
+
type: 'fixed-size',
|
|
95
|
+
numSlots: 10
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
expect( () => parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toThrow();
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
it( 'throws when target usage is outside range', () => {
|
|
103
|
+
const workerTuner = {
|
|
104
|
+
tunerOptions: {
|
|
105
|
+
targetMemoryUsage: 1.5,
|
|
106
|
+
targetCpuUsage: 0.9
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
expect( () => parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toThrow();
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
it( 'throws when minimum slots exceeds maximum slots', () => {
|
|
114
|
+
const workerTuner = {
|
|
115
|
+
tunerOptions: {
|
|
116
|
+
targetMemoryUsage: 0.8,
|
|
117
|
+
targetCpuUsage: 0.9
|
|
118
|
+
},
|
|
119
|
+
activityTaskSlotOptions: {
|
|
120
|
+
minimumSlots: 10,
|
|
121
|
+
maximumSlots: 5
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
expect( () => parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toThrow();
|
|
126
|
+
} );
|
|
127
|
+
} );
|
package/src/worker/index.js
CHANGED
|
@@ -36,7 +36,8 @@ const {
|
|
|
36
36
|
maxConcurrentActivityTaskPolls,
|
|
37
37
|
maxConcurrentWorkflowTaskPolls,
|
|
38
38
|
shutdownForceTime,
|
|
39
|
-
shutdownGraceTime
|
|
39
|
+
shutdownGraceTime,
|
|
40
|
+
workerTuner
|
|
40
41
|
} = configs;
|
|
41
42
|
|
|
42
43
|
const state = {
|
|
@@ -88,6 +89,9 @@ const execute = async () => {
|
|
|
88
89
|
state.catalogJob = new CatalogJob( { connection: state.connection, namespace, catalog, catalogHash } );
|
|
89
90
|
|
|
90
91
|
log.info( 'Creating worker...' );
|
|
92
|
+
if ( workerTuner ) {
|
|
93
|
+
log.info( 'Using worker tuner options', { ...workerTuner } );
|
|
94
|
+
}
|
|
91
95
|
const worker = await Worker.create( {
|
|
92
96
|
connection: state.connection,
|
|
93
97
|
namespace,
|
|
@@ -96,8 +100,13 @@ const execute = async () => {
|
|
|
96
100
|
activities,
|
|
97
101
|
sinks,
|
|
98
102
|
interceptors: initInterceptors( { activities, workflows } ),
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
// tuner isn't compatible with concurrent task executions configs
|
|
104
|
+
...( workerTuner ? {
|
|
105
|
+
tuner: workerTuner
|
|
106
|
+
} : {
|
|
107
|
+
maxConcurrentWorkflowTaskExecutions,
|
|
108
|
+
maxConcurrentActivityTaskExecutions
|
|
109
|
+
} ),
|
|
101
110
|
maxCachedWorkflows,
|
|
102
111
|
maxConcurrentActivityTaskPolls,
|
|
103
112
|
maxConcurrentWorkflowTaskPolls,
|
package/src/worker/index.spec.js
CHANGED
|
@@ -45,6 +45,7 @@ const {
|
|
|
45
45
|
maxCachedWorkflows: 1000,
|
|
46
46
|
maxConcurrentActivityTaskPolls: 5,
|
|
47
47
|
maxConcurrentWorkflowTaskPolls: 5,
|
|
48
|
+
workerTuner: undefined,
|
|
48
49
|
processFailureShutdownDelay: 0,
|
|
49
50
|
shutdownForceTime: undefined,
|
|
50
51
|
shutdownGraceTime: undefined
|
|
@@ -185,6 +186,7 @@ describe( 'worker/index', () => {
|
|
|
185
186
|
resetPromises();
|
|
186
187
|
configValues.apiKey = undefined;
|
|
187
188
|
configValues.grpcProxy = undefined;
|
|
189
|
+
configValues.workerTuner = undefined;
|
|
188
190
|
configValues.shutdownForceTime = undefined;
|
|
189
191
|
configValues.shutdownGraceTime = undefined;
|
|
190
192
|
catalogJobInstance.error = null;
|
|
@@ -263,6 +265,32 @@ describe( 'worker/index', () => {
|
|
|
263
265
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
264
266
|
} );
|
|
265
267
|
|
|
268
|
+
it( 'passes worker tuner instead of incompatible execution concurrency options', async () => {
|
|
269
|
+
configValues.workerTuner = {
|
|
270
|
+
tunerOptions: {
|
|
271
|
+
targetMemoryUsage: 0.8,
|
|
272
|
+
targetCpuUsage: 0.9
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const { Worker } = await import( '@temporalio/worker' );
|
|
276
|
+
|
|
277
|
+
await importWorker();
|
|
278
|
+
|
|
279
|
+
await vi.waitFor( () => expect( Worker.create ).toHaveBeenCalled() );
|
|
280
|
+
|
|
281
|
+
const workerOptions = Worker.create.mock.calls[0][0];
|
|
282
|
+
expect( workerOptions ).toEqual( expect.objectContaining( {
|
|
283
|
+
tuner: configValues.workerTuner,
|
|
284
|
+
maxCachedWorkflows: configValues.maxCachedWorkflows,
|
|
285
|
+
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
286
|
+
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
287
|
+
} ) );
|
|
288
|
+
expect( workerOptions ).not.toHaveProperty( 'maxConcurrentWorkflowTaskExecutions' );
|
|
289
|
+
expect( workerOptions ).not.toHaveProperty( 'maxConcurrentActivityTaskExecutions' );
|
|
290
|
+
|
|
291
|
+
await settleWorker();
|
|
292
|
+
} );
|
|
293
|
+
|
|
266
294
|
it( 'enables TLS when apiKey is set', async () => {
|
|
267
295
|
configValues.apiKey = 'secret';
|
|
268
296
|
const { NativeConnection } = await import( '@temporalio/worker' );
|