Skip to content

Commit 3fee5c2

Browse files
Fix draining behaviour (#1180)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 8f23fad commit 3fee5c2

6 files changed

Lines changed: 86 additions & 30 deletions

File tree

.changeset/plenty-laws-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@livekit/agents": patch
3+
---
4+
5+
Fix worker draining behaviour

agents/src/cli.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ const runServer = async (args: CliArgs) => {
4040
process.exit(130); // SIGINT exit code
4141
});
4242
if (args.production) {
43-
await server.drain();
43+
try {
44+
await server.drain();
45+
} catch (e) {
46+
logger.error(e);
47+
}
4448
}
4549
await server.close();
4650
logger.debug('worker closed due to SIGINT.');
@@ -50,7 +54,11 @@ const runServer = async (args: CliArgs) => {
5054
process.once('SIGTERM', async () => {
5155
logger.debug('SIGTERM received in CLI.');
5256
if (args.production) {
53-
await server.drain();
57+
try {
58+
await server.drain();
59+
} catch (e) {
60+
logger.error(e);
61+
}
5462
}
5563
await server.close();
5664
logger.debug('worker closed due to SIGTERM.');

agents/src/ipc/job_executor.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
4+
import type { Throws } from '@livekit/throws-transformer/throws';
45
import type { RunningJobInfo } from '../job.js';
56

67
export interface JobExecutor {
@@ -9,11 +10,11 @@ export interface JobExecutor {
910
runningJob: RunningJobInfo | undefined;
1011
status: JobStatus;
1112

12-
start(): Promise<void>;
13-
join(): Promise<void>;
14-
initialize(): Promise<void>;
15-
close(): Promise<void>;
16-
launchJob(info: RunningJobInfo): Promise<void>;
13+
start(): Promise<Throws<void, Error>>;
14+
join(): Promise<Throws<void, Error>>;
15+
initialize(): Promise<Throws<void, Error>>;
16+
close(): Promise<Throws<void, Error>>;
17+
launchJob(info: RunningJobInfo): Promise<Throws<void, Error>>;
1718
}
1819

1920
export enum JobStatus {

agents/src/ipc/proc_pool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
import { MultiMutex, Mutex } from '@livekit/mutex';
5+
import type { Throws } from '@livekit/throws-transformer/throws';
56
import type { RunningJobInfo } from '../job.js';
67
import { Queue } from '../utils.js';
78
import type { InferenceExecutor } from './inference_executor.js';
@@ -53,7 +54,7 @@ export class ProcPool {
5354
return this.executors.find((x) => x.runningJob && x.runningJob.job.id === id) || null;
5455
}
5556

56-
async launchJob(info: RunningJobInfo) {
57+
async launchJob(info: RunningJobInfo): Promise<Throws<void, Error>> {
5758
let proc: JobExecutor;
5859
if (this.procMutex) {
5960
const entry = await this.warmedProcQueue.get();

agents/src/utils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ export function waitUntilTimeout<T, E extends Error = IdleTimeoutError>(
837837
promise: Promise<T>,
838838
timeoutMs: number,
839839
throwError?: () => E,
840-
): Promise<Throws<T, E>> {
840+
): Promise<Throws<T, E | IdleTimeoutError>> {
841841
let timer: ReturnType<typeof setTimeout> | undefined;
842842
return Promise.race([
843843
promise,
@@ -968,11 +968,20 @@ export async function waitForAbort(signal: AbortSignal) {
968968
abortFuture.resolve();
969969
signal.removeEventListener('abort', handler);
970970
};
971-
971+
if (signal.aborted) {
972+
return;
973+
}
972974
signal.addEventListener('abort', handler, { once: true });
973975
return await abortFuture.await;
974976
}
975977

978+
export async function rejectOnAbort(signal: AbortSignal): Promise<never> {
979+
if (signal.aborted) throw signal.reason;
980+
const abortFuture = new Future<never>();
981+
signal.addEventListener('abort', () => abortFuture.reject(signal.reason), { once: true });
982+
return abortFuture.await;
983+
}
984+
976985
/**
977986
* Combines two abort signals into a single abort signal.
978987
* @param a - The first abort signal.

agents/src/worker.ts

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
WorkerMessage,
1111
WorkerStatus,
1212
} from '@livekit/protocol';
13+
import type { Throws } from '@livekit/throws-transformer/throws';
1314
import type { ParticipantInfo } from 'livekit-server-sdk';
1415
import { AccessToken, RoomServiceClient } from 'livekit-server-sdk';
1516
import { EventEmitter } from 'node:events';
@@ -22,7 +23,7 @@ import { ProcPool } from './ipc/proc_pool.js';
2223
import type { JobAcceptArguments, JobProcess, RunningJobInfo } from './job.js';
2324
import { JobRequest } from './job.js';
2425
import { log } from './log.js';
25-
import { Future } from './utils.js';
26+
import { Future, rejectOnAbort } from './utils.js';
2627
import { version } from './version.js';
2728

2829
const MAX_RECONNECT_ATTEMPTS = 10;
@@ -428,7 +429,7 @@ export class AgentServer {
428429
}
429430

430431
/** @throws {@link WorkerError} if worker did not drain in time */
431-
async drain(timeout?: number) {
432+
async drain(timeout?: number): Promise<Throws<void, WorkerError>> {
432433
if (this.#draining) {
433434
return;
434435
}
@@ -450,7 +451,7 @@ export class AgentServer {
450451

451452
const joinJobs = async () => {
452453
return Promise.all(
453-
this.#procPool.processes.map((proc) => {
454+
this.#procPool.processes.map((proc): Promise<Throws<void, Error>> => {
454455
if (!proc.runningJob) {
455456
proc.close();
456457
}
@@ -459,17 +460,16 @@ export class AgentServer {
459460
);
460461
};
461462

462-
let timer: NodeJS.Timeout | undefined;
463+
const promises = [joinJobs()];
464+
463465
if (timeout) {
464-
timer = setTimeout(() => {
465-
throw new WorkerError('timed out draining');
466-
}, timeout);
466+
promises.push(
467+
rejectOnAbort(AbortSignal.timeout(timeout)).catch(() => {
468+
throw new WorkerError('timed out draining');
469+
}),
470+
);
467471
}
468-
await joinJobs().then(() => {
469-
if (timeout) {
470-
clearTimeout(timer);
471-
}
472-
});
472+
await Promise.race(promises);
473473
}
474474

475475
async simulateJob(roomName: string, participantIdentity?: string) {
@@ -629,6 +629,25 @@ export class AgentServer {
629629
const loadMonitor = setInterval(() => {
630630
if (closingWS) clearInterval(loadMonitor);
631631

632+
if (this.#draining) {
633+
if (currentStatus !== WorkerStatus.WS_FULL) {
634+
currentStatus = WorkerStatus.WS_FULL;
635+
this.event.emit(
636+
'worker_msg',
637+
new WorkerMessage({
638+
message: {
639+
case: 'updateWorker',
640+
value: {
641+
load: 1,
642+
status: WorkerStatus.WS_FULL,
643+
},
644+
},
645+
}),
646+
);
647+
}
648+
return;
649+
}
650+
632651
const oldStatus = currentStatus;
633652
this.#opts
634653
.loadFunc(this)
@@ -708,6 +727,7 @@ export class AgentServer {
708727
);
709728

710729
this.#pending[req.id] = new PendingAssignment();
730+
711731
const timer = setTimeout(() => {
712732
this.#logger.child({ req }).warn(`assignment for job ${req.id} timed out`);
713733
return;
@@ -718,13 +738,17 @@ export class AgentServer {
718738
});
719739

720740
if (asgn) {
721-
await this.#procPool.launchJob({
722-
acceptArguments: args,
723-
job: msg.job!,
724-
url: asgn.url || this.#opts.wsURL,
725-
token: asgn.token,
726-
workerId: this.id,
727-
});
741+
try {
742+
await this.#procPool.launchJob({
743+
acceptArguments: args,
744+
job: msg.job!,
745+
url: asgn.url || this.#opts.wsURL,
746+
token: asgn.token,
747+
workerId: this.id,
748+
});
749+
} catch (e) {
750+
this.#logger.child({ requestId: req.id }).error(e, 'error launching job');
751+
}
728752
} else {
729753
this.#logger.child({ requestId: req.id }).warn('pending assignment not found');
730754
}
@@ -735,6 +759,14 @@ export class AgentServer {
735759
.child({ jobId: msg.job?.id, resuming: msg.resuming, agentName: this.#opts.agentName })
736760
.info('received job request');
737761

762+
if (this.#draining) {
763+
this.#logger
764+
.child({ jobId: msg.job?.id, resuming: msg.resuming, agentName: this.#opts.agentName })
765+
.info('Worker is draining and no longer available, rejecting job');
766+
await req.reject();
767+
return;
768+
}
769+
738770
const jobRequestTask = async () => {
739771
try {
740772
await this.#opts.requestFunc(req);
@@ -770,7 +802,7 @@ export class AgentServer {
770802
// safe to ignore
771803
return;
772804
}
773-
await proc.close();
805+
await proc.close().catch((e) => this.#logger.error(e, 'Error terminating job'));
774806
}
775807

776808
async close() {

0 commit comments

Comments
 (0)