fix(agent-jobs): stop heartbeat from resurrecting terminated jobs
This commit is contained in:
@@ -1223,6 +1223,7 @@ export const recoverStaleJobs = internalMutation({
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
await ctx.db.patch(job._id, {
|
await ctx.db.patch(job._id, {
|
||||||
status: 'timed_out',
|
status: 'timed_out',
|
||||||
|
workspaceStatus: 'expired',
|
||||||
error:
|
error:
|
||||||
job.error ??
|
job.error ??
|
||||||
'The worker stopped reporting progress; the job timed out.',
|
'The worker stopped reporting progress; the job timed out.',
|
||||||
@@ -1512,12 +1513,15 @@ export const heartbeatWorkspace = mutation({
|
|||||||
if (job?.claimedBy !== args.workerId) {
|
if (job?.claimedBy !== args.workerId) {
|
||||||
throw new ConvexError('Agent job not claimed by this worker.');
|
throw new ConvexError('Agent job not claimed by this worker.');
|
||||||
}
|
}
|
||||||
|
if (isTerminalStatus(job.status)) {
|
||||||
|
return { success: true, cancelRequested: true };
|
||||||
|
}
|
||||||
await ctx.db.patch(args.jobId, {
|
await ctx.db.patch(args.jobId, {
|
||||||
workspaceStatus: 'active',
|
workspaceStatus: 'active',
|
||||||
lastHeartbeatAt: Date.now(),
|
lastHeartbeatAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
});
|
});
|
||||||
return { success: true, cancelRequested: job.status === 'cancelled' };
|
return { success: true, cancelRequested: false };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ describe('recoverStaleJobs', () => {
|
|||||||
|
|
||||||
expect(result).toEqual({ recovered: 1 });
|
expect(result).toEqual({ recovered: 1 });
|
||||||
expect(state.job?.status).toBe('timed_out');
|
expect(state.job?.status).toBe('timed_out');
|
||||||
|
expect(state.job?.workspaceStatus).toBe('expired');
|
||||||
expect(state.job?.completedAt).toBeTypeOf('number');
|
expect(state.job?.completedAt).toBeTypeOf('number');
|
||||||
expect(state.request?.status).toBe('failed');
|
expect(state.request?.status).toBe('failed');
|
||||||
expect(state.thread?.status).toBe('failed');
|
expect(state.thread?.status).toBe('failed');
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const spoonInput = {
|
|||||||
|
|
||||||
const seedJob = async (
|
const seedJob = async (
|
||||||
t: ReturnType<typeof convexTest>,
|
t: ReturnType<typeof convexTest>,
|
||||||
status: 'cancelled' | 'running',
|
status: 'cancelled' | 'running' | 'timed_out',
|
||||||
) =>
|
) =>
|
||||||
await t.mutation(async (ctx) => {
|
await t.mutation(async (ctx) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -109,7 +109,7 @@ describe('updateStatus terminal guard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('heartbeatWorkspace cancellation propagation', () => {
|
describe('heartbeatWorkspace cancellation propagation', () => {
|
||||||
test('reports cancellation while refreshing the workspace heartbeat', async () => {
|
test('requests teardown for a cancelled job without resurrecting it', async () => {
|
||||||
const t = convexTest(schema, modules);
|
const t = convexTest(schema, modules);
|
||||||
const jobId = await seedJob(t, 'cancelled');
|
const jobId = await seedJob(t, 'cancelled');
|
||||||
|
|
||||||
@@ -122,8 +122,25 @@ describe('heartbeatWorkspace cancellation propagation', () => {
|
|||||||
expect(result).toEqual({ success: true, cancelRequested: true });
|
expect(result).toEqual({ success: true, cancelRequested: true });
|
||||||
const job = await t.run(async (ctx) => await ctx.db.get(jobId));
|
const job = await t.run(async (ctx) => await ctx.db.get(jobId));
|
||||||
expect(job?.status).toBe('cancelled');
|
expect(job?.status).toBe('cancelled');
|
||||||
expect(job?.workspaceStatus).toBe('active');
|
expect(job?.workspaceStatus).not.toBe('active');
|
||||||
expect(job?.lastHeartbeatAt).toBeTypeOf('number');
|
expect(job?.lastHeartbeatAt).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requests teardown for a timed_out job without resurrecting it', async () => {
|
||||||
|
const t = convexTest(schema, modules);
|
||||||
|
const jobId = await seedJob(t, 'timed_out');
|
||||||
|
|
||||||
|
const result = await t.mutation(api.agentJobs.heartbeatWorkspace, {
|
||||||
|
workerToken: 'test-worker-token',
|
||||||
|
workerId: 'worker-1',
|
||||||
|
jobId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: true, cancelRequested: true });
|
||||||
|
const job = await t.run(async (ctx) => await ctx.db.get(jobId));
|
||||||
|
expect(job?.status).toBe('timed_out');
|
||||||
|
expect(job?.workspaceStatus).not.toBe('active');
|
||||||
|
expect(job?.lastHeartbeatAt).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reports no cancellation for an active job', async () => {
|
test('reports no cancellation for an active job', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user