mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
fix(ssh): roll back a lease retirement whose durable write fails
`flush()` logs and swallows write errors, so a failed write left these leases retired in memory while disk still called them attached — and the pane bindings scrubbed alongside them stayed scrubbed. Use `flushOrThrow` and restore both the lease states and the affected session partitions when it throws, reporting nothing retired. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
+47
-1
@@ -7326,10 +7326,16 @@ export class Store {
|
||||
const winners = new Set(winnerByPane.values())
|
||||
const now = Date.now()
|
||||
const superseded: SshRemotePtyLease[] = []
|
||||
const restore: (() => void)[] = []
|
||||
for (const lease of live) {
|
||||
if (!lease.worktreeId || !lease.tabId || !lease.leafId || winners.has(lease)) {
|
||||
continue
|
||||
}
|
||||
const { state, updatedAt } = lease
|
||||
restore.push(() => {
|
||||
lease.state = state
|
||||
lease.updatedAt = updatedAt
|
||||
})
|
||||
lease.state = 'expired'
|
||||
lease.updatedAt = now
|
||||
superseded.push(lease)
|
||||
@@ -7337,11 +7343,51 @@ export class Store {
|
||||
if (superseded.length === 0) {
|
||||
return 0
|
||||
}
|
||||
const sessionsBefore = this.cloneSshLeaseBindingSessions(targetId)
|
||||
this.clearSshRemotePtyBindingsForLeases(targetId, superseded)
|
||||
this.flush()
|
||||
try {
|
||||
// Why flushOrThrow: flush() swallows write errors, which would leave these
|
||||
// leases retired in memory but attached on disk for the rest of the session.
|
||||
this.flushOrThrow()
|
||||
} catch (err) {
|
||||
for (const undo of restore) {
|
||||
undo()
|
||||
}
|
||||
this.restoreSshLeaseBindingSessions(targetId, sessionsBefore)
|
||||
console.error('[persistence] Failed to retire duplicate pane leases:', err)
|
||||
return 0
|
||||
}
|
||||
return superseded.length
|
||||
}
|
||||
|
||||
private cloneSshLeaseBindingSessions(targetId: string): {
|
||||
local?: WorkspaceSessionState
|
||||
host?: WorkspaceSessionState
|
||||
} {
|
||||
const host = this.state.workspaceSessionsByHostId?.[toSshExecutionHostId(targetId)]
|
||||
return {
|
||||
...(this.state.workspaceSession
|
||||
? { local: cloneWorkspaceSessionState(this.state.workspaceSession) }
|
||||
: {}),
|
||||
...(host ? { host: cloneWorkspaceSessionState(host) } : {})
|
||||
}
|
||||
}
|
||||
|
||||
private restoreSshLeaseBindingSessions(
|
||||
targetId: string,
|
||||
sessions: { local?: WorkspaceSessionState; host?: WorkspaceSessionState }
|
||||
): void {
|
||||
if (sessions.local) {
|
||||
this.state.workspaceSession = sessions.local
|
||||
}
|
||||
if (sessions.host) {
|
||||
this.state.workspaceSessionsByHostId = {
|
||||
...this.state.workspaceSessionsByHostId,
|
||||
[toSshExecutionHostId(targetId)]: sessions.host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markSshRemotePtyLeases(targetId: string, state: SshRemotePtyLease['state']): void {
|
||||
if (this.updateSshRemotePtyLeaseStates(targetId, state)) {
|
||||
this.flush()
|
||||
|
||||
@@ -187,6 +187,23 @@ describe('STA-3077: existing duplicate leases are healed, not revived', () => {
|
||||
expect(liveLeasesForPane(store).map((lease) => lease.ptyId)).toEqual(['relay-pty-bound'])
|
||||
})
|
||||
|
||||
// A retirement that is not durable must not be believed: it would read as
|
||||
// retired in memory and attached on disk for the rest of the session.
|
||||
it('rolls the retirement back when the durable write fails', async () => {
|
||||
const store = await createStore({
|
||||
sshRemotePtyLeases: [
|
||||
{ ...leaseFor('relay-pty-a', 1), createdAt: 1 },
|
||||
{ ...leaseFor('relay-pty-b', 2), createdAt: 2 }
|
||||
]
|
||||
})
|
||||
vi.spyOn(store, 'flushOrThrow').mockImplementation(() => {
|
||||
throw new Error('disk full')
|
||||
})
|
||||
|
||||
expect(store.supersedeDuplicatePaneLeases(TARGET)).toBe(0)
|
||||
expect(liveLeasesForPane(store)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('leaves distinct panes alone', async () => {
|
||||
const otherLeaf = '8a2b4c6d-1e3f-4a5b-8c7d-9e0f1a2b3c4d'
|
||||
const store = await createStore({
|
||||
|
||||
Reference in New Issue
Block a user