167 lines
4.9 KiB
TypeScript
167 lines
4.9 KiB
TypeScript
type ScheduledWork<T> = (signal: AbortSignal) => Promise<T>
|
|
|
|
type QueueEntry<T> = {
|
|
work: ScheduledWork<T>
|
|
signal?: AbortSignal
|
|
resolve: (value: T) => void
|
|
reject: (reason: unknown) => void
|
|
removeAbortListener?: () => void
|
|
}
|
|
|
|
export type SubagentSchedulerOptions = {
|
|
concurrency?: number
|
|
queueLimit?: number
|
|
timeoutMs?: number
|
|
}
|
|
|
|
function abortError(signal?: AbortSignal): Error {
|
|
const reason = signal?.reason
|
|
if (reason instanceof Error) {
|
|
return reason
|
|
}
|
|
const error = new Error('子专家任务已取消')
|
|
error.name = 'AbortError'
|
|
return error
|
|
}
|
|
|
|
export class SubagentScheduler {
|
|
private readonly concurrency: number
|
|
private readonly queueLimit: number
|
|
private readonly timeoutMs: number
|
|
private readonly queue: QueueEntry<unknown>[] = []
|
|
private readonly activeControllers = new Set<AbortController>()
|
|
private active = 0
|
|
private disposed = false
|
|
private readonly idleWaiters = new Set<() => void>()
|
|
|
|
constructor(options: SubagentSchedulerOptions = {}) {
|
|
this.concurrency = options.concurrency ?? 3
|
|
this.queueLimit = options.queueLimit ?? 20
|
|
this.timeoutMs = options.timeoutMs ?? 120_000
|
|
if (
|
|
!Number.isSafeInteger(this.concurrency) ||
|
|
this.concurrency < 1 ||
|
|
!Number.isSafeInteger(this.queueLimit) ||
|
|
this.queueLimit < 0 ||
|
|
!Number.isSafeInteger(this.timeoutMs) ||
|
|
this.timeoutMs < 1
|
|
) {
|
|
throw new RangeError('子专家调度器配置无效')
|
|
}
|
|
}
|
|
|
|
schedule<T>(
|
|
work: ScheduledWork<T>,
|
|
signal?: AbortSignal
|
|
): Promise<T> {
|
|
if (this.disposed) {
|
|
return Promise.reject(new Error('子专家调度器已关闭'))
|
|
}
|
|
if (signal?.aborted) {
|
|
return Promise.reject(abortError(signal))
|
|
}
|
|
if (this.active >= this.concurrency && this.queue.length >= this.queueLimit) {
|
|
return Promise.reject(new Error('子专家任务队列已满'))
|
|
}
|
|
return new Promise<T>((resolve, reject) => {
|
|
const entry: QueueEntry<T> = { work, signal, resolve, reject }
|
|
if (signal) {
|
|
const onAbort = (): void => {
|
|
const index = this.queue.indexOf(entry as QueueEntry<unknown>)
|
|
if (index >= 0) {
|
|
this.queue.splice(index, 1)
|
|
entry.removeAbortListener?.()
|
|
reject(abortError(signal))
|
|
}
|
|
}
|
|
signal.addEventListener('abort', onAbort, { once: true })
|
|
entry.removeAbortListener = () =>
|
|
signal.removeEventListener('abort', onAbort)
|
|
}
|
|
if (this.active < this.concurrency) {
|
|
this.start(entry)
|
|
} else {
|
|
this.queue.push(entry as QueueEntry<unknown>)
|
|
}
|
|
})
|
|
}
|
|
|
|
cancelAll(reason = new Error('子专家任务已取消')): void {
|
|
for (const entry of this.queue.splice(0)) {
|
|
entry.removeAbortListener?.()
|
|
entry.reject(reason)
|
|
}
|
|
for (const controller of this.activeControllers) {
|
|
controller.abort(reason)
|
|
}
|
|
}
|
|
|
|
waitForIdle(): Promise<void> {
|
|
if (this.active === 0 && this.queue.length === 0) {
|
|
return Promise.resolve()
|
|
}
|
|
return new Promise((resolve) => this.idleWaiters.add(resolve))
|
|
}
|
|
|
|
dispose(): void {
|
|
this.disposed = true
|
|
this.cancelAll(new Error('子专家调度器已关闭'))
|
|
}
|
|
|
|
private start<T>(entry: QueueEntry<T>): void {
|
|
entry.removeAbortListener?.()
|
|
this.active += 1
|
|
const controller = new AbortController()
|
|
this.activeControllers.add(controller)
|
|
const forwardAbort = (): void =>
|
|
controller.abort(abortError(entry.signal))
|
|
entry.signal?.addEventListener('abort', forwardAbort, { once: true })
|
|
const timeout = setTimeout(() => {
|
|
controller.abort(new Error('子专家任务超过 120 秒超时限制'))
|
|
}, this.timeoutMs)
|
|
|
|
const workPromise = Promise.resolve().then(() => {
|
|
controller.signal.throwIfAborted()
|
|
return entry.work(controller.signal)
|
|
})
|
|
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
const onAbort = (): void => {
|
|
controller.signal.removeEventListener('abort', onAbort)
|
|
reject(abortError(controller.signal))
|
|
}
|
|
controller.signal.addEventListener('abort', onAbort, { once: true })
|
|
})
|
|
void Promise.race([workPromise, abortPromise])
|
|
.then(entry.resolve, entry.reject)
|
|
.finally(() => {
|
|
clearTimeout(timeout)
|
|
entry.signal?.removeEventListener('abort', forwardAbort)
|
|
this.activeControllers.delete(controller)
|
|
this.active -= 1
|
|
this.drain()
|
|
if (this.active === 0 && this.queue.length === 0) {
|
|
for (const resolve of this.idleWaiters) {
|
|
resolve()
|
|
}
|
|
this.idleWaiters.clear()
|
|
}
|
|
})
|
|
}
|
|
|
|
private drain(): void {
|
|
while (
|
|
!this.disposed &&
|
|
this.active < this.concurrency &&
|
|
this.queue.length > 0
|
|
) {
|
|
const entry = this.queue.shift()!
|
|
if (entry.signal?.aborted) {
|
|
entry.removeAbortListener?.()
|
|
entry.reject(abortError(entry.signal))
|
|
continue
|
|
}
|
|
this.start(entry)
|
|
}
|
|
}
|
|
}
|