Skip to content

Commit b40b22a

Browse files
committed
fix(run-engine): correct park deadline and snapshot state for debounced parked runs
Two defects surface when a run parked on an external deployment id is pushed by a debounce key. Both were reproduced against a local instance before fixing. 1. The run is expired before it is due. The park deadline is armed once, when the run is first parked, from max(now, delayUntil) + deadline. Debounce pushes delayUntil out afterwards: rescheduleDelayedRun reschedules enqueueDelayedRun:<id>, and the redis-worker reschedule is an update-only ZADD, so expireParkedExternalDeploymentRun:<id> is never re-armed. Repeat triggers on one key walk delayUntil past a deadline that no longer moves, and the run is expired with EXTERNAL_DEPLOYMENT_NOT_FOUND before it was ever due to start. Observed: a run due at 14:01:37 expired at 13:57:02. The expiry job already loads delayUntil, so it now re-arms from the current value and returns instead of expiring a run that is not due. Putting the guard there rather than in the debounce path covers every caller that moves delayUntil, and it stays bounded by the debounce max-duration contract. 2. The run reports itself as delayed while it is parked. rescheduleRun hardcoded a DELAYED/DELAYED execution snapshot, so a debounce push left the run row on PENDING_VERSION while its latest snapshot claimed DELAYED, and the run page described a parked run as delayed. The snapshot statuses are now supplied by the caller and default to DELAYED, so the ordinary delayed path is unchanged, and rescheduleDelayedRun passes the parked statuses through when the run is parked.
1 parent 0110494 commit b40b22a

5 files changed

Lines changed: 154 additions & 3 deletions

File tree

internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export class DelayedRunSystem {
4848
throw new ServiceValidationError("Cannot reschedule a run that is not delayed");
4949
}
5050

51+
const isParked = snapshot.runStatus === "PENDING_VERSION";
52+
5153
const updatedRun = await this.$.runStore.rescheduleRun(
5254
runId,
5355
{
@@ -57,6 +59,13 @@ export class DelayedRunSystem {
5759
environmentType: snapshot.environmentType,
5860
projectId: snapshot.projectId,
5961
organizationId: snapshot.organizationId,
62+
...(isParked
63+
? {
64+
executionStatus: "RUN_CREATED" as const,
65+
runStatus: "PENDING_VERSION" as const,
66+
description: "Parked run was rescheduled to a future date",
67+
}
68+
: {}),
6069
},
6170
},
6271
prisma

internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,22 @@ export class PendingVersionSystem {
470470
);
471471
}
472472

