Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/1.docs/50.tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Prefer `scheduledTasks` when your deployment target supports it (see [platform s

### Platform support

- **`dev`**, **`node_server`**, **`node_cluster`**, **`node_middleware`**, **`bun`** and **`deno_server`** presets are supported by the [croner](https://croner.56k.guru/) engine.
- **`dev`**, **`node_server`**, **`node_cluster`**, **`node_middleware`**, **`bun`** and **`deno_server`** presets are supported by the [croner](https://croner.56k.guru/) engine. Schedules stop when the Nitro `close` hook runs. With `node_middleware`, Nitro does not own the server, so call `useNitroHooks().callHook("close")` when your host server shuts down.
- **`cloudflare_module`** and **`cloudflare_pages`** presets have native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Nitro automatically generates the cron triggers in the wrangler config at build time - no manual wrangler setup required.
- **`vercel`** preset has native integration with [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs). Nitro automatically generates the cron job configuration at build time - no manual `vercel.json` setup required. You can secure cron endpoints by setting the `CRON_SECRET` environment variable.
- More presets (with native primitives support) are planned to be supported!
Expand Down
38 changes: 26 additions & 12 deletions src/runtime/internal/task.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Cron } from "croner";
import { HTTPError } from "h3";
import type { Task, TaskContext, TaskEvent, TaskPayload, TaskResult } from "nitro/types";
import { useNitroHooks } from "./app.ts";
import { scheduledTasks, tasks } from "#nitro/virtual/tasks";

/** @experimental */
Expand Down Expand Up @@ -64,19 +65,32 @@ export function startScheduleRunner({
scheduledTime: Date.now(),
};

const cronJobs: Cron[] = [];
useNitroHooks().hook("close", () => {
for (const job of cronJobs) {
try {
job.stop();
} catch (error) {
console.error("Error while stopping scheduled task", error);
}
}
});

for (const schedule of scheduledTasks) {
new Cron(schedule.cron, async () => {
await Promise.all(
schedule.tasks.map((name) =>
runTask(name, {
payload,
context: { waitUntil },
}).catch((error) => {
console.error(`Error while running scheduled task "${name}"`, error);
})
)
);
});
cronJobs.push(
new Cron(schedule.cron, async () => {
await Promise.all(
schedule.tasks.map((name) =>
runTask(name, {
payload,
context: { waitUntil },
}).catch((error) => {
console.error(`Error while running scheduled task "${name}"`, error);
})
)
);
})
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

Expand Down
81 changes: 81 additions & 0 deletions test/unit/task.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const { closeHookMock, cronMock, cronStopMock } = vi.hoisted(() => {
const cronStopMock = vi.fn();
return {
closeHookMock: vi.fn(),
cronMock: vi.fn(function () {
return { stop: cronStopMock };
}),
cronStopMock,
};
});

vi.mock("croner", () => ({ Cron: cronMock }));
vi.mock("../../src/runtime/internal/app.ts", () => ({
useNitroHooks: () => ({ hook: closeHookMock }),
}));
vi.mock("#nitro/virtual/tasks", () => ({
scheduledTasks: [
{ cron: "*/5 * * * *", tasks: ["test"] },
{ cron: "*/10 * * * *", tasks: ["test"] },
],
tasks: {},
}));

import { startScheduleRunner } from "../../src/runtime/internal/task.ts";

describe("startScheduleRunner", () => {
beforeEach(() => {
vi.stubEnv("TEST", "");
vi.clearAllMocks();
});

afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

it("creates a Cron job for each schedule", () => {
startScheduleRunner();

expect(cronMock).toHaveBeenCalledTimes(2);
expect(cronMock).toHaveBeenCalledWith("*/5 * * * *", expect.any(Function));
expect(cronMock).toHaveBeenCalledWith("*/10 * * * *", expect.any(Function));
});

it("stops the schedule runner when the Nitro app closes", () => {
startScheduleRunner();

expect(closeHookMock).toHaveBeenCalledWith("close", expect.any(Function));

const closeScheduleRunner = closeHookMock.mock.calls[0]![1];
closeScheduleRunner();

expect(cronStopMock).toHaveBeenCalledTimes(2);
});

it("continues stopping schedules when one Cron job fails to stop", () => {
const error = new Error("stop failed");
const firstStopMock = vi.fn(() => {
throw error;
});
const secondStopMock = vi.fn();
const consoleErrorMock = vi.spyOn(console, "error").mockImplementation(() => {});
cronMock
.mockImplementationOnce(function () {
return { stop: firstStopMock };
})
.mockImplementationOnce(function () {
return { stop: secondStopMock };
});

startScheduleRunner();
const closeScheduleRunner = closeHookMock.mock.calls[0]![1];

expect(() => closeScheduleRunner()).not.toThrow();
expect(firstStopMock).toHaveBeenCalledOnce();
expect(secondStopMock).toHaveBeenCalledOnce();
expect(consoleErrorMock).toHaveBeenCalledWith("Error while stopping scheduled task", error);
});
});
Loading