Files
orca/src/main/worker-threads/lazy-worker-thread-host.ts
T
Brennan Benson 175f10e32b Run local usage services in a supervised backend process (#40792)
* Move local usage services into a supervised backend process

* Own usage recovery and worker retirement lifetimes

* Preserve answered usage data during background refresh

* Run independent usage scans concurrently in the backend

* Limit backend startup paths to the delegated usage service

* Show usage failures through backend recovery

* Include usage failure strings in the English catalog

* Keep usage update details beside refresh failures

* Resolve usage freshness failure from the final producer snapshot

* Retain the forced refresh completion requirement

* Keep managed backend failures out of application crash reports

* Preserve usage recovery boundaries across backend reconnects

* Match the backend RPC cancellation signal in the usage seam proof

* Avoid serializing backend requests before port delivery

* Keep receiving request validation in the boundary schemas

* test: align usage transport coverage with renderer project

* fix: preserve unknown usage state and renderer retirement

* fix(usage): revalidate tracking before scan admission

* fix(usage): retry failed backend startup on demand

* test(usage): isolate stale backend ownership checks
2026-11-11 16:39:52 -08:00

141 lines
4.1 KiB
TypeScript

import type { WorkerRequestTransport, WorkerThreadFactory } from './worker-thread-transport'
/** Clients own request state and must fail queued calls when spawning fails. */
export class LazyWorkerThreadHost<TResponse> {
private worker: WorkerRequestTransport | null = null
private idleTimer: NodeJS.Timeout | null = null
private cleanupListeners: (() => void) | null = null
private reportedUnavailable = false
private retiring: { completion: Promise<void>; failure?: unknown } | null = null
constructor(
private readonly options: {
factory: WorkerThreadFactory
idleTeardownMs: number
onMessage: (response: TResponse) => void
onError: (error: Error) => void
onExit: (code: number) => void
/** No active or queued requests; checked again at idle expiry. */
isIdle: () => boolean
/** Called only for the first spawn failure. */
onUnavailable: (error: unknown) => void
/** Native calls can delay termination; wait for exit before spawning another worker (#24572). */
awaitRetirement?: boolean
/** Usage ownership fences require exact exit proof after termination fails. */
strictRetirement?: boolean
}
) {}
get current(): WorkerRequestTransport | null {
return this.worker
}
get isRetiring(): boolean {
return this.retiring !== null
}
/** Returns null if spawning fails or a previous worker is still retiring. */
ensure(): WorkerRequestTransport | null {
if (this.worker) {
return this.worker
}
if (this.retiring) {
return null
}
try {
const worker = this.options.factory()
const onMessage = (response: TResponse): void => this.options.onMessage(response)
const onError = (error: Error): void => this.options.onError(error)
const onExit = (code: number): void => this.options.onExit(code)
worker.on('message', onMessage)
worker.on('error', onError)
worker.on('exit', onExit)
this.cleanupListeners = () => {
worker.off('message', onMessage)
worker.off('error', onError)
worker.off('exit', onExit)
}
// Never keep the app alive for background work.
worker.unref?.()
this.worker = worker
return worker
} catch (err) {
if (!this.reportedUnavailable) {
this.reportedUnavailable = true
this.options.onUnavailable(err)
}
return null
}
}
destroy(): void {
this.clearIdleTimer()
const worker = this.worker
this.worker = null
if (!worker) {
return
}
this.cleanupListeners?.()
this.cleanupListeners = null
worker.removeAllListeners()
if (!this.options.awaitRetirement) {
void worker.terminate().catch(() => {})
return
}
const retirement: { completion: Promise<void>; failure?: unknown } = {
completion: Promise.resolve()
}
this.retiring = retirement
let proveExit!: () => void
const exitProof = new Promise<void>((resolve) => {
proveExit = resolve
})
const onExit = (): void => {
worker.off('exit', onExit)
if (this.retiring === retirement) {
this.retiring = null
}
proveExit()
}
worker.on('exit', onExit)
const terminated = worker.terminate().then(onExit, (error: unknown) => {
if (this.options.strictRetirement) {
retirement.failure = error
} else {
onExit()
}
})
retirement.completion = Promise.race([exitProof, terminated])
}
async retired(): Promise<void> {
const retirement = this.retiring
await retirement?.completion
if (this.retiring === retirement && retirement?.failure !== undefined) {
throw retirement.failure
}
}
scheduleIdleTeardown(): void {
this.clearIdleTimer()
if (!this.worker) {
return
}
this.idleTimer = setTimeout(() => {
this.idleTimer = null
// Recheck idle state so requests arriving during the delay survive.
if (this.options.isIdle()) {
this.destroy()
}
}, this.options.idleTeardownMs)
this.idleTimer.unref?.()
}
clearIdleTimer(): void {
if (this.idleTimer) {
clearTimeout(this.idleTimer)
this.idleTimer = null
}
}
}