473+
if (run.delayUntil && run.delayUntil > new Date()) {
474+
this.$.logger.info(
475+
"expireParkedExternalDeploymentRun: run is not due yet, re-arming the park deadline",
476+
{ runId, externalDeploymentId, delayUntil: run.delayUntil }
477+
);
478+
479+
await this.scheduleExternalDeploymentParkDeadline({
480+
runId,
481+
externalDeploymentId,
482+
ttl: run.ttl,
483+
delayUntil: run.delayUntil,
484+
});
485+
486+
return;
487+
}
488+
473489
const error: TaskRunError = {
474490
type: "STRING_ERROR",
475491
raw: `Run expired because no deployment with external id '${externalDeploymentId}' became available`,

internal-packages/run-engine/src/engine/tests/externalDeploymentParking.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,128 @@ describe("RunEngine external deployment parking", () => {
566566
}
567567
);
568568

569+
containerTest(
570+
"a debounce push on a parked run keeps the snapshot parked instead of reporting it delayed",
571+
async ({ prisma, redisOptions }) => {
572+
const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
573+
const engine = createEngine(prisma, redisOptions);
574+
575+
try {
576+
const taskIdentifier = "test-task";
577+
578+
const run = await engine.trigger(
579+
{
580+
number: 1,
581+
friendlyId: "run_1234",
582+
environment: authenticatedEnvironment,
583+
taskIdentifier,
584+
payload: "{}",
585+
payloadType: "application/json",
586+
context: {},
587+
traceContext: {},
588+
traceId: "t1234",
589+
spanId: "s1234",
590+
queue: `task/${taskIdentifier}`,
591+
isTest: false,
592+
tags: [],
593+
delayUntil: new Date(Date.now() + 60 * 1000),
594+
annotations: {
595+
triggerSource: "sdk",
596+
triggerAction: "trigger",
597+
rootTriggerSource: "sdk",
598+
externalDeploymentId: "commit-snapshot",
599+
},
600+
parkedOnExternalDeploymentId: "commit-snapshot",
601+
},
602+
prisma
603+
);
604+
605+
await engine.delayedRunSystem.rescheduleDelayedRun({
606+
runId: run.id,
607+
delayUntil: new Date(Date.now() + 10 * 60 * 1000),
608+
tx: prisma,
609+
});
610+
611+
const snapshots = await prisma.taskRunExecutionSnapshot.findMany({
612+
where: { runId: run.id },
613+
orderBy: { createdAt: "asc" },
614+
select: { executionStatus: true, runStatus: true },
615+
});
616+
617+
const latest = snapshots.at(-1);
618+
619+
assertNonNullable(latest);
620+
expect(latest.runStatus).toBe("PENDING_VERSION");
621+
expect(latest.executionStatus).toBe("RUN_CREATED");
622+
623+
const stillParked = await prisma.taskRun.findFirstOrThrow({ where: { id: run.id } });
624+
expect(stillParked.status).toBe("PENDING_VERSION");
625+
} finally {
626+
await engine.quit();
627+
}
628+
}
629+
);
630+
631+
containerTest(
632+
"the parking deadline re-arms instead of expiring a run whose delay was pushed past it",
633+
async ({ prisma, redisOptions }) => {
634+
const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
635+
const engine = createEngine(prisma, redisOptions);
636+
637+
try {
638+
const taskIdentifier = "test-task";
639+
640+
const run = await engine.trigger(
641+
{
642+
number: 1,
643+
friendlyId: "run_1234",
644+
environment: authenticatedEnvironment,
645+
taskIdentifier,
646+
payload: "{}",
647+
payloadType: "application/json",
648+
context: {},
649+
traceContext: {},
650+
traceId: "t1234",
651+
spanId: "s1234",
652+
queue: `task/${taskIdentifier}`,
653+
isTest: false,
654+
tags: [],
655+
delayUntil: new Date(Date.now() + 60 * 1000),
656+
annotations: {
657+
triggerSource: "sdk",
658+
triggerAction: "trigger",
659+
rootTriggerSource: "sdk",
660+
externalDeploymentId: "commit-pushed",
661+
},
662+
parkedOnExternalDeploymentId: "commit-pushed",
663+
},
664+
prisma
665+
);
666+
667+
// Stand in for a debounce push: the run's delay moves out, but nothing re-arms the
668+
// deadline that was computed when the run was first parked.
669+
const pushedDelayUntil = new Date(Date.now() + 60 * 60 * 1000);
670+
await prisma.taskRun.update({
671+
where: { id: run.id },
672+
data: { delayUntil: pushedDelayUntil },
673+
});
674+
675+
await engine.pendingVersionSystem.expireParkedExternalDeploymentRun({
676+
runId: run.id,
677+
externalDeploymentId: "commit-pushed",
678+
});
679+
680+
const stillParked = await prisma.taskRun.findFirstOrThrow({ where: { id: run.id } });
681+
682+
expect(stillParked.status).toBe("PENDING_VERSION");
683+
expect(stillParked.statusReason).toBe("EXTERNAL_DEPLOYMENT_PENDING");
684+
expect(stillParked.expiredAt).toBeNull();
685+
} finally {
686+
await engine.quit();
687+
}
688+
}
689+
);
690+
569691
containerTest(
570692
"the parking deadline expires a run whose deployment never arrived",
571693
async ({ prisma, redisOptions }) => {

internal-packages/run-store/src/PostgresRunStore.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,9 +1439,10 @@ export class PostgresRunStore implements RunStore {
14391439
executionSnapshots: {
14401440
create: {
14411441
engine: "V2",
1442-
executionStatus: "DELAYED",
1443-
description: "Delayed run was rescheduled to a future date",
1444-
runStatus: "DELAYED",
1442+
executionStatus: data.snapshot.executionStatus ?? "DELAYED",
1443+
description:
1444+
data.snapshot.description ?? "Delayed run was rescheduled to a future date",
1445+
runStatus: data.snapshot.runStatus ?? "DELAYED",
14451446
environmentId: data.snapshot.environmentId,
14461447
environmentType: data.snapshot.environmentType,
14471448
projectId: data.snapshot.projectId,

internal-packages/run-store/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export type RescheduleSnapshotInput = {
7979
environmentType: RuntimeEnvironmentType;
8080
projectId: string;
8181
organizationId: string;
82+
executionStatus?: TaskRunExecutionStatus;
83+
runStatus?: TaskRunStatus;
84+
description?: string;
8285
};
8386

8487
export type LockSnapshotInput = {

0 commit comments

Comments
 (0)