Files
orca/src/main/ipc/runtime-watcher-process-pool.ts
T
OrcaWinandm4air 5f308bfa9c revert: take the 26 Phase 3 (#16741 port) PRs back out of main (#24559)
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)"

This reverts commit 783101b304.

* Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)"

This reverts commit 38c2d1dcb9.

* Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)"

This reverts commit 8b76683b40.

* Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)"

This reverts commit d3f8c5063b.

* Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)"

This reverts commit 43d9b43d3f.

* Revert "feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)"

This reverts commit b093d3ab20.

* Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)"

This reverts commit 1a9ac0e955.

* Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)"

This reverts commit 99db2bfae4.

* Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)"

This reverts commit 34a582bd39.

* Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)"

This reverts commit d53063d2b1.

* Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)"

This reverts commit ff212dbbef.

* Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)"

This reverts commit 92cb71765e.

* Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)"

This reverts commit 6b36e4f85b.

* Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)"

This reverts commit d23ecef301.

* Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)"

This reverts commit dd87ae578d.

* Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)"

This reverts commit ece9e4d2e3.

* Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)"

This reverts commit 3fbdaba262.

* Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)"

This reverts commit 4e8edc8872.

* Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)"

This reverts commit 60c93263cc.

* Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)"

This reverts commit 99e0303572.

* Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)"

This reverts commit 817af768b0.

* Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)"

This reverts commit c9918931c8.

* Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)"

This reverts commit dc08ffeba9.

* Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)"

This reverts commit a789233bbb.

* Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)"

This reverts commit 0b812bd698.

* Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)"

This reverts commit 3aa2d3af7c.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-02 00:52:32 -07:00

327 lines
11 KiB
TypeScript

