Skip to content

Commit 686efad

Browse files
committed
support zero, improve tests, add server-change
1 parent 91d52f1 commit 686efad

9 files changed

Lines changed: 116 additions & 66 deletions

File tree

apps/webapp/app/presenters/v3/ViewSchedulePresenter.server.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type ViewScheduleOptions = {
1313
projectId: string;
1414
friendlyId: string;
1515
environmentId: string;
16+
includeRunHistory?: boolean;
1617
};
1718

1819
export class ViewSchedulePresenter {
@@ -22,7 +23,13 @@ export class ViewSchedulePresenter {
2223
this.#prismaClient = prismaClient;
2324
}
2425

25-
public async call({ userId, projectId, friendlyId, environmentId }: ViewScheduleOptions) {
26+
public async call({
27+
userId,
28+
projectId,
29+
friendlyId,
30+
environmentId,
31+
includeRunHistory = true,
32+
}: ViewScheduleOptions) {
2633
const schedule = await this.#prismaClient.taskSchedule.findFirst({
2734
select: {
2835
id: true,
@@ -79,17 +86,14 @@ export class ViewSchedulePresenter {
7986
? nextScheduledTimestamps(schedule.generatorExpression, schedule.timezone, new Date(), 5)
8087
: [];
8188

82-
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
83-
schedule.project.organizationId,
84-
"standard"
85-
);
86-
const runPresenter = new NextRunListPresenter(this.#prismaClient, clickhouse);
87-
const { runs } = await runPresenter.call(schedule.project.organizationId, environmentId, {
88-
projectId: schedule.project.id,
89-
scheduleId: schedule.id,
90-
pageSize: 5,
91-
period: "31d",
92-
});
89+
const runs = includeRunHistory
90+
? await this.#getRunHistory({
91+
organizationId: schedule.project.organizationId,
92+
environmentId,
93+
projectId: schedule.project.id,
94+
scheduleId: schedule.id,
95+
})
96+
: [];
9397

9498
return {
9599
schedule: {
@@ -110,6 +114,32 @@ export class ViewSchedulePresenter {
110114
};
111115
}
112116

117+
async #getRunHistory({
118+
organizationId,
119+
environmentId,
120+
projectId,
121+
scheduleId,
122+
}: {
123+
organizationId: string;
124+
environmentId: string;
125+
projectId: string;
126+
scheduleId: string;
127+
}) {
128+
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
129+
organizationId,
130+
"standard"
131+
);
132+
const runPresenter = new NextRunListPresenter(this.#prismaClient, clickhouse);
133+
const { runs } = await runPresenter.call(organizationId, environmentId, {
134+
projectId,
135+
scheduleId,
136+
pageSize: 5,
137+
period: "31d",
138+
});
139+
140+
return runs;
141+
}
142+
113143
public toJSONResponse(result: NonNullable<Awaited<ReturnType<ViewSchedulePresenter["call"]>>>) {
114144
const response: ScheduleObject = {
115145
id: result.schedule.friendlyId,

apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
6464
projectId: authenticationResult.environment.projectId,
6565
friendlyId: parsedParams.data.scheduleId,
6666
environmentId: authenticationResult.environment.id,
67+
includeRunHistory: false,
6768
});
6869

6970
if (!result) {

apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
6464
projectId: authenticationResult.environment.projectId,
6565
friendlyId: parsedParams.data.scheduleId,
6666
environmentId: authenticationResult.environment.id,
67+
includeRunHistory: false,
6768
});
6869

6970
if (!result) {

apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
178178
projectId: authenticationResult.environment.projectId,
179179
friendlyId: parsedParams.data.scheduleId,
180180
environmentId: authenticationResult.environment.id,
181+
includeRunHistory: false,
181182
});
182183

