mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 08:02:54 +00:00
fix(persistence): fence duplicate lease rollback
This commit is contained in:
+13
-3
@@ -7353,10 +7353,16 @@ export class Store {
|
||||
lease.worktreeId === winner.worktreeId &&
|
||||
// Leaf only: a lease freezes its tabId, and a pane broken out to a new tab would otherwise
|
||||
// never compete with its own predecessor — which is the reported cardinality growth.
|
||||
lease.leafId === winner.leafId &&
|
||||
lease.state !== 'terminated' &&
|
||||
lease.state !== 'expired'
|
||||
lease.leafId === winner.leafId
|
||||
) {
|
||||
if (lease.state === 'expired') {
|
||||
// A concurrent same-pane owner confirms this lease must outlive rollback as retired.
|
||||
this.advanceSshRemotePtyLeaseMutationVersion(lease)
|
||||
continue
|
||||
}
|
||||
if (lease.state === 'terminated') {
|
||||
continue
|
||||
}
|
||||
lease.state = 'expired'
|
||||
lease.updatedAt = now
|
||||
this.advanceSshRemotePtyLeaseMutationVersion(lease)
|
||||
@@ -7678,6 +7684,10 @@ export class Store {
|
||||
continue
|
||||
}
|
||||
if (state === 'detached' && lease.state !== 'attached') {
|
||||
if (lease.state === 'expired') {
|
||||
// A later detach fences rollback from restoring attached ownership.
|
||||
this.advanceSshRemotePtyLeaseMutationVersion(lease)
|
||||
}
|
||||
continue
|
||||
}
|
||||
this.advanceSshRemotePtyLeaseMutationVersion(lease)
|
||||
|
||||
@@ -315,6 +315,63 @@ describe('STA-3077: existing duplicate leases are healed, not revived', () => {
|
||||
).toBe('attached')
|
||||
})
|
||||
|
||||
it('does not revive a provisional loser after a concurrent same-pane upsert', async () => {
|
||||
const store = await createStore({
|
||||
sshRemotePtyLeases: [
|
||||
{ ...leaseFor('relay-pty-a', 1), createdAt: 1 },
|
||||
{ ...leaseFor('relay-pty-b', 2), createdAt: 2 }
|
||||
]
|
||||
})
|
||||
let rejectWrite: (error: Error) => void = () => {}
|
||||
vi.spyOn(
|
||||
store as unknown as { flushDurableStateOrThrowAsync: () => Promise<void> },
|
||||
'flushDurableStateOrThrowAsync'
|
||||
).mockImplementation(
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
rejectWrite = reject
|
||||
})
|
||||
)
|
||||
|
||||
const retirement = store.supersedeDuplicatePaneLeases(TARGET)
|
||||
store.upsertSshRemotePtyLease({ ...leaseFor('relay-pty-c', 3), createdAt: 3 })
|
||||
rejectWrite(new Error('disk full'))
|
||||
|
||||
expect(await retirement).toBe(0)
|
||||
expect(liveLeasesForPane(store).map((lease) => lease.ptyId)).toEqual(['relay-pty-c'])
|
||||
})
|
||||
|
||||
it('does not restore attached state over a concurrent detach', async () => {
|
||||
const store = await createStore({
|
||||
sshRemotePtyLeases: [
|
||||
{ ...leaseFor('relay-pty-a', 1), createdAt: 1 },
|
||||
{ ...leaseFor('relay-pty-b', 2), createdAt: 2 }
|
||||
]
|
||||
})
|
||||
let rejectWrite: (error: Error) => void = () => {}
|
||||
vi.spyOn(
|
||||
store as unknown as { flushDurableStateOrThrowAsync: () => Promise<void> },
|
||||
'flushDurableStateOrThrowAsync'
|
||||
).mockImplementation(
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
rejectWrite = reject
|
||||
})
|
||||
)
|
||||
|
||||
const retirement = store.supersedeDuplicatePaneLeases(TARGET)
|
||||
store.markSshRemotePtyLeasesForShutdown(TARGET, 'detached')
|
||||
rejectWrite(new Error('disk full'))
|
||||
|
||||
expect(await retirement).toBe(0)
|
||||
expect(
|
||||
store.getSshRemotePtyLeases(TARGET).find((lease) => lease.ptyId === 'relay-pty-a')?.state
|
||||
).toBe('expired')
|
||||
expect(
|
||||
store.getSshRemotePtyLeases(TARGET).find((lease) => lease.ptyId === 'relay-pty-b')?.state
|
||||
).toBe('detached')
|
||||
})
|
||||
|
||||
it('does not roll back concurrent session or lease changes after a failed write', async () => {
|
||||
const store = await createStore({
|
||||
sshRemotePtyLeases: [
|
||||
|
||||
Reference in New Issue
Block a user