import { isWatcherProcessFailure, WatcherProcessFailure } from './parcel-watcher-process-failure'
import { WatcherProcessSupervisor } from './parcel-watcher-process-supervisor'
import type { WatcherProcessSubscribeOptions } from './parcel-watcher-process-protocol'
import type {
WatcherProcessCallback,
WatcherProcessHooks,
WatcherProcessSubscription
} from './parcel-watcher-process-subscription'
import { RuntimeWatcherPendingAssignment } from './runtime-watcher-pending-assignment'
import { RuntimeWatcherPoolLifecycle } from './runtime-watcher-pool-lifecycle'
import { RuntimeWatcherPredecessorBarriers } from './runtime-watcher-predecessor-barriers'
import { RuntimeWatcherQuarantineQueue } from './runtime-watcher-quarantine-queue'
import { handleRuntimeWatcherSubscriptionFailure } from './runtime-watcher-subscription-failure'
import type {
RuntimeWatcherPoolAssignment,
RuntimeWatcherPoolSlot,
RuntimeWatcherPoolSupervisor,
RuntimeWatcherProcessPoolOptions
} from './runtime-watcher-pool-state'
export type { RuntimeWatcherProcessPoolOptions } from './runtime-watcher-pool-state'
// Why: extra ~50 MiB processes are justified only when quarantine isolates a fault.
const DEFAULT_MAX_SHARED_SUPERVISORS = 1
const DEFAULT_MAX_QUARANTINE_SUPERVISORS = 4
/**
* Share children while healthy, then move failed roots into bounded quarantine slots.
*/
export class RuntimeWatcherProcessPool {
private readonly maxSharedSupervisors: number
private readonly maxQuarantineSupervisors: number
private readonly createSupervisor: () => RuntimeWatcherPoolSupervisor
private readonly activeSlots = new Set<RuntimeWatcherPoolSlot>()
private readonly allSlots = new Set<RuntimeWatcherPoolSlot>()
private readonly assignments = new Map<string, RuntimeWatcherPoolAssignment>()
private readonly pendingAssignments = new Map<
string,
RuntimeWatcherPendingAssignment<RuntimeWatcherPoolAssignment>
>()
private readonly lifecycle = new RuntimeWatcherPoolLifecycle()
private readonly predecessorBarriers = new RuntimeWatcherPredecessorBarriers()
private readonly quarantineQueue: RuntimeWatcherQuarantineQueue<RuntimeWatcherPoolSlot>
constructor(options: RuntimeWatcherProcessPoolOptions = {}) {
this.maxSharedSupervisors = Math.max(
1,
options.maxSharedSupervisors ?? DEFAULT_MAX_SHARED_SUPERVISORS
)
this.maxQuarantineSupervisors = Math.max(
1,
options.maxQuarantineSupervisors ?? DEFAULT_MAX_QUARANTINE_SUPERVISORS
)
this.quarantineQueue = new RuntimeWatcherQuarantineQueue(this.maxQuarantineSupervisors)
this.createSupervisor = options.createSupervisor ?? (() => new WatcherProcessSupervisor())
}
async subscribe(
dir: string,
callback: WatcherProcessCallback,
opts: WatcherProcessSubscribeOptions,
hooks: WatcherProcessHooks = {}
): Promise<WatcherProcessSubscription> {
this.lifecycle.assertActive()
const assignmentOrPromise = this.assignmentForRoot(dir, hooks)
const assignment =
assignmentOrPromise instanceof Promise ? await assignmentOrPromise : assignmentOrPromise
this.lifecycle.assertActive()
const { slot } = assignment
let assignmentReleased = false
const releaseAssignment = (): void => {
if (assignmentReleased) {
return
}
assignmentReleased = true
this.releaseRoot(assignment, dir)
}
const onTerminalError = (error: Error): void => {
handleRuntimeWatcherSubscriptionFailure(
error,
releaseAssignment,
(failure) => this.retireSlot(slot, failure),
() => this.lifecycle.quarantineOrFuse(dir, slot.isolated)
)
hooks.onTerminalError?.(error)
}
let subscription: WatcherProcessSubscription
try {
subscription = await slot.supervisor.subscribe(dir, callback, opts, {
...hooks,
onTerminalError
})
} catch (error) {
handleRuntimeWatcherSubscriptionFailure(
error,
releaseAssignment,
(failure) => this.retireSlot(slot, failure),
() => this.lifecycle.quarantineOrFuse(dir, slot.isolated)
)
throw error
}
let unsubscribePromise: Promise<void> | undefined
return {
unsubscribe: (): Promise<void> => {
unsubscribePromise ??= subscription.unsubscribe().then(
() => releaseAssignment(),
(error: unknown) => {
if (isWatcherProcessFailure(error) && error.scope === 'supervisor') {
this.retireSlot(slot, error)
} else {
releaseAssignment()
}
throw error
}
)
return unsubscribePromise
}
}
}
/** Kill every pooled watcher child (production shutdown or test reset). */
dispose(): void {
this.lifecycle.dispose()
// disposeSlot deletes the current slot from allSlots; deleting the
// in-progress element during Set iteration is safe, so no snapshot needed.
const disposedError = new WatcherProcessFailure(
'file watcher supervisor disposed',
'supervisor',
'supervisor_disposed'
)
this.quarantineQueue.failAll(disposedError)
for (const slot of this.allSlots) {
this.disposeSlot(slot)
}
this.activeSlots.clear()
this.allSlots.clear()
this.assignments.clear()
this.pendingAssignments.clear()
this.predecessorBarriers.clear()
}
resetForTest(): void {
this.dispose()
this.lifecycle.reset()
}
forgetRoot(dir: string): void {
// Physical subscriptions release their assignment through unsubscribe or
// terminal callbacks. This clears fault history after setup gives up.
if (!this.assignments.has(dir)) {
this.quarantineQueue.failRoot(dir)
this.lifecycle.isolatedRoots.delete(dir)
this.lifecycle.failedQuarantineRoots.delete(dir)
}
}
private assignmentForRoot(
dir: string,
hooks: WatcherProcessHooks
): RuntimeWatcherPoolAssignment | Promise<RuntimeWatcherPoolAssignment> {
this.predecessorBarriers.throwIfRetained(dir)
if (this.lifecycle.failedQuarantineRoots.has(dir)) {
throw new WatcherProcessFailure(
'file watcher process failed again in quarantine',
'supervisor',
'supervisor_crash_fuse'
)
}
const assigned = this.assignments.get(dir)
if (assigned && !assigned.slot.retired) {
assigned.leases++
return assigned
}
const pending = this.pendingAssignments.get(dir)
if (pending) {
return pending.wait(hooks, (assignment) => assignment.leases++)
}
const slot = this.lifecycle.isIsolated(dir) ? this.quarantineSlot(dir) : this.sharedSlot()
if (slot instanceof Promise) {
let abandoned = false
let grantedAssignment: RuntimeWatcherPoolAssignment | undefined
const basePromise = slot.then((resolvedSlot) => {
grantedAssignment = this.assignRootToSlot(dir, resolvedSlot, 0)
if (abandoned) {
this.releaseRoot(grantedAssignment, dir, false)
}
return grantedAssignment
})
const pendingAssignment = new RuntimeWatcherPendingAssignment(
basePromise,
() => {
abandoned = true
if (grantedAssignment) {
this.releaseRoot(grantedAssignment, dir, false)
} else {
this.quarantineQueue.failRoot(dir)
}
},
() => {
if (this.pendingAssignments.get(dir) === pendingAssignment) {
this.pendingAssignments.delete(dir)
}
}
)
this.pendingAssignments.set(dir, pendingAssignment)
return pendingAssignment.wait(hooks, (assignment) => assignment.leases++)
}
return this.assignRootToSlot(dir, slot)
}
private assignRootToSlot(
dir: string,
slot: RuntimeWatcherPoolSlot,
leases = 1
): RuntimeWatcherPoolAssignment {
const assignment = { slot, leases }
slot.roots.add(dir)
this.assignments.set(dir, assignment)
return assignment
}
private sharedSlot(): RuntimeWatcherPoolSlot {
const sharedSlots = Array.from(this.activeSlots).filter(
(slot) => !slot.isolated && !slot.retired
)
if (sharedSlots.length < this.maxSharedSupervisors) {
return this.createSlot(false)
}
return sharedSlots.reduce((leastLoaded, candidate) =>
candidate.roots.size < leastLoaded.roots.size ? candidate : leastLoaded
)
}
private quarantineSlot(dir: string): RuntimeWatcherPoolSlot | Promise<RuntimeWatcherPoolSlot> {
const quarantineSlots = Array.from(this.activeSlots).filter(
(slot) => slot.isolated && !slot.retired
)
if (quarantineSlots.length < this.maxQuarantineSupervisors) {
return this.createSlot(true)
}
return this.quarantineQueue.wait(dir)
}
private createSlot(isolated: boolean): RuntimeWatcherPoolSlot {
const slot: RuntimeWatcherPoolSlot = {
supervisor: this.createSupervisor(),
roots: new Set(),
isolated,
retired: false,
disposed: false
}
this.activeSlots.add(slot)
this.allSlots.add(slot)
return slot
}
private retireSlot(slot: RuntimeWatcherPoolSlot, error: WatcherProcessFailure): void {
if (slot.retired) {
return
}
slot.retired = true
this.activeSlots.delete(slot)
// Why: a replacement cannot safely reacquire a root while the failed child
// may still own its native watcher handle (notably on Windows).
this.predecessorBarriers.retain(slot.roots, error)
for (const root of slot.roots) {
if (this.assignments.get(root)?.slot === slot) {
this.assignments.delete(root)
}
this.lifecycle.quarantineOrFuse(root, slot.isolated)
}
slot.roots.clear()
// Why: failAllSubscriptions is still iterating callbacks; defer disposal
// so every logical root receives the supervisor failure first.
queueMicrotask(() => {
this.disposeSlot(slot)
this.drainQuarantineWaiters()
})
}
private releaseRoot(
assignment: RuntimeWatcherPoolAssignment,
dir: string,
releaseLease = true
): void {
if (this.assignments.get(dir) !== assignment) {
return
}
if (releaseLease) {
assignment.leases--
}
if (assignment.leases > 0) {
return
}
const { slot } = assignment
this.assignments.delete(dir)
slot.roots.delete(dir)
this.lifecycle.isolatedRoots.delete(dir)
if (slot.isolated && slot.roots.size === 0 && !slot.retired) {
if (!this.quarantineQueue.grantNext(slot)) {
this.activeSlots.delete(slot)
this.disposeSlot(slot)
}
}
}
private drainQuarantineWaiters(): void {
while (
this.quarantineQueue.length > 0 &&
Array.from(this.activeSlots).filter((slot) => slot.isolated && !slot.retired).length <
this.maxQuarantineSupervisors
) {
this.quarantineQueue.grantNext(this.createSlot(true))
}
}
private disposeSlot(slot: RuntimeWatcherPoolSlot): void {
if (slot.disposed) {
return
}
slot.disposed = true
slot.supervisor.dispose()
this.allSlots.delete(slot)
}
}