fix: tear down timed-out speech workers (#2880)
This commit is contained in:
parent
03e43ef165
commit
b9165ac4bd
|
|
@ -29,6 +29,10 @@ const { MockWorker, getCreatedWorkerCount, getLastWorker, resetWorkers } = vi.ho
|
|||
return this
|
||||
}
|
||||
|
||||
listenerCount(eventName: string): number {
|
||||
return this.listeners.get(eventName)?.size ?? 0
|
||||
}
|
||||
|
||||
removeAllListeners(): this {
|
||||
this.listeners.clear()
|
||||
return this
|
||||
|
|
@ -239,4 +243,33 @@ describe('SttService', () => {
|
|||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not retain or reuse a worker that timed out while stopping', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const service = new SttService({
|
||||
getModelState: vi.fn().mockResolvedValue({ id: 'model-a', status: 'ready' }),
|
||||
getModelDir: vi.fn().mockReturnValue('/tmp/model-a')
|
||||
} as never)
|
||||
|
||||
await service.startDictation('model-a', vi.fn(), undefined, 'desktop')
|
||||
const firstWorker = getLastWorker()
|
||||
expect(firstWorker).toBeDefined()
|
||||
firstWorker!.emitStoppedOnStop = false
|
||||
|
||||
const stopPromise = service.stopDictation('desktop')
|
||||
await vi.advanceTimersByTimeAsync(60_000)
|
||||
await stopPromise
|
||||
|
||||
expect(firstWorker!.terminated).toBe(true)
|
||||
expect(firstWorker!.listenerCount('message')).toBe(0)
|
||||
|
||||
await service.startDictation('model-a', vi.fn(), undefined, 'desktop')
|
||||
|
||||
expect(getCreatedWorkerCount()).toBe(2)
|
||||
expect(getLastWorker()).not.toBe(firstWorker)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable max-lines -- Why: speech worker ownership, warm reuse, and
|
||||
timeout teardown must stay co-located so dictation lifecycle state cannot drift. */
|
||||
import { Worker } from 'worker_threads'
|
||||
import { join } from 'path'
|
||||
import { app } from 'electron'
|
||||
|
|
@ -232,26 +234,62 @@ export class SttService {
|
|||
const worker = this.worker
|
||||
worker.postMessage({ type: 'stop' })
|
||||
|
||||
let forcedTeardown = false
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
worker.terminate()
|
||||
let settled = false
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const cleanup = (): void => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
worker.off('message', onStopped)
|
||||
}
|
||||
|
||||
const finish = (): void => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
cleanup()
|
||||
resolve()
|
||||
}, STOP_DICTATION_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const onStopped = (msg: { type: string; text?: string; error?: string }) => {
|
||||
if (msg.type === 'stopped') {
|
||||
clearTimeout(timeout)
|
||||
worker.off('message', onStopped)
|
||||
resolve()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
forcedTeardown = true
|
||||
cleanup()
|
||||
// Why: a worker that cannot finish dictation is no longer reusable; do
|
||||
// not keep it in the warm-worker slot or retain its message listeners.
|
||||
worker.removeAllListeners()
|
||||
void worker.terminate().finally(resolve)
|
||||
}, STOP_DICTATION_TIMEOUT_MS)
|
||||
|
||||
worker.on('message', onStopped)
|
||||
})
|
||||
|
||||
if (this.worker === worker) {
|
||||
this.activeOwner = null
|
||||
this.eventSink = null
|
||||
this.scheduleIdleTeardown()
|
||||
if (forcedTeardown) {
|
||||
this.worker = null
|
||||
this.activeModelId = null
|
||||
this.activeHotwordsFilePath = undefined
|
||||
this.activeOwner = null
|
||||
this.eventSink = null
|
||||
} else {
|
||||
this.activeOwner = null
|
||||
this.eventSink = null
|
||||
this.scheduleIdleTeardown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue