mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
fix(runtime): keep timed-out graph generations fenced
This commit is contained in:
@@ -3143,6 +3143,8 @@ export class OrcaRuntimeService {
|
||||
private headlessGraphFallbackAvailable = false
|
||||
private pendingHeadlessPromotionWindowId: number | null = null
|
||||
private rendererGeneration: string | null = null
|
||||
// Only a live renderer whose notification frame failed may heal with the last accepted generation.
|
||||
private sameGenerationGraphRecoveryAllowed = false
|
||||
/** Window whose graph was retired when the window closed; only a fresh attachWindow() revives it. */
|
||||
private retiredGraphWindowId: number | null = null
|
||||
private readonly graphReloadLifecycle = new RuntimeGraphReloadLifecycle({
|
||||
@@ -7141,17 +7143,17 @@ export class OrcaRuntimeService {
|
||||
if (
|
||||
typeof rendererGeneration === 'string' &&
|
||||
rendererGeneration === this.rendererGeneration &&
|
||||
this.graphStatus === 'reloading'
|
||||
this.graphStatus !== 'ready' &&
|
||||
!this.sameGenerationGraphRecoveryAllowed
|
||||
) {
|
||||
// Why only while reloading: a replacement document is on its way, so the
|
||||
// pre-reload one must not settle the fence. Any other non-ready state has
|
||||
// no successor coming — rejecting the live renderer there stranded the
|
||||
// graph `unavailable` for the rest of the process (STA-5523).
|
||||
// Only an explicit frame-unavailable failure proves the last document may heal;
|
||||
// a reload timeout or process exit must keep its superseded generation fenced.
|
||||
throw new Error('Runtime graph publisher belongs to a superseded renderer generation')
|
||||
}
|
||||
// Why after the fences: a rejected publication used to keep the authority it
|
||||
// took on the way in, pinning the graph to a window that could never publish.
|
||||
this.authoritativeWindowId = windowId
|
||||
this.sameGenerationGraphRecoveryAllowed = false
|
||||
if (windowId === HEADLESS_RUNTIME_WINDOW_ID) {
|
||||
this.headlessGraphFallbackAvailable = true
|
||||
this.rendererGeneration = null
|
||||
@@ -31663,6 +31665,7 @@ export class OrcaRuntimeService {
|
||||
// Why: the rebuilt graph decides whether an incarnation survived; do not stale proven process identities before that comparison.
|
||||
this.rendererGraphEpoch += 1
|
||||
this.graphStatus = 'reloading'
|
||||
this.sameGenerationGraphRecoveryAllowed = false
|
||||
const revision = this.graphReloadLifecycle.begin(windowId)
|
||||
this.setTerminalSideEffectConsumerAvailable(false)
|
||||
this.rememberDetachedPreAllocatedLeaves()
|
||||
@@ -31717,13 +31720,14 @@ export class OrcaRuntimeService {
|
||||
this.pendingHeadlessPromotionWindowId = null
|
||||
}
|
||||
this.graphStatus = 'ready'
|
||||
this.sameGenerationGraphRecoveryAllowed = false
|
||||
this.setTerminalSideEffectConsumerAvailable(windowId !== HEADLESS_RUNTIME_WINDOW_ID)
|
||||
this.refreshWritableFlags()
|
||||
}
|
||||
|
||||
markGraphReloadFailed(
|
||||
windowId: number,
|
||||
_reason: 'renderer-frame-unavailable' | 'renderer-process-gone'
|
||||
reason: 'renderer-frame-unavailable' | 'renderer-process-gone'
|
||||
): void {
|
||||
if (windowId !== this.authoritativeWindowId) {
|
||||
return
|
||||
@@ -31732,6 +31736,7 @@ export class OrcaRuntimeService {
|
||||
this.beginGraphReload(windowId)
|
||||
}
|
||||
this.graphReloadLifecycle.settleActive('failure')
|
||||
this.sameGenerationGraphRecoveryAllowed = reason === 'renderer-frame-unavailable'
|
||||
this.transitionGraphReloadToTerminalState(windowId)
|
||||
}
|
||||
|
||||
@@ -31750,6 +31755,7 @@ export class OrcaRuntimeService {
|
||||
// Why: this runs when the window itself closed, so its renderer is finished
|
||||
// publishing — record that before any branch drops the authority binding.
|
||||
this.retiredGraphWindowId = windowId
|
||||
this.sameGenerationGraphRecoveryAllowed = false
|
||||
this.graphReloadLifecycle.settleActive('cancelled')
|
||||
if (this.shouldRestoreHeadlessGraph(windowId)) {
|
||||
this.pendingHeadlessPromotionWindowId = null
|
||||
@@ -31778,6 +31784,7 @@ export class OrcaRuntimeService {
|
||||
if (windowId !== this.authoritativeWindowId || this.graphStatus !== 'reloading') {
|
||||
return
|
||||
}
|
||||
this.sameGenerationGraphRecoveryAllowed = false
|
||||
this.transitionGraphReloadToTerminalState(windowId)
|
||||
}
|
||||
|
||||
@@ -31808,6 +31815,7 @@ export class OrcaRuntimeService {
|
||||
this.authoritativeWindowId = HEADLESS_RUNTIME_WINDOW_ID
|
||||
this.graphStatus = 'ready'
|
||||
this.rendererGeneration = null
|
||||
this.sameGenerationGraphRecoveryAllowed = false
|
||||
this.setTerminalSideEffectConsumerAvailable(false)
|
||||
this.tabs.clear()
|
||||
this.leaves.clear()
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
* heal a terminally-failed graph — leaving paired mobile clients on an empty
|
||||
* tab inventory for the rest of the desktop process's life.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { RUNTIME_GRAPH_RELOAD_TIMEOUT_MS } from './runtime-graph-reload-lifecycle'
|
||||
import { HEADLESS_RUNTIME_WINDOW_ID } from '../../shared/runtime-types'
|
||||
|
||||
const WINDOW_ID = 7
|
||||
const OTHER_WINDOW_ID = 9
|
||||
@@ -41,6 +43,10 @@ function desktopGraph(generation: string): Parameters<OrcaRuntimeService['syncWi
|
||||
}
|
||||
|
||||
describe('runtime graph publication recovery', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('lets the still-live renderer republish after a terminal reload failure', () => {
|
||||
const runtime = createRuntime()
|
||||
runtime.attachWindow(WINDOW_ID)
|
||||
@@ -76,6 +82,37 @@ describe('runtime graph publication recovery', () => {
|
||||
expect(runtime.getStatus()).toMatchObject({ graphStatus: 'reloading' })
|
||||
})
|
||||
|
||||
it('still rejects the pre-reload document after its replacement times out', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = createRuntime()
|
||||
runtime.attachWindow(WINDOW_ID)
|
||||
runtime.syncWindowGraph(WINDOW_ID, desktopGraph('renderer-a'))
|
||||
runtime.markRendererReloading(WINDOW_ID)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(RUNTIME_GRAPH_RELOAD_TIMEOUT_MS)
|
||||
expect(runtime.getStatus()).toMatchObject({ graphStatus: 'unavailable' })
|
||||
|
||||
expect(() => runtime.syncWindowGraph(WINDOW_ID, desktopGraph('renderer-a'))).toThrow(
|
||||
'superseded renderer generation'
|
||||
)
|
||||
runtime.syncWindowGraph(WINDOW_ID, desktopGraph('renderer-b'))
|
||||
expect(runtime.getStatus()).toMatchObject({ graphStatus: 'ready' })
|
||||
})
|
||||
|
||||
it('still rejects the pre-reload document after its renderer exits', () => {
|
||||
const runtime = createRuntime()
|
||||
runtime.attachWindow(WINDOW_ID)
|
||||
runtime.syncWindowGraph(WINDOW_ID, desktopGraph('renderer-a'))
|
||||
runtime.markRendererReloading(WINDOW_ID)
|
||||
runtime.markGraphReloadFailed(WINDOW_ID, 'renderer-process-gone')
|
||||
|
||||
expect(() => runtime.syncWindowGraph(WINDOW_ID, desktopGraph('renderer-a'))).toThrow(
|
||||
'superseded renderer generation'
|
||||
)
|
||||
runtime.syncWindowGraph(WINDOW_ID, desktopGraph('renderer-b'))
|
||||
expect(runtime.getStatus()).toMatchObject({ graphStatus: 'ready' })
|
||||
})
|
||||
|
||||
it('refuses a late publication from a window whose graph was retired', () => {
|
||||
const runtime = createRuntime()
|
||||
runtime.attachWindow(WINDOW_ID)
|
||||
@@ -95,4 +132,19 @@ describe('runtime graph publication recovery', () => {
|
||||
graphStatus: 'ready'
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses a late publication from a retired headless-promotion window', () => {
|
||||
const runtime = createRuntime()
|
||||
runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] })
|
||||
runtime.attachWindow(WINDOW_ID)
|
||||
runtime.markGraphUnavailable(WINDOW_ID)
|
||||
|
||||
expect(() => runtime.syncWindowGraph(WINDOW_ID, desktopGraph('renderer-a'))).toThrow(
|
||||
'pending desktop promotion'
|
||||
)
|
||||
expect(runtime.getStatus()).toMatchObject({
|
||||
authoritativeWindowId: HEADLESS_RUNTIME_WINDOW_ID,
|
||||
graphStatus: 'ready'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user