Skip to content

Commit 6401695

Browse files
committed
fix(mothership): page past live controllers, stop a scan on shutdown, dispose chat pubsub in tests
1 parent 03d1411 commit 6401695

3 files changed

Lines changed: 40 additions & 24 deletions

File tree

‎apps/sim/lib/mothership/async-runs/orphaned-runs.ts‎

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -497,29 +497,38 @@ const ABANDONED_RUN_IDLE_MS = 60_000
497497
* no run is provably abandoned.
498498
*/
499499
export async function findAbandonedRecoverableRuns(limit: number): Promise<UnownedRun[]> {
500-
const candidates: UnownedRun[] = await db
501-
.select(unownedRunColumns)
502-
.from(copilotRuns)
503-
.where(
504-
and(
505-
inArray(copilotRuns.status, UNFINISHED_RUN_STATUSES),
506-
isNotNull(controllerToken),
507-
sql`${copilotRuns.requestContext}->'recovery'->>'kind' = 'interactive_stream'`,
508-
idleFor(ABANDONED_RUN_IDLE_MS),
509-
not(idleFor(ORPHANED_RUN_GRACE_MS)),
510-
not(stopRequested)
500+
const abandoned: UnownedRun[] = []
501+
let after: string | undefined
502+
/** Pages past runs whose controller is alive, so they never crowd out an abandoned one. */
503+
while (abandoned.length < limit) {
504+
const candidates: UnownedRun[] = await db
505+
.select(unownedRunColumns)
506+
.from(copilotRuns)
507+
.where(
508+
and(
509+
inArray(copilotRuns.status, UNFINISHED_RUN_STATUSES),
510+
isNotNull(controllerToken),
511+
sql`${copilotRuns.requestContext}->'recovery'->>'kind' = 'interactive_stream'`,
512+
idleFor(ABANDONED_RUN_IDLE_MS),
513+
not(idleFor(ORPHANED_RUN_GRACE_MS)),
514+
not(stopRequested),
515+
after ? gt(copilotRuns.id, after) : undefined
516+
)
511517
)
512-
)
513-
.orderBy(asc(copilotRuns.updatedAt))
514-
.limit(limit)
515-
if (candidates.length === 0) return []
516-
try {
517-
const locked = await findStreamsHoldingChatLock(candidates)
518-
return candidates.filter((run) => !locked.has(run.streamId))
519-
} catch (error) {
520-
logger.warn('Chat stream ownership is unreadable; no run is taken over this round', {
521-
error: getErrorMessage(error),
522-
})
523-
return []
518+
.orderBy(asc(copilotRuns.id))
519+
.limit(SWEEP_BATCH_SIZE)
520+
if (candidates.length === 0) break
521+
after = candidates[candidates.length - 1].id
522+
try {
523+
const locked = await findStreamsHoldingChatLock(candidates)
524+
abandoned.push(...candidates.filter((run) => !locked.has(run.streamId)))
525+
} catch (error) {
526+
logger.warn('Chat stream ownership is unreadable; no run is taken over this round', {
527+
error: getErrorMessage(error),
528+
})
529+
return []
530+
}
531+
if (candidates.length < SWEEP_BATCH_SIZE) break
524532
}
533+
return abandoned.slice(0, limit)
525534
}

‎apps/sim/lib/mothership/request/application/abandoned-runs.integration.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
revokeExhaustedSimToolExecutions,
8282
upsertAsyncToolCall,
8383
} from '@/lib/mothership/async-runs/repository'
84+
import { chatPubSub } from '@/lib/mothership/chat-status'
8485
import { takeOverAbandonedRuns } from '@/lib/mothership/request/application/abandoned-runs'
8586
import {
8687
createProviderToolCallIdentity,
@@ -324,6 +325,7 @@ describe.runIf(Boolean(redisUrl))('taking over a run whose controller died', ()
324325
await db.delete(permissions).where(eq(permissions.entityId, workspaceId))
325326
await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId))
326327
for (const id of [ownerId, userId]) await db.delete(user).where(eq(user.id, id))
328+
chatPubSub?.dispose()
327329
await closeRedisConnection()
328330
})
329331

‎apps/sim/lib/mothership/request/application/abandoned-runs.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const DELEGATION_TTL_MS = 60_000
2626

2727
let timer: ReturnType<typeof setInterval> | undefined
2828
let running = false
29+
let stopped = false
2930

3031
/**
3132
* Resumes the run as its user, re-checking that user's current access. A user who may no
@@ -77,6 +78,7 @@ export async function takeOverAbandonedRuns(): Promise<void> {
7778
running = true
7879
try {
7980
for (const run of await findAbandonedRecoverableRuns(TAKEOVER_BATCH_SIZE)) {
81+
if (stopped) return
8082
await takeOver(run).catch((error) =>
8183
logger.warn('Abandoned run could not be taken over; a later round retries', {
8284
runId: run.id,
@@ -102,7 +104,10 @@ export function startAbandonedRunTakeover(): void {
102104
)
103105
}, TAKEOVER_INTERVAL_MS)
104106
timer.unref()
105-
const stop = () => clearInterval(timer)
107+
const stop = () => {
108+
stopped = true
109+
clearInterval(timer)
110+
}
106111
process.once('SIGTERM', stop)
107112
process.once('SIGINT', stop)
108113
}

0 commit comments

Comments
 (0)