diff --git a/cli/tests/unit/cron_test.ts b/cli/tests/unit/cron_test.ts index 1d46efdb49..79918ed2f2 100644 --- a/cli/tests/unit/cron_test.ts +++ b/cli/tests/unit/cron_test.ts @@ -89,17 +89,18 @@ Deno.test(function invalidScheduleTest() { Deno.test(function invalidBackoffScheduleTest() { assertThrows( () => - Deno.cron("abc", "*/1 * * * *", () => {}, { - backoffSchedule: [1, 1, 1, 1, 1, 1], - }), + Deno.cron( + "abc", + "*/1 * * * *", + { backoffSchedule: [1, 1, 1, 1, 1, 1] }, + () => {}, + ), TypeError, "Invalid backoff schedule", ); assertThrows( () => - Deno.cron("abc", "*/1 * * * *", () => {}, { - backoffSchedule: [3600001], - }), + Deno.cron("abc", "*/1 * * * *", { backoffSchedule: [3600001] }, () => {}), TypeError, "Invalid backoff schedule", ); @@ -109,16 +110,19 @@ Deno.test(async function tooManyCrons() { const crons: Promise[] = []; const ac = new AbortController(); for (let i = 0; i <= 100; i++) { - const c = Deno.cron(`abc_${i}`, "*/1 * * * *", () => {}, { - signal: ac.signal, - }); + const c = Deno.cron( + `abc_${i}`, + "*/1 * * * *", + { signal: ac.signal }, + () => {}, + ); crons.push(c); } try { assertThrows( () => { - Deno.cron("next-cron", "*/1 * * * *", () => {}, { signal: ac.signal }); + Deno.cron("next-cron", "*/1 * * * *", { signal: ac.signal }, () => {}); }, TypeError, "Too many crons", @@ -133,8 +137,7 @@ Deno.test(async function tooManyCrons() { Deno.test(async function duplicateCrons() { const ac = new AbortController(); - const c = Deno.cron("abc", "*/20 * * * *", () => { - }, { signal: ac.signal }); + const c = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => {}); try { assertThrows( () => Deno.cron("abc", "*/20 * * * *", () => {}), @@ -153,12 +156,12 @@ Deno.test(async function basicTest() { let count = 0; const { promise, resolve } = Promise.withResolvers(); const ac = new AbortController(); - const c = Deno.cron("abc", "*/20 * * * *", () => { + const c = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => { count++; if (count > 5) { resolve(); } - }, { signal: ac.signal }); + }); try { await promise; } finally { @@ -179,18 +182,18 @@ Deno.test(async function multipleCrons() { void >(); const ac = new AbortController(); - const c0 = Deno.cron("abc", "*/20 * * * *", () => { + const c0 = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => { count0++; if (count0 > 5) { resolve0(); } - }, { signal: ac.signal }); - const c1 = Deno.cron("xyz", "*/20 * * * *", () => { + }); + const c1 = Deno.cron("xyz", "*/20 * * * *", { signal: ac.signal }, () => { count1++; if (count1 > 5) { resolve1(); } - }, { signal: ac.signal }); + }); try { await promise0; await promise1; @@ -212,11 +215,16 @@ Deno.test(async function overlappingExecutions() { void >(); const ac = new AbortController(); - const c = Deno.cron("abc", "*/20 * * * *", async () => { - resolve0(); - count++; - await promise1; - }, { signal: ac.signal }); + const c = Deno.cron( + "abc", + "*/20 * * * *", + { signal: ac.signal }, + async () => { + resolve0(); + count++; + await promise1; + }, + ); try { await promise0; } finally { @@ -228,16 +236,44 @@ Deno.test(async function overlappingExecutions() { assertEquals(count, 1); }); -Deno.test(async function retriesWithBackkoffSchedule() { +Deno.test(async function retriesWithBackoffSchedule() { Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "5000"); let count = 0; const ac = new AbortController(); - const c = Deno.cron("abc", "*/20 * * * *", async () => { + const c = Deno.cron("abc", "*/20 * * * *", { + signal: ac.signal, + backoffSchedule: [10, 20], + }, async () => { count += 1; await sleep(10); throw new TypeError("cron error"); - }, { signal: ac.signal, backoffSchedule: [10, 20] }); + }); + try { + await sleep(6000); + } finally { + ac.abort(); + await c; + } + + // The cron should have executed 3 times (1st attempt and 2 retries). + assertEquals(count, 3); +}); + +Deno.test(async function retriesWithBackoffScheduleOldApi() { + Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "5000"); + + let count = 0; + const ac = new AbortController(); + const c = Deno.cron("abc2", "*/20 * * * *", async () => { + count += 1; + await sleep(10); + throw new TypeError("cron error"); + }, { + signal: ac.signal, + backoffSchedule: [10, 20], + }); + try { await sleep(6000); } finally { diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index 3ca8a212aa..5cba27a5e2 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -1334,12 +1334,67 @@ declare namespace Deno { * second, 5 seconds, and 10 seconds delay between each retry. * * @category Cron + * @deprecated Use other {@linkcode cron} overloads instead. This overload + * will be removed in the future. */ export function cron( name: string, schedule: string, handler: () => Promise | void, - options?: { backoffSchedule?: number[]; signal?: AbortSignal }, + options: { backoffSchedule?: number[]; signal?: AbortSignal }, + ): Promise; + + /** **UNSTABLE**: New API, yet to be vetted. + * + * Create a cron job that will periodically execute the provided handler + * callback based on the specified schedule. + * + * ```ts + * Deno.cron("sample cron", "20 * * * *", () => { + * console.log("cron job executed"); + * }); + * ``` + * + * `schedule` is a Unix cron format expression, where time is specified + * using UTC time zone. + * + * @category Cron + */ + export function cron( + name: string, + schedule: string, + handler: () => Promise | void, + ): Promise; + + /** **UNSTABLE**: New API, yet to be vetted. + * + * Create a cron job that will periodically execute the provided handler + * callback based on the specified schedule. + * + * ```ts + * Deno.cron("sample cron", "20 * * * *", { + * backoffSchedule: [10, 20] + * }, () => { + * console.log("cron job executed"); + * }); + * ``` + * + * `schedule` is a Unix cron format expression, where time is specified + * using UTC time zone. + * + * `backoffSchedule` option can be used to specify the retry policy for failed + * executions. Each element in the array represents the number of milliseconds + * to wait before retrying the execution. For example, `[1000, 5000, 10000]` + * means that a failed execution will be retried at most 3 times, with 1 + * second, 5 seconds, and 10 seconds delay between each retry. + * + * @category Cron + */ + export function cron( + name: string, + schedule: string, + options: { backoffSchedule?: number[]; signal?: AbortSignal }, + handler: () => Promise | void, ): Promise; /** **UNSTABLE**: New API, yet to be vetted. diff --git a/ext/cron/01_cron.ts b/ext/cron/01_cron.ts index a615ae34bf..30343905c5 100644 --- a/ext/cron/01_cron.ts +++ b/ext/cron/01_cron.ts @@ -6,8 +6,12 @@ const core = Deno.core; function cron( name: string, schedule: string, - handler: () => Promise | void, - options?: { backoffSchedule?: number[]; signal?: AbortSignal }, + handlerOrOptions1: + | (() => Promise | void) + | ({ backoffSchedule?: number[]; signal?: AbortSignal }), + handlerOrOptions2?: + | (() => Promise | void) + | ({ backoffSchedule?: number[]; signal?: AbortSignal }), ) { if (name === undefined) { throw new TypeError("Deno.cron requires a unique name"); @@ -15,7 +19,20 @@ function cron( if (schedule === undefined) { throw new TypeError("Deno.cron requires a valid schedule"); } - if (handler === undefined) { + + let handler: () => Promise | void; + let options: { backoffSchedule?: number[]; signal?: AbortSignal } | undefined; + + if (typeof handlerOrOptions1 === "function") { + handler = handlerOrOptions1; + if (typeof handlerOrOptions2 === "function") { + throw new TypeError("options must be an object"); + } + options = handlerOrOptions2; + } else if (typeof handlerOrOptions2 === "function") { + handler = handlerOrOptions2; + options = handlerOrOptions1; + } else { throw new TypeError("Deno.cron requires a handler"); } diff --git a/ext/cron/local.rs b/ext/cron/local.rs index 0b6dcae2e9..5f1ace6d11 100644 --- a/ext/cron/local.rs +++ b/ext/cron/local.rs @@ -174,8 +174,11 @@ impl RuntimeState { .map(move |name| (*ts, name.clone())) .collect::>() }) - .map(|(_, name)| { - (name.clone(), self.crons.get(&name).unwrap().next_tx.clone()) + .filter_map(|(_, name)| { + self + .crons + .get(&name) + .map(|c| (name.clone(), c.next_tx.clone())) }) .collect::>() };