@livekit/agents 0.1.0 → 0.3.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +47 -0
- package/LICENSE +201 -0
- package/dist/audio.d.ts +9 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +54 -0
- package/dist/audio.js.map +1 -0
- package/dist/cli.d.ts +12 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +102 -19
- package/dist/cli.js.map +1 -1
- package/dist/generator.d.ts +17 -6
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +20 -3
- package/dist/generator.js.map +1 -1
- package/dist/http_server.d.ts +1 -1
- package/dist/http_server.d.ts.map +1 -1
- package/dist/http_server.js +5 -3
- package/dist/http_server.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/ipc/job_executor.d.ts +19 -0
- package/dist/ipc/job_executor.d.ts.map +1 -0
- package/dist/ipc/job_executor.js +8 -0
- package/dist/ipc/job_executor.js.map +1 -0
- package/dist/ipc/job_main.d.ts +7 -4
- package/dist/ipc/job_main.d.ts.map +1 -1
- package/dist/ipc/job_main.js +102 -59
- package/dist/ipc/job_main.js.map +1 -1
- package/dist/ipc/message.d.ts +41 -0
- package/dist/ipc/message.d.ts.map +1 -0
- package/dist/ipc/message.js +2 -0
- package/dist/ipc/message.js.map +1 -0
- package/dist/ipc/proc_job_executor.d.ts +15 -0
- package/dist/ipc/proc_job_executor.d.ts.map +1 -0
- package/dist/ipc/proc_job_executor.js +150 -0
- package/dist/ipc/proc_job_executor.js.map +1 -0
- package/dist/ipc/proc_pool.d.ts +26 -0
- package/dist/ipc/proc_pool.d.ts.map +1 -0
- package/dist/ipc/proc_pool.js +83 -0
- package/dist/ipc/proc_pool.js.map +1 -0
- package/dist/job.d.ts +100 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +213 -0
- package/dist/job.js.map +1 -0
- package/dist/llm/function_context.d.ts +20 -0
- package/dist/llm/function_context.d.ts.map +1 -0
- package/dist/llm/function_context.js +37 -0
- package/dist/llm/function_context.js.map +1 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +6 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/log.d.ts +12 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +28 -11
- package/dist/log.js.map +1 -1
- package/dist/multimodal/agent_playout.d.ts +34 -0
- package/dist/multimodal/agent_playout.d.ts.map +1 -0
- package/dist/multimodal/agent_playout.js +221 -0
- package/dist/multimodal/agent_playout.js.map +1 -0
- package/dist/multimodal/index.d.ts +3 -0
- package/dist/multimodal/index.d.ts.map +1 -0
- package/dist/multimodal/index.js +6 -0
- package/dist/multimodal/index.js.map +1 -0
- package/dist/multimodal/multimodal_agent.d.ts +47 -0
- package/dist/multimodal/multimodal_agent.d.ts.map +1 -0
- package/dist/multimodal/multimodal_agent.js +331 -0
- package/dist/multimodal/multimodal_agent.js.map +1 -0
- package/dist/plugin.js +20 -7
- package/dist/plugin.js.map +1 -1
- package/dist/stt/index.d.ts +1 -1
- package/dist/stt/index.d.ts.map +1 -1
- package/dist/stt/index.js.map +1 -1
- package/dist/stt/stream_adapter.d.ts +2 -11
- package/dist/stt/stream_adapter.d.ts.map +1 -1
- package/dist/stt/stream_adapter.js +47 -33
- package/dist/stt/stream_adapter.js.map +1 -1
- package/dist/stt/stt.d.ts +27 -0
- package/dist/stt/stt.d.ts.map +1 -1
- package/dist/stt/stt.js +32 -5
- package/dist/stt/stt.js.map +1 -1
- package/dist/transcription.d.ts +22 -0
- package/dist/transcription.d.ts.map +1 -0
- package/dist/transcription.js +111 -0
- package/dist/transcription.js.map +1 -0
- package/dist/tts/stream_adapter.d.ts +4 -11
- package/dist/tts/stream_adapter.d.ts.map +1 -1
- package/dist/tts/stream_adapter.js +66 -32
- package/dist/tts/stream_adapter.js.map +1 -1
- package/dist/tts/tts.d.ts +10 -0
- package/dist/tts/tts.d.ts.map +1 -1
- package/dist/tts/tts.js +48 -7
- package/dist/tts/tts.js.map +1 -1
- package/dist/utils.d.ts +59 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +212 -6
- package/dist/utils.js.map +1 -1
- package/dist/vad.d.ts +29 -0
- package/dist/vad.d.ts.map +1 -1
- package/dist/vad.js.map +1 -1
- package/dist/worker.d.ts +69 -50
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +414 -213
- package/dist/worker.js.map +1 -1
- package/package.json +12 -10
- package/src/audio.ts +62 -0
- package/src/cli.ts +108 -20
- package/src/generator.ts +27 -7
- package/src/http_server.ts +5 -0
- package/src/index.ts +15 -3
- package/src/ipc/job_executor.ts +25 -0
- package/src/ipc/job_main.ts +141 -61
- package/src/ipc/message.ts +39 -0
- package/src/ipc/proc_job_executor.ts +162 -0
- package/src/ipc/proc_pool.ts +109 -0
- package/src/job.ts +278 -0
- package/src/llm/function_context.ts +61 -0
- package/src/llm/index.ts +11 -0
- package/src/log.ts +40 -8
- package/src/multimodal/agent_playout.ts +254 -0
- package/src/multimodal/index.ts +5 -0
- package/src/multimodal/multimodal_agent.ts +428 -0
- package/src/stt/index.ts +1 -1
- package/src/stt/stream_adapter.ts +32 -32
- package/src/stt/stt.ts +27 -0
- package/src/transcription.ts +128 -0
- package/src/tts/stream_adapter.ts +32 -31
- package/src/tts/tts.ts +10 -0
- package/src/utils.ts +257 -3
- package/src/vad.ts +29 -0
- package/src/worker.ts +465 -172
- package/tsconfig.json +7 -1
- package/dist/ipc/job_process.d.ts +0 -22
- package/dist/ipc/job_process.d.ts.map +0 -1
- package/dist/ipc/job_process.js +0 -73
- package/dist/ipc/job_process.js.map +0 -1
- package/dist/ipc/protocol.d.ts +0 -40
- package/dist/ipc/protocol.d.ts.map +0 -1
- package/dist/ipc/protocol.js +0 -14
- package/dist/ipc/protocol.js.map +0 -1
- package/dist/job_context.d.ts +0 -16
- package/dist/job_context.d.ts.map +0 -1
- package/dist/job_context.js +0 -31
- package/dist/job_context.js.map +0 -1
- package/dist/job_request.d.ts +0 -42
- package/dist/job_request.d.ts.map +0 -1
- package/dist/job_request.js +0 -79
- package/dist/job_request.js.map +0 -1
- package/src/ipc/job_process.ts +0 -96
- package/src/ipc/protocol.ts +0 -51
- package/src/job_context.ts +0 -49
- package/src/job_request.ts +0 -118
package/src/ipc/job_main.ts
CHANGED
|
@@ -1,83 +1,163 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { Room, RoomEvent } from '@livekit/rtc-node';
|
|
5
|
+
import type { ChildProcess } from 'child_process';
|
|
6
|
+
import { fork } from 'child_process';
|
|
7
7
|
import { EventEmitter, once } from 'events';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import type { Logger } from 'pino';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { type Agent, isAgent } from '../generator.js';
|
|
11
|
+
import type { RunningJobInfo } from '../job.js';
|
|
12
|
+
import { JobContext } from '../job.js';
|
|
13
|
+
import { JobProcess } from '../job.js';
|
|
14
|
+
import { initializeLogger, log } from '../log.js';
|
|
15
|
+
import { defaultInitializeProcessFunc } from '../worker.js';
|
|
16
|
+
import type { IPCMessage } from './message.js';
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
const ORPHANED_TIMEOUT = 15 * 1000;
|
|
19
|
+
|
|
20
|
+
type StartArgs = {
|
|
21
|
+
agentFile: string;
|
|
22
|
+
// userArguments: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type JobTask = {
|
|
26
|
+
ctx: JobContext;
|
|
27
|
+
task: Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const runProcess = (args: StartArgs): ChildProcess => {
|
|
31
|
+
return fork(fileURLToPath(import.meta.url), [args.agentFile]);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const startJob = (
|
|
35
|
+
proc: JobProcess,
|
|
36
|
+
func: (ctx: JobContext) => Promise<void>,
|
|
37
|
+
info: RunningJobInfo,
|
|
38
|
+
closeEvent: EventEmitter,
|
|
39
|
+
logger: Logger,
|
|
40
|
+
): JobTask => {
|
|
41
|
+
let connect = false;
|
|
42
|
+
let shutdown = false;
|
|
43
|
+
|
|
44
|
+
const room = new Room();
|
|
45
|
+
room.on(RoomEvent.Disconnected, () => {
|
|
46
|
+
closeEvent.emit('close', false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const onConnect = () => {
|
|
50
|
+
connect = true;
|
|
51
|
+
};
|
|
52
|
+
const onShutdown = (reason: string) => {
|
|
53
|
+
shutdown = true;
|
|
54
|
+
closeEvent.emit('close', true, reason);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ctx = new JobContext(proc, info, room, onConnect, onShutdown);
|
|
58
|
+
|
|
59
|
+
const task = new Promise<void>(async () => {
|
|
60
|
+
const unconnectedTimeout = setTimeout(() => {
|
|
61
|
+
if (!(connect || shutdown)) {
|
|
62
|
+
logger.warn(
|
|
63
|
+
'room not connect after job_entry was called after 10 seconds, ',
|
|
64
|
+
'did you forget to call ctx.connect()?',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}, 10000);
|
|
68
|
+
func(ctx).finally(() => clearTimeout(unconnectedTimeout));
|
|
69
|
+
|
|
70
|
+
await once(closeEvent, 'close').then((close) => {
|
|
71
|
+
logger.debug('shutting down');
|
|
72
|
+
process.send!({ case: 'exiting', value: { reason: close[1] } });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await room.disconnect();
|
|
76
|
+
logger.debug('disconnected from room');
|
|
77
|
+
|
|
78
|
+
const shutdownTasks = [];
|
|
79
|
+
for (const callback of ctx.shutdownCallbacks) {
|
|
80
|
+
shutdownTasks.push(callback());
|
|
81
|
+
}
|
|
82
|
+
await Promise.all(shutdownTasks).catch(() => logger.error('error while shutting down the job'));
|
|
83
|
+
|
|
84
|
+
process.send!({ case: 'done' });
|
|
85
|
+
process.exit();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { ctx, task };
|
|
14
89
|
};
|
|
15
90
|
|
|
16
91
|
if (process.send) {
|
|
17
92
|
// process.argv:
|
|
18
93
|
// [0] `node'
|
|
19
94
|
// [1] import.meta.filename
|
|
20
|
-
// [2]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const args = msg.message.value as JobAssignment;
|
|
27
|
-
|
|
28
|
-
const room = new Room();
|
|
29
|
-
const closeEvent = new EventEmitter();
|
|
30
|
-
let shuttingDown = false;
|
|
31
|
-
let closed = false;
|
|
32
|
-
|
|
33
|
-
process.on('message', (msg: Message) => {
|
|
34
|
-
if (msg.type === IPC_MESSAGE.ShutdownRequest) {
|
|
35
|
-
shuttingDown = true;
|
|
36
|
-
closed = true;
|
|
37
|
-
closeEvent.emit('close');
|
|
38
|
-
} else if (msg.type === IPC_MESSAGE.Ping) {
|
|
39
|
-
process.send!({
|
|
40
|
-
type: IPC_MESSAGE.Pong,
|
|
41
|
-
lastTimestamp: (msg as Ping).timestamp,
|
|
42
|
-
timestamp: Date.now(),
|
|
43
|
-
});
|
|
95
|
+
// [2] import.meta.filename of function containing entry file
|
|
96
|
+
const moduleFile = process.argv[2];
|
|
97
|
+
const agent: Agent = await import(moduleFile).then((module) => {
|
|
98
|
+
const agent = module.default;
|
|
99
|
+
if (agent === undefined || !isAgent(agent)) {
|
|
100
|
+
throw new Error(`Unable to load agent: Missing or invalid default export in ${moduleFile}`);
|
|
44
101
|
}
|
|
102
|
+
return agent;
|
|
45
103
|
});
|
|
104
|
+
if (!agent.prewarm) {
|
|
105
|
+
agent.prewarm = defaultInitializeProcessFunc;
|
|
106
|
+
}
|
|
46
107
|
|
|
47
108
|
// don't do anything on C-c
|
|
109
|
+
// this is handled in cli, triggering a termination of all child processes at once.
|
|
48
110
|
process.on('SIGINT', () => {});
|
|
49
111
|
|
|
50
|
-
|
|
112
|
+
await once(process, 'message').then(([msg]: IPCMessage[]) => {
|
|
113
|
+
if (msg.case !== 'initializeRequest') {
|
|
114
|
+
throw new Error('first message must be InitializeRequest');
|
|
115
|
+
}
|
|
116
|
+
initializeLogger(msg.value.loggerOptions);
|
|
117
|
+
});
|
|
118
|
+
const proc = new JobProcess();
|
|
119
|
+
let logger = log().child({ pid: proc.pid });
|
|
51
120
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
121
|
+
logger.debug('initializing job runner');
|
|
122
|
+
agent.prewarm(proc);
|
|
123
|
+
logger.debug('job runner initialized');
|
|
124
|
+
process.send({ case: 'initializeResponse' });
|
|
55
125
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
import(process.argv[3]).then((agent) => {
|
|
59
|
-
agent.default.entry(new JobContext(closeEvent, args.job!, room));
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
};
|
|
126
|
+
let job: JobTask | undefined = undefined;
|
|
127
|
+
const closeEvent = new EventEmitter();
|
|
63
128
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
})
|
|
69
|
-
.catch((err) => {
|
|
70
|
-
if (!closed) process.send!({ type: IPC_MESSAGE.StartJobResponse, err });
|
|
71
|
-
});
|
|
72
|
-
});
|
|
129
|
+
const orphanedTimeout = setTimeout(() => {
|
|
130
|
+
logger.warn('process orphaned, shutting down');
|
|
131
|
+
process.exit();
|
|
132
|
+
}, ORPHANED_TIMEOUT);
|
|
73
133
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
134
|
+
process.on('message', (msg: IPCMessage) => {
|
|
135
|
+
switch (msg.case) {
|
|
136
|
+
case 'pingRequest': {
|
|
137
|
+
orphanedTimeout.refresh();
|
|
138
|
+
process.send!({
|
|
139
|
+
case: 'pongResponse',
|
|
140
|
+
value: { lastTimestamp: msg.value.timestamp, timestamp: Date.now() },
|
|
141
|
+
});
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'startJobRequest': {
|
|
145
|
+
if (job) {
|
|
146
|
+
throw new Error('job task already running');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
logger = logger.child({ jobID: msg.value.runningJob.job.id });
|
|
150
|
+
|
|
151
|
+
job = startJob(proc, agent.entry, msg.value.runningJob, closeEvent, logger);
|
|
152
|
+
logger.debug('job started');
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'shutdownRequest': {
|
|
156
|
+
if (!job) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
closeEvent.emit('close', '');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
83
163
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { RunningJobInfo } from '../job.js';
|
|
5
|
+
import type { LoggerOptions } from '../log.js';
|
|
6
|
+
|
|
7
|
+
export type IPCMessage =
|
|
8
|
+
| {
|
|
9
|
+
case: 'initializeRequest';
|
|
10
|
+
value: { loggerOptions: LoggerOptions };
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
case: 'initializeResponse';
|
|
14
|
+
value: undefined;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
case: 'pingRequest';
|
|
18
|
+
value: { timestamp: number };
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
case: 'pongResponse';
|
|
22
|
+
value: { lastTimestamp: number; timestamp: number };
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
case: 'startJobRequest';
|
|
26
|
+
value: { runningJob: RunningJobInfo };
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
case: 'shutdownRequest';
|
|
30
|
+
value: { reason?: string };
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
case: 'exiting';
|
|
34
|
+
value: { reason?: string };
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
case: 'done';
|
|
38
|
+
value: undefined;
|
|
39
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { ChildProcess } from 'child_process';
|
|
5
|
+
import { once } from 'events';
|
|
6
|
+
import type { RunningJobInfo } from '../job.js';
|
|
7
|
+
import { log, loggerOptions } from '../log.js';
|
|
8
|
+
import { Future } from '../utils.js';
|
|
9
|
+
import type { ProcOpts } from './job_executor.js';
|
|
10
|
+
import { JobExecutor } from './job_executor.js';
|
|
11
|
+
import type { IPCMessage } from './message.js';
|
|
12
|
+
|
|
13
|
+
export class ProcJobExecutor extends JobExecutor {
|
|
14
|
+
#opts: ProcOpts;
|
|
15
|
+
#started = false;
|
|
16
|
+
#closing = false;
|
|
17
|
+
#runningJob?: RunningJobInfo = undefined;
|
|
18
|
+
#proc?: ChildProcess;
|
|
19
|
+
#pingInterval?: ReturnType<typeof setInterval>;
|
|
20
|
+
#pongTimeout?: ReturnType<typeof setTimeout>;
|
|
21
|
+
#init = new Future();
|
|
22
|
+
#join = new Future();
|
|
23
|
+
#logger = log().child({ runningJob: this.#runningJob });
|
|
24
|
+
|
|
25
|
+
constructor(agent: string, initializeTimeout: number, closeTimeout: number) {
|
|
26
|
+
super();
|
|
27
|
+
this.#opts = {
|
|
28
|
+
agent,
|
|
29
|
+
initializeTimeout,
|
|
30
|
+
closeTimeout,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get started(): boolean {
|
|
35
|
+
return this.#started;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get runningJob(): RunningJobInfo | undefined {
|
|
39
|
+
return this.#runningJob;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async start() {
|
|
43
|
+
if (this.#started) {
|
|
44
|
+
throw new Error('runner already started');
|
|
45
|
+
} else if (this.#closing) {
|
|
46
|
+
throw new Error('runner is closed');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.#proc = await import('./job_main.js').then((m) =>
|
|
50
|
+
m.runProcess({
|
|
51
|
+
agentFile: this.#opts.agent,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
this.#started = true;
|
|
56
|
+
this.run();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async run() {
|
|
60
|
+
await this.#init.await;
|
|
61
|
+
|
|
62
|
+
this.#pingInterval = setInterval(() => {
|
|
63
|
+
this.#proc!.send({ case: 'pingRequest', value: { timestamp: Date.now() } });
|
|
64
|
+
}, this.PING_INTERVAL);
|
|
65
|
+
|
|
66
|
+
this.#pongTimeout = setTimeout(() => {
|
|
67
|
+
this.#logger.warn('job is unresponsive');
|
|
68
|
+
clearTimeout(this.#pongTimeout);
|
|
69
|
+
clearInterval(this.#pingInterval);
|
|
70
|
+
this.#proc!.kill();
|
|
71
|
+
this.#join.resolve();
|
|
72
|
+
}, this.PING_TIMEOUT);
|
|
73
|
+
|
|
74
|
+
const listener = (msg: IPCMessage) => {
|
|
75
|
+
switch (msg.case) {
|
|
76
|
+
case 'pongResponse': {
|
|
77
|
+
const delay = Date.now() - msg.value.timestamp;
|
|
78
|
+
if (delay > this.HIGH_PING_THRESHOLD) {
|
|
79
|
+
this.#logger.child({ delay }).warn('job executor is unresponsive');
|
|
80
|
+
}
|
|
81
|
+
this.#pongTimeout?.refresh();
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'exiting': {
|
|
85
|
+
this.#logger.child({ reason: msg.value.reason }).debug('job exiting');
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'done': {
|
|
89
|
+
this.#closing = true;
|
|
90
|
+
this.#proc!.off('message', listener);
|
|
91
|
+
this.#join.resolve();
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this.#proc!.on('message', listener);
|
|
97
|
+
this.#proc!.on('error', (err) => {
|
|
98
|
+
if (this.#closing) return;
|
|
99
|
+
this.#logger.child({ err }).warn('job process exited unexpectedly');
|
|
100
|
+
clearTimeout(this.#pongTimeout);
|
|
101
|
+
clearInterval(this.#pingInterval);
|
|
102
|
+
this.#join.resolve();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await this.#join.await;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async join() {
|
|
109
|
+
if (!this.#started) {
|
|
110
|
+
throw new Error('runner not started');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await this.#join.await;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async initialize() {
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
const err = new Error('runner initialization timed out');
|
|
119
|
+
this.#init.reject(err);
|
|
120
|
+
throw err;
|
|
121
|
+
}, this.#opts.initializeTimeout);
|
|
122
|
+
this.#proc!.send({ case: 'initializeRequest', value: { loggerOptions } });
|
|
123
|
+
await once(this.#proc!, 'message').then(([msg]: IPCMessage[]) => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
if (msg.case !== 'initializeResponse') {
|
|
126
|
+
throw new Error('first message must be InitializeResponse');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
this.#init.resolve();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async close() {
|
|
133
|
+
if (!this.#started) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.#closing = true;
|
|
137
|
+
|
|
138
|
+
if (!this.#runningJob) {
|
|
139
|
+
this.#proc!.kill();
|
|
140
|
+
this.#join.resolve();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.#proc!.send({ case: 'shutdownRequest' });
|
|
144
|
+
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
this.#logger.error('job shutdown is taking too much time');
|
|
147
|
+
}, this.#opts.closeTimeout);
|
|
148
|
+
await this.#join.await.then(() => {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
clearTimeout(this.#pongTimeout);
|
|
151
|
+
clearInterval(this.#pingInterval);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async launchJob(info: RunningJobInfo) {
|
|
156
|
+
if (this.#runningJob) {
|
|
157
|
+
throw new Error('executor already has a running job');
|
|
158
|
+
}
|
|
159
|
+
this.#runningJob = info;
|
|
160
|
+
this.#proc!.send({ case: 'startJobRequest', value: { runningJob: info } });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { RunningJobInfo } from '../job.js';
|
|
5
|
+
import { Mutex, Queue } from '../utils.js';
|
|
6
|
+
import type { JobExecutor } from './job_executor.js';
|
|
7
|
+
import { ProcJobExecutor } from './proc_job_executor.js';
|
|
8
|
+
|
|
9
|
+
export class ProcPool {
|
|
10
|
+
agent: string;
|
|
11
|
+
initializeTimeout: number;
|
|
12
|
+
closeTimeout: number;
|
|
13
|
+
executors: JobExecutor[] = [];
|
|
14
|
+
tasks: Promise<void>[] = [];
|
|
15
|
+
started = false;
|
|
16
|
+
closed = false;
|
|
17
|
+
controller = new AbortController();
|
|
18
|
+
initMutex = new Mutex();
|
|
19
|
+
procMutex: Mutex;
|
|
20
|
+
procUnlock?: () => void;
|
|
21
|
+
warmedProcQueue = new Queue<JobExecutor>();
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
agent: string,
|
|
25
|
+
numIdleProcesses: number,
|
|
26
|
+
initializeTimeout: number,
|
|
27
|
+
closeTimeout: number,
|
|
28
|
+
) {
|
|
29
|
+
this.agent = agent;
|
|
30
|
+
this.procMutex = new Mutex(numIdleProcesses);
|
|
31
|
+
this.initializeTimeout = initializeTimeout;
|
|
32
|
+
this.closeTimeout = closeTimeout;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get processes(): JobExecutor[] {
|
|
36
|
+
return this.executors;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getByJobId(id: string): JobExecutor | null {
|
|
40
|
+
return this.executors.find((x) => x.runningJob && x.runningJob.job.id === id) || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async launchJob(info: RunningJobInfo) {
|
|
44
|
+
const proc = await this.warmedProcQueue.get();
|
|
45
|
+
if (this.procUnlock) {
|
|
46
|
+
this.procUnlock();
|
|
47
|
+
this.procUnlock = undefined;
|
|
48
|
+
}
|
|
49
|
+
await proc.launchJob(info);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async procWatchTask() {
|
|
53
|
+
const proc = new ProcJobExecutor(this.agent, this.initializeTimeout, this.closeTimeout);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
this.executors.push(proc);
|
|
57
|
+
|
|
58
|
+
const unlock = await this.initMutex.lock();
|
|
59
|
+
if (this.closed) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await proc.start();
|
|
64
|
+
try {
|
|
65
|
+
await proc.initialize();
|
|
66
|
+
await this.warmedProcQueue.put(proc);
|
|
67
|
+
} catch {
|
|
68
|
+
if (this.procUnlock) {
|
|
69
|
+
this.procUnlock();
|
|
70
|
+
this.procUnlock = undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
unlock();
|
|
75
|
+
await proc.join();
|
|
76
|
+
} finally {
|
|
77
|
+
this.executors.splice(this.executors.indexOf(proc));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
start() {
|
|
82
|
+
if (this.started) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.started = true;
|
|
87
|
+
this.run(this.controller.signal);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async run(signal: AbortSignal) {
|
|
91
|
+
while (!signal.aborted) {
|
|
92
|
+
this.procUnlock = await this.procMutex.lock();
|
|
93
|
+
const task = this.procWatchTask();
|
|
94
|
+
this.tasks.push(task);
|
|
95
|
+
task.finally(() => this.tasks.splice(this.tasks.indexOf(task)));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async close() {
|
|
100
|
+
if (!this.started) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.closed = true;
|
|
104
|
+
this.controller.abort();
|
|
105
|
+
this.warmedProcQueue.items.forEach((e) => e.close());
|
|
106
|
+
this.executors.forEach((e) => e.close());
|
|
107
|
+
await Promise.allSettled(this.tasks);
|
|
108
|
+
}
|
|
109
|
+
}
|