183184
if (!result) {

apps/webapp/app/v3/scheduleWindow.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export function formatScheduleWindow({
5454
return undefined;
5555
}
5656

57+
if (windowDurationSeconds === 0) {
58+
return "0m";
59+
}
60+
5761
if (windowDurationSeconds % SECONDS_PER_UNIT.d === 0) {
5862
return `${windowDurationSeconds / SECONDS_PER_UNIT.d}d`;
5963
}

apps/webapp/test/scheduleWindow.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ describe("schedule window persistence", () => {
1111
windowDurationSeconds: 1_800,
1212
windowPercentage: null,
1313
});
14+
expect(normalizeScheduleWindow("0m")).toEqual({
15+
windowDurationSeconds: 0,
16+
windowPercentage: null,
17+
});
1418
expect(normalizeScheduleWindow("30%")).toEqual({
1519
windowDurationSeconds: null,
1620
windowPercentage: 30,
@@ -22,6 +26,12 @@ describe("schedule window persistence", () => {
2226
});
2327

2428
it("formats stored windows canonically", () => {
29+
expect(
30+
formatScheduleWindow({
31+
windowDurationSeconds: 0,
32+
windowPercentage: null,
33+
})
34+
).toBe("0m");
2535
expect(
2636
formatScheduleWindow({
2737
windowDurationSeconds: 86_400,

internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -179,58 +179,52 @@ describe("RunQueue.enqueueMessage fast path", () => {
179179
}
180180
);
181181

182-
redisTest(
183-
"should not fast-path a future-scored message",
184-
async ({ redisContainer }) => {
185-
const queue = createQueue(redisContainer, "runqueue:fp-future-score:");
182+
redisTest("should not fast-path a future-scored message", async ({ redisContainer }) => {
183+
const queue = createQueue(redisContainer, "runqueue:fp-future-score:");
186184

187-
try {
188-
await queue.updateEnvConcurrencyLimits(authenticatedEnvDev);
185+
try {
186+
await queue.updateEnvConcurrencyLimits(authenticatedEnvDev);
189187

190-
const futureMessage: InputPayload = {
191-
...messageDev,
192-
runId: "r_future_score",
193-
timestamp: Date.now() + 60_000,
194-
};
188+
const futureMessage: InputPayload = {
189+
...messageDev,
190+
runId: "r_future_score",
191+
timestamp: Date.now() + 60_000,
192+
};
195193

196-
await queue.enqueueMessage({
197-
env: authenticatedEnvDev,
198-
message: futureMessage,
199-
workerQueue: authenticatedEnvDev.id,
200-
enableFastPath: true,
201-
});
194+
await queue.enqueueMessage({
195+
env: authenticatedEnvDev,
196+
message: futureMessage,
197+
workerQueue: authenticatedEnvDev.id,
198+
enableFastPath: true,
199+
});
202200

203-
const queueLength = await queue.lengthOfQueue(
204-
authenticatedEnvDev,
205-
futureMessage.queue
206-
);
207-
const queueConcurrency = await queue.currentConcurrencyOfQueue(
208-
authenticatedEnvDev,
209-
futureMessage.queue
210-
);
211-
const dequeued = await queue.dequeueMessageFromWorkerQueue(
212-
"test_12345",
213-
authenticatedEnvDev.id,
214-
{ blockingPop: false }
215-
);
201+
const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, futureMessage.queue);
202+
const queueConcurrency = await queue.currentConcurrencyOfQueue(
203+
authenticatedEnvDev,
204+
futureMessage.queue
205+
);
206+
const dequeued = await queue.dequeueMessageFromWorkerQueue(
207+
"test_12345",
208+
authenticatedEnvDev.id,
209+
{ blockingPop: false }
210+
);
216211

217-
expect({
218-
// A future-scored message must remain in the sorted set until it is eligible.
219-
queueLength,
220-
// It must not claim concurrency before it becomes eligible.
221-
queueConcurrency,
222-
// It must not be visible to a worker before its timestamp.
223-
dequeuedMessageId: dequeued?.messageId,
224-
}).toEqual({
225-
queueLength: 1,
226-
queueConcurrency: 0,
227-
dequeuedMessageId: undefined,
228-
});
229-
} finally {
230-
await queue.quit();
231-
}
212+
expect({
213+
// A future-scored message must remain in the sorted set until it is eligible.
214+
queueLength,
215+
// It must not claim concurrency before it becomes eligible.
216+
queueConcurrency,
217+
// It must not be visible to a worker before its timestamp.
218+
dequeuedMessageId: dequeued?.messageId,
219+
}).toEqual({
220+
queueLength: 1,
221+
queueConcurrency: 0,
222+
dequeuedMessageId: undefined,
223+
});
224+
} finally {
225+
await queue.quit();
232226
}
233-
);
227+
});
234228

235229
redisTest("should take slow path when enableFastPath is false", async ({ redisContainer }) => {
236230
const queue = createQueue(redisContainer, "runqueue:fp2:");

internal-packages/schedule-engine/src/engine/scheduleTiming.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ describe("parseScheduleWindow", () => {
1515
["30m", { type: "duration", durationSeconds: 1_800 }],
1616
["2h", { type: "duration", durationSeconds: 7_200 }],
1717
["1d", { type: "duration", durationSeconds: 86_400 }],
18+
["0m", { type: "duration", durationSeconds: 0 }],
19+
["0h", { type: "duration", durationSeconds: 0 }],
20+
["0d", { type: "duration", durationSeconds: 0 }],
1821
["0%", { type: "percentage", percentage: 0 }],
1922
["12%", { type: "percentage", percentage: 12 }],
2023
["100%", { type: "percentage", percentage: 100 }],
@@ -24,7 +27,7 @@ describe("parseScheduleWindow", () => {
2427

2528
it.each([
2629
"",
27-
"0m",
30+
"00m",
2831
"01m",
2932
"1.5h",
3033
"30s",
@@ -51,6 +54,10 @@ describe("schedule window validation", () => {
5154
expect(() => validateScheduleWindow({ type: "percentage", percentage })).not.toThrow();
5255
});
5356

57+
it("allows a zero-duration window", () => {
58+
expect(() => validateScheduleWindow({ type: "duration", durationSeconds: 0 })).not.toThrow();
59+
});
60+
5461
it("allows an absolute window equal to the nominal interval", () => {
5562
expect(() =>
5663
validateScheduleWindowForInterval({ type: "duration", durationSeconds: 300 }, 5 * 60_000)
@@ -64,7 +71,7 @@ describe("schedule window validation", () => {
6471
});
6572

6673
it.each([
67-
{ type: "duration", durationSeconds: 0 },
74+
{ type: "duration", durationSeconds: -1 },
6875
{ type: "duration", durationSeconds: 1.5 },
6976
{ type: "percentage", percentage: -100 },
7077
{ type: "percentage", percentage: 101 },

internal-packages/schedule-engine/src/engine/scheduleTiming.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ export type EffectiveScheduleTime = {
3131
/**
3232
* Parses the public schedule-window syntax.
3333
*
34-
* Durations are positive whole minutes, hours, or days. Percentages are whole
35-
* numbers from 0% through 100%.
34+
* Durations are non-negative whole minutes, hours, or days. Percentages are
35+
* whole numbers from 0% through 100%.
3636
*/
3737
export function parseScheduleWindow(value: string): NormalizedScheduleWindow {
38-
const durationMatch = /^([1-9]\d*)([mhd])$/.exec(value);
38+
const durationMatch = /^(0|[1-9]\d*)([mhd])$/.exec(value);
3939

4040
if (durationMatch) {
4141
const amount = Number(durationMatch[1]);
@@ -57,18 +57,20 @@ export function parseScheduleWindow(value: string): NormalizedScheduleWindow {
5757
}
5858

5959
throw new TypeError(
60-
'Schedule window must be a positive duration such as "30m", "2h", or "1d", or a percentage such as "30%"'
60+
'Schedule window must be a whole duration such as "30m", "2h", or "1d", or a percentage such as "30%"'
6161
);
6262
}
6363

6464
export function validateScheduleWindow(window: NormalizedScheduleWindow): void {
6565
if (window.type === "duration") {
6666
if (
6767
!Number.isSafeInteger(window.durationSeconds) ||
68-
window.durationSeconds <= 0 ||
68+
window.durationSeconds < 0 ||
6969
window.durationSeconds > MAX_POSTGRES_INT
7070
) {
71-
throw new RangeError("Schedule window duration must be a positive integer number of seconds");
71+
throw new RangeError(
72+
"Schedule window duration must be a non-negative integer number of seconds"
73+
);
7274
}
7375

7476
return;

0 commit comments

Comments
 (0)