@outputai/core 0.9.3-next.c318502.0 → 0.10.1-next.37650c5.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.9.3-next.c318502.0",
3
+ "version": "0.10.1-next.37650c5.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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
+ } );
@@ -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
- maxConcurrentWorkflowTaskExecutions,
100
- maxConcurrentActivityTaskExecutions,
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,
@@ -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' );