mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)" This reverts commit783101b304. * Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)" This reverts commit38c2d1dcb9. * Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)" This reverts commit8b76683b40. * Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)" This reverts commitd3f8c5063b. * Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)" This reverts commit43d9b43d3f. * 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 commitb093d3ab20. * Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)" This reverts commit1a9ac0e955. * Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)" This reverts commit99db2bfae4. * Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)" This reverts commit34a582bd39. * Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)" This reverts commitd53063d2b1. * Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)" This reverts commitff212dbbef. * Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)" This reverts commit92cb71765e. * Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)" This reverts commit6b36e4f85b. * Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)" This reverts commitd23ecef301. * Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)" This reverts commitdd87ae578d. * Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)" This reverts commitece9e4d2e3. * Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)" This reverts commit3fbdaba262. * Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)" This reverts commit4e8edc8872. * Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)" This reverts commit60c93263cc. * Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)" This reverts commit99e0303572. * Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)" This reverts commit817af768b0. * Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)" This reverts commitc9918931c8. * Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)" This reverts commitdc08ffeba9. * Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)" This reverts commita789233bbb. * Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)" This reverts commit0b812bd698. * Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)" This reverts commit3aa2d3af7c. --------- Co-authored-by: m4air <m4air@Mac.localdomain>
327 lines
11 KiB
TypeScript
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)
|
|
}
|
|
}
|