@outputai/core 0.15.1-next.e254388.0 → 0.15.1-next.fd6bb62.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 +1 -1
- package/src/worker/configs.js +23 -4
- package/src/worker/index.js +16 -47
- package/src/worker/telemetry.js +122 -2
- package/src/worker/worker_options.js +43 -0
package/package.json
CHANGED
package/src/worker/configs.js
CHANGED
|
@@ -22,13 +22,31 @@ const envVarSchema = z.object( {
|
|
|
22
22
|
// Worker concurrency — tune these via env vars to adjust for your workload.
|
|
23
23
|
// Each step (API, LLM, etc.) call is one activity. Lower this to reduce memory pressure.
|
|
24
24
|
TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 40 ) ),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/*
|
|
26
|
+
Workflow tasks that may run at once. These are short state transitions, so this is rarely the
|
|
27
|
+
throughput limit - activity concurrency above is. Left unset so @temporalio/worker applies its
|
|
28
|
+
own default of 40; the startup 'Worker sizing' log reports it as workflowTaskSlots.
|
|
29
|
+
https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#maxconcurrentworkflowtaskexecutions
|
|
30
|
+
*/
|
|
31
|
+
TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_EXECUTIONS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().optional() ),
|
|
32
|
+
/*
|
|
33
|
+
LRU cache for sticky workflow execution, holding each workflow's state until it completes or is
|
|
34
|
+
evicted. Left unset so @temporalio/worker sizes it from the isolate's heap limit, which a flat
|
|
35
|
+
value here cannot track. That formula assumes roughly 600 cached workflows per GB, so set this
|
|
36
|
+
where payloads are heavier - the startup 'Worker sizing' log reports what it resolved to.
|
|
37
|
+
https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#maxcachedworkflows
|
|
38
|
+
*/
|
|
39
|
+
TEMPORAL_MAX_CACHED_WORKFLOWS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().optional() ),
|
|
29
40
|
// How aggressively the worker pulls tasks from Temporal.
|
|
30
41
|
TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 5 ) ),
|
|
31
42
|
TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 5 ) ),
|
|
43
|
+
/*
|
|
44
|
+
Threads running workflow sandboxes. Each is a V8 isolate and NODE_OPTIONS heap caps apply per
|
|
45
|
+
isolate, so this multiplies the heap the process can commit - the startup 'Worker sizing'
|
|
46
|
+
log reports the total. Left unset so @temporalio/worker picks: 1 with reuseV8Context, 2 without.
|
|
47
|
+
https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#workflowthreadpoolsize
|
|
48
|
+
*/
|
|
49
|
+
TEMPORAL_WORKFLOW_THREAD_POOL_SIZE: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().optional() ),
|
|
32
50
|
// JSON-encoded Temporal Worker tuner options.
|
|
33
51
|
TEMPORAL_WORKER_TUNER: workerTunerEnvSchema,
|
|
34
52
|
// Activity configs
|
|
@@ -64,6 +82,7 @@ export const maxConcurrentWorkflowTaskExecutions = envVars.TEMPORAL_MAX_CONCURRE
|
|
|
64
82
|
export const maxCachedWorkflows = envVars.TEMPORAL_MAX_CACHED_WORKFLOWS;
|
|
65
83
|
export const maxConcurrentActivityTaskPolls = envVars.TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS;
|
|
66
84
|
export const maxConcurrentWorkflowTaskPolls = envVars.TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS;
|
|
85
|
+
export const workflowThreadPoolSize = envVars.TEMPORAL_WORKFLOW_THREAD_POOL_SIZE;
|
|
67
86
|
export const workerTuner = envVars.TEMPORAL_WORKER_TUNER;
|
|
68
87
|
export const namespace = envVars.TEMPORAL_NAMESPACE;
|
|
69
88
|
export const taskQueue = envVars.OUTPUT_CATALOG_ID;
|
package/src/worker/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Worker, NativeConnection } from '@temporalio/worker';
|
|
2
|
-
import
|
|
2
|
+
import { address, apiKey, namespace, taskQueue, grpcProxy, hookFlushTimeoutMs } from './configs.js';
|
|
3
3
|
import { loadActivities } from './loader/activities.js';
|
|
4
4
|
import { loadWorkflows } from './loader/workflows.js';
|
|
5
5
|
import { loadHooks } from './loader/hooks.js';
|
|
@@ -15,7 +15,7 @@ import { CatalogPublisher } from './catalog_workflow/catalog_publisher.js';
|
|
|
15
15
|
import { mainEventBus } from '#bus';
|
|
16
16
|
import { flushPendingHooks } from '#hooks/pending_hooks';
|
|
17
17
|
import { BusEventType } from '#consts';
|
|
18
|
-
import { setupTelemetry } from './telemetry.js';
|
|
18
|
+
import { setupTelemetry, logIsolateSizing } from './telemetry.js';
|
|
19
19
|
import { TemporalConnectionMonitor } from './connection_monitor.js';
|
|
20
20
|
import { bindGlobalFunctions } from './global_functions.js';
|
|
21
21
|
import { setupClientConfig } from '#temporal/client';
|
|
@@ -24,28 +24,12 @@ import { setupTemporalLogger } from './temporal_logger.js';
|
|
|
24
24
|
import { WorkerRunner } from './worker_runner.js';
|
|
25
25
|
import { shutdownServices } from './shutdown.js';
|
|
26
26
|
import { init as initCredentials } from '#credentials/init';
|
|
27
|
+
import { buildWorkerOptions } from './worker_options.js';
|
|
27
28
|
|
|
28
29
|
import './log_hooks.js';
|
|
29
30
|
|
|
30
31
|
const log = createChildLogger( 'Worker' );
|
|
31
32
|
|
|
32
|
-
const {
|
|
33
|
-
address,
|
|
34
|
-
apiKey,
|
|
35
|
-
namespace,
|
|
36
|
-
taskQueue,
|
|
37
|
-
grpcProxy,
|
|
38
|
-
maxConcurrentWorkflowTaskExecutions,
|
|
39
|
-
maxConcurrentActivityTaskExecutions,
|
|
40
|
-
maxCachedWorkflows,
|
|
41
|
-
maxConcurrentActivityTaskPolls,
|
|
42
|
-
maxConcurrentWorkflowTaskPolls,
|
|
43
|
-
shutdownForceTime,
|
|
44
|
-
shutdownGraceTime,
|
|
45
|
-
hookFlushTimeoutMs,
|
|
46
|
-
workerTuner
|
|
47
|
-
} = configs;
|
|
48
|
-
|
|
49
33
|
const services = {
|
|
50
34
|
connection: null,
|
|
51
35
|
connectionMonitor: null,
|
|
@@ -113,34 +97,19 @@ const execute = async () => {
|
|
|
113
97
|
await run( () => services.catalogPublisher.run() );
|
|
114
98
|
|
|
115
99
|
log.info( 'Creating Temporal worker...' );
|
|
116
|
-
const worker = await run( () => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// tuner isn't compatible with concurrent task executions configs
|
|
130
|
-
...( workerTuner ? {
|
|
131
|
-
tuner: workerTuner
|
|
132
|
-
} : {
|
|
133
|
-
maxConcurrentWorkflowTaskExecutions,
|
|
134
|
-
maxConcurrentActivityTaskExecutions
|
|
135
|
-
} ),
|
|
136
|
-
maxCachedWorkflows,
|
|
137
|
-
maxConcurrentActivityTaskPolls,
|
|
138
|
-
maxConcurrentWorkflowTaskPolls,
|
|
139
|
-
bundlerOptions: { webpackConfigHook },
|
|
140
|
-
...( shutdownForceTime !== undefined && { shutdownForceTime } ),
|
|
141
|
-
...( shutdownGraceTime !== undefined && { shutdownGraceTime } )
|
|
142
|
-
} );
|
|
143
|
-
} );
|
|
100
|
+
const worker = await run( () => Worker.create( {
|
|
101
|
+
connection,
|
|
102
|
+
namespace,
|
|
103
|
+
taskQueue,
|
|
104
|
+
workflowsPath,
|
|
105
|
+
activities,
|
|
106
|
+
sinks,
|
|
107
|
+
interceptors: initInterceptors( { activities, workflows } ),
|
|
108
|
+
bundlerOptions: { webpackConfigHook },
|
|
109
|
+
...buildWorkerOptions()
|
|
110
|
+
} ) );
|
|
111
|
+
|
|
112
|
+
run( () => logIsolateSizing( { worker } ) );
|
|
144
113
|
|
|
145
114
|
log.info( 'Setting up telemetry...' );
|
|
146
115
|
run( () => setupTelemetry( { worker } ) );
|
package/src/worker/telemetry.js
CHANGED
|
@@ -1,21 +1,141 @@
|
|
|
1
|
+
import { totalmem } from 'node:os';
|
|
2
|
+
import { getHeapStatistics } from 'node:v8';
|
|
1
3
|
import { serializeError } from '#helpers/error_serializer';
|
|
2
4
|
import { createChildLogger } from '#logger';
|
|
3
5
|
import { workerTelemetryIntervalMs } from './configs.js';
|
|
4
6
|
|
|
5
7
|
const log = createChildLogger( 'Telemetry' );
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the memory limit to compare the worker against: the container's limit, or the
|
|
11
|
+
* machine's total memory when there is none.
|
|
12
|
+
*
|
|
13
|
+
* `process.constrainedMemory()` returns a sentinel near 2^64 rather than 0 when the process is
|
|
14
|
+
* unconstrained, so values at or above the machine's memory are treated as no limit.
|
|
15
|
+
*
|
|
16
|
+
* @returns {number} Bytes.
|
|
17
|
+
*/
|
|
18
|
+
const resolveMemoryLimit = () => {
|
|
19
|
+
const constrainedMemory = process.constrainedMemory();
|
|
20
|
+
const machineMemory = totalmem();
|
|
21
|
+
return constrainedMemory > 0 && constrainedMemory < machineMemory ? constrainedMemory : machineMemory;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Log the worker's isolate and cache sizing. Warn when the projected
|
|
26
|
+
* heap exceeds the memory limit, or when workflow task slots exceed
|
|
27
|
+
* the cache.
|
|
28
|
+
*
|
|
29
|
+
* The worker runs one isolate on the main thread and one per workflow
|
|
30
|
+
* thread. A NODE_OPTIONS heap cap applies to each isolate, not to the
|
|
31
|
+
* process, so on a 4 GB container `--max-old-space-size-percentage=85`
|
|
32
|
+
* allows each isolate ~3.4 GB and two isolates can commit well over
|
|
33
|
+
* the container's memory.
|
|
34
|
+
*
|
|
35
|
+
* The cache and the task slots are sized independently:
|
|
36
|
+
* `@temporalio/worker` derives `maxCachedWorkflows` from the heap limit
|
|
37
|
+
* but fixes `workflowTaskSlots` at 40. On a small container the slots
|
|
38
|
+
* can exceed the cache, which causes tasks to evict each other's
|
|
39
|
+
* workflows and forces history replays.
|
|
40
|
+
*
|
|
41
|
+
* Logged fields are read from `worker.options`, so each is the resolved
|
|
42
|
+
* value rather than the requested one:
|
|
43
|
+
*
|
|
44
|
+
* - `workflowThreadPoolSize`: threads running workflow sandboxes.
|
|
45
|
+
* Defaults to 1 with `reuseV8Context`, 2 without. Ignored under
|
|
46
|
+
* `debugMode`.
|
|
47
|
+
* - `reuseV8Context`: whether workflows share one VM context per thread
|
|
48
|
+
* or each get a new one. Enabled by default. Upstream estimates
|
|
49
|
+
* roughly 600 cached workflows per GB with it and 250 without.
|
|
50
|
+
* - `isolateCount`: `1 + workflowThreadPoolSize`, the multiplier on the
|
|
51
|
+
* per-isolate heap cap. 1 under `debugMode`, which runs workflows in
|
|
52
|
+
* the main isolate and spawns no workflow threads.
|
|
53
|
+
* - `heapSizeLimit`: the main isolate's V8 heap ceiling, as resolved
|
|
54
|
+
* from NODE_OPTIONS.
|
|
55
|
+
* - `projectedCommittedHeap`: `isolateCount * heapSizeLimit`, the heap
|
|
56
|
+
* the process can commit if every isolate fills. Only the main
|
|
57
|
+
* thread's limit is readable, so this assumes the workflow threads
|
|
58
|
+
* have the same one.
|
|
59
|
+
* - `memoryLimit`: the limit the projection is compared against. The
|
|
60
|
+
* container's limit, or the machine's total memory when unconstrained.
|
|
61
|
+
* - `maxCachedWorkflows`: sticky cache size. Derived from
|
|
62
|
+
* `heapSizeLimit` when TEMPORAL_MAX_CACHED_WORKFLOWS is unset.
|
|
63
|
+
* - `workflowTaskSlots`: workflow tasks that may run at once, from the
|
|
64
|
+
* tuner. Undefined with a resource-based tuner, which sizes slots at
|
|
65
|
+
* runtime.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} params Parameters.
|
|
68
|
+
* @param {import('@temporalio/worker').Worker} params.worker The created Temporal worker.
|
|
69
|
+
*/
|
|
70
|
+
export const logIsolateSizing = ( { worker } ) => {
|
|
71
|
+
const { workflowThreadPoolSize, reuseV8Context, maxCachedWorkflows, tuner, debugMode } = worker.options;
|
|
72
|
+
// debugMode runs workflows in the main isolate and spawns no workflow threads.
|
|
73
|
+
const isolateCount = debugMode ? 1 : 1 + workflowThreadPoolSize;
|
|
74
|
+
const heapSizeLimit = getHeapStatistics().heap_size_limit;
|
|
75
|
+
const memoryLimit = resolveMemoryLimit();
|
|
76
|
+
const workflowTaskSlots = tuner?.workflowTaskSlotSupplier?.numSlots;
|
|
77
|
+
const projectedCommittedHeap = isolateCount * heapSizeLimit;
|
|
78
|
+
|
|
79
|
+
log.info( 'Worker sizing', {
|
|
80
|
+
workflowThreadPoolSize,
|
|
81
|
+
reuseV8Context,
|
|
82
|
+
isolateCount,
|
|
83
|
+
heapSizeLimit,
|
|
84
|
+
projectedCommittedHeap,
|
|
85
|
+
memoryLimit,
|
|
86
|
+
maxCachedWorkflows,
|
|
87
|
+
workflowTaskSlots
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
if ( projectedCommittedHeap > memoryLimit ) {
|
|
91
|
+
log.warn( 'Worker isolates may commit more heap than the host allows', { isolateCount, heapSizeLimit, projectedCommittedHeap, memoryLimit } );
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ( workflowTaskSlots > maxCachedWorkflows ) {
|
|
95
|
+
log.warn( 'Worker runs more workflow tasks than it can cache, forcing replays', { workflowTaskSlots, maxCachedWorkflows } );
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Start an interval that logs worker status and memory every
|
|
101
|
+
* OUTPUT_WORKER_TELEMETRY_INTERVAL_MS. Disabled when it is 0.
|
|
102
|
+
*
|
|
103
|
+
* Each record holds the Temporal worker's `status` and a `memory`
|
|
104
|
+
* block. The memory fields mix process-wide and main-isolate scopes:
|
|
105
|
+
*
|
|
106
|
+
* - `availableMemory`: memory the process can still allocate, within
|
|
107
|
+
* the container limit.
|
|
108
|
+
* - `memoryLimit`: the container's limit, or the machine's total memory
|
|
109
|
+
* when unconstrained.
|
|
110
|
+
* - `memoryUsage`: raw `process.memoryUsage()`. `rss` is process-wide;
|
|
111
|
+
* `heapTotal` and `heapUsed` cover only the main isolate.
|
|
112
|
+
* - `nonMainHeap`: `memoryUsage.rss - memoryUsage.heapUsed`, the
|
|
113
|
+
* resident memory outside the main isolate's live objects: workflow
|
|
114
|
+
* thread heaps, Rust core allocations, buffers, and heap pages V8 has
|
|
115
|
+
* committed but not filled. An upper bound on the workflow thread
|
|
116
|
+
* heap, not a measurement of it. Cached workflow memory is counted
|
|
117
|
+
* here, so it grows with the cache while `heapUsed` stays flat.
|
|
118
|
+
* - `heapSizeLimit`: the main isolate's V8 heap ceiling. Constant, but
|
|
119
|
+
* included in each sample so records are self-contained.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} params Parameters.
|
|
122
|
+
* @param {import('@temporalio/worker').Worker} params.worker The created Temporal worker.
|
|
123
|
+
*/
|
|
7
124
|
export const setupTelemetry = ( { worker } ) => {
|
|
8
125
|
if ( workerTelemetryIntervalMs <= 0 ) {
|
|
9
126
|
return;
|
|
10
127
|
}
|
|
11
128
|
setInterval( () => {
|
|
12
129
|
try {
|
|
130
|
+
const memoryUsage = process.memoryUsage();
|
|
13
131
|
log.info( 'Worker', {
|
|
14
132
|
status: worker.getStatus(),
|
|
15
133
|
memory: {
|
|
16
134
|
availableMemory: process.availableMemory(),
|
|
17
|
-
|
|
18
|
-
memoryUsage
|
|
135
|
+
memoryLimit: resolveMemoryLimit(),
|
|
136
|
+
memoryUsage,
|
|
137
|
+
nonMainHeap: memoryUsage.rss - memoryUsage.heapUsed,
|
|
138
|
+
heapSizeLimit: getHeapStatistics().heap_size_limit
|
|
19
139
|
}
|
|
20
140
|
} );
|
|
21
141
|
} catch ( error ) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as configs from './configs.js';
|
|
2
|
+
import { createChildLogger } from '#logger';
|
|
3
|
+
|
|
4
|
+
const log = createChildLogger( 'Worker' );
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
maxConcurrentWorkflowTaskExecutions,
|
|
8
|
+
maxConcurrentActivityTaskExecutions,
|
|
9
|
+
maxCachedWorkflows,
|
|
10
|
+
maxConcurrentActivityTaskPolls,
|
|
11
|
+
maxConcurrentWorkflowTaskPolls,
|
|
12
|
+
workflowThreadPoolSize,
|
|
13
|
+
shutdownForceTime,
|
|
14
|
+
shutdownGraceTime,
|
|
15
|
+
workerTuner
|
|
16
|
+
} = configs;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds the concurrency, cache and shutdown options passed to `Worker.create`.
|
|
20
|
+
* Unset values are omitted so @temporalio/worker applies its own defaults.
|
|
21
|
+
* @returns {object} partial Temporal worker options
|
|
22
|
+
*/
|
|
23
|
+
export const buildWorkerOptions = () => {
|
|
24
|
+
if ( workerTuner ) {
|
|
25
|
+
log.info( 'Using worker tuner options', { ...workerTuner } );
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
// tuner isn't compatible with concurrent task executions configs
|
|
29
|
+
...( workerTuner ? {
|
|
30
|
+
tuner: workerTuner
|
|
31
|
+
} : {
|
|
32
|
+
...( maxConcurrentWorkflowTaskExecutions !== undefined && { maxConcurrentWorkflowTaskExecutions } ),
|
|
33
|
+
maxConcurrentActivityTaskExecutions
|
|
34
|
+
} ),
|
|
35
|
+
...( maxCachedWorkflows !== undefined && { maxCachedWorkflows } ),
|
|
36
|
+
maxConcurrentActivityTaskPolls,
|
|
37
|
+
maxConcurrentWorkflowTaskPolls,
|
|
38
|
+
// Omitted when unset so @temporalio/worker applies its own default, which depends on reuseV8Context.
|
|
39
|
+
...( workflowThreadPoolSize !== undefined && { workflowThreadPoolSize } ),
|
|
40
|
+
...( shutdownForceTime !== undefined && { shutdownForceTime } ),
|
|
41
|
+
...( shutdownGraceTime !== undefined && { shutdownGraceTime } )
|
|
42
|
+
};
|
|
43
|
+
};
|