Skip to content

Commit

Permalink
feat: experimental scheduled tasks (#2179)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Feb 27, 2024
1 parent 79d641c commit 32f1e6e
Show file tree
Hide file tree
Showing 19 changed files with 238 additions and 17 deletions.
58 changes: 56 additions & 2 deletions docs/1.guide/10.tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,46 @@ export default defineTask({
> [!NOTE]
> Use `server/tasks/db/migrate.ts` for Nuxt.
## Run tasks

To execute tasks, you can use `runTask(name, { payload? })` utility.
## Scheduled tasks

You can define scheduled tasks using Nitro configuration to automatically run after each period of time.

::code-group
```ts [nitro.config.ts]
export default defineNitroConfig({
tasks: {
// Run `cms:update` task every minute
'* * * * *': ['cms:update']
}
})
```

```ts [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
tasks: {
// Run `cms:update` task every minute
'* * * * *': ['cms:update']
}
}
})
```

::

> [!TIP]
> You can use [crontab.guru](https://crontab.guru/) to easily generate and understand cron tab patterns.
### Platform support

- `dev`, `node-server`, `bun` and `deno-server` presets are supported with [croner](https://croner.56k.guru/) engine.
- `cloudflare_module` and `cloudflare_pages` preset have native integraton with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Make sure to configure wrangler to use exactly same patterns you define in `scheduledTasks` to be matched.
- More presets (with native primitives support) are planned to be supported!

## Programmatically run tasks

To manually run tasks, you can use `runTask(name, { payload? })` utility.

**Example:**

Expand Down Expand Up @@ -91,7 +128,15 @@ This endpoint returns a list of available task names and their meta.
"tasks": {
"db:migrate": {
"description": "Run database migrations"
},
"cms:update": {
"description": "Update CMS content"
}
},
"scheduledTasks": {
"* * * * *": [
"cms:update"
]
}
}
```
Expand Down Expand Up @@ -123,3 +168,12 @@ nitro task list
```sh
nitro task run db:migrate --payload "{}"
```

## Notes

### Concurrency

Each task can have **one running instance**. Calling a task of same name multiple times in parallel, results to calling it once and all callers will get the same return value.

> [!NOTE]
> Nitro tasks can be running multiple times and in parralel.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@
"citty": "^0.1.6",
"consola": "^3.2.3",
"cookie-es": "^1.0.0",
"db0": "^0.1.2",
"croner": "^8.0.1",
"crossws": "^0.2.3",
"db0": "^0.1.2",
"defu": "^6.1.4",
"destr": "^2.0.3",
"dot-prop": "^8.0.2",
Expand Down Expand Up @@ -126,7 +127,6 @@
"unwasm": "^0.3.7"
},
"devDependencies": {
"better-sqlite3": "^9.4.3",
"@azure/functions": "^3.5.1",
"@azure/static-web-apps-cli": "^1.1.6",
"@cloudflare/workers-types": "^4.20240222.0",
Expand All @@ -140,6 +140,7 @@
"@types/serve-static": "^1.15.5",
"@vitest/coverage-v8": "^1.3.1",
"automd": "^0.3.6",
"better-sqlite3": "^9.4.3",
"changelogen": "^0.5.5",
"edge-runtime": "^2.5.9",
"eslint": "^8.57.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export const nitroRuntimeDependencies = [
"unenv",
"unstorage",
"crossws",
"croner",
];
29 changes: 25 additions & 4 deletions src/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export async function createNitro(
}
}

// Tasks
// Scan Tasks
if (nitro.options.experimental.tasks) {
const scannedTasks = await scanTasks(nitro);
for (const scannedTask of scannedTasks) {
Expand All @@ -124,7 +124,28 @@ export async function createNitro(
}
}

nitro.options.virtual["#internal/nitro/virtual/tasks"] = () => `
// Virtual module for tasks (TODO: Move to rollup plugin)
nitro.options.virtual["#internal/nitro/virtual/tasks"] = () => {
const _scheduledTasks = Object.entries(nitro.options.scheduledTasks || {})
.map(([cron, _tasks]) => {
const tasks = (Array.isArray(_tasks) ? _tasks : [_tasks]).filter(
(name) => {
if (!nitro.options.tasks[name]) {
nitro.logger.warn(`Scheduled task \`${name}\` is not defined!`);
return false;
}
return true;
}
);
return { cron, tasks };
})
.filter((e) => e.tasks.length > 0);
const scheduledTasks: false | { cron: string; tasks: string[] }[] =
_scheduledTasks.length > 0 ? _scheduledTasks : false;

return /* js */ `
export const scheduledTasks = ${JSON.stringify(scheduledTasks)};
export const tasks = {
${Object.entries(nitro.options.tasks)
.map(
Expand All @@ -141,8 +162,8 @@ export const tasks = {
}`
)
.join(",\n")}
};
`;
};`;
};

// Auto imports
if (nitro.options.imports) {
Expand Down
1 change: 1 addition & 0 deletions src/rollup/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export const getRollupConfig = (nitro: Nitro): RollupConfig => {
// Internal
_asyncContext: nitro.options.experimental.asyncContext,
_websocket: nitro.options.experimental.websocket,
_tasks: nitro.options.experimental.tasks,
};

// Universal import.meta
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/entries/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "#internal/nitro/virtual/polyfill";
import type {} from "bun";
import wsAdapter from "crossws/adapters/bun";
import { nitroApp } from "../app";
import { startScheduleRunner } from "../task";

const ws = import.meta._websocket
? wsAdapter(nitroApp.h3App.websocket)
Expand Down Expand Up @@ -34,3 +35,8 @@ const server = Bun.serve({
});

console.log(`Listening on http://localhost:${server.port}...`);

// Scheduled tasks
if (import.meta._tasks) {
startScheduleRunner();
}
18 changes: 17 additions & 1 deletion src/runtime/entries/cloudflare-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import manifest from "__STATIC_CONTENT_MANIFEST";
import wsAdapter from "crossws/adapters/cloudflare";
import { requestHasBody } from "../utils";
import { nitroApp } from "#internal/nitro/app";
import { useRuntimeConfig } from "#internal/nitro";
import { runCronTasks, runTask, useRuntimeConfig } from "#internal/nitro";
import { getPublicAssetMeta } from "#internal/nitro/virtual/public-assets";

const ws = import.meta._websocket
Expand Down Expand Up @@ -78,6 +78,22 @@ export default {
body,
});
},
scheduled(event: any, env: CFModuleEnv, context: ExecutionContext) {
if (import.meta._tasks) {
globalThis.__env__ = env;
context.waitUntil(
runCronTasks(event.cron, {
context: {
cloudflare: {
env,
context,
},
},
payload: {},
})
);
}
},
};

function assetsCacheControl(_request) {
Expand Down
18 changes: 18 additions & 0 deletions src/runtime/entries/cloudflare-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import "#internal/nitro/virtual/polyfill";
import type {
Request as CFRequest,
EventContext,
ExecutionContext,
} from "@cloudflare/workers-types";
import wsAdapter from "crossws/adapters/cloudflare";
import { runCronTasks } from "../task";
import { requestHasBody } from "#internal/nitro/utils";
import { nitroApp } from "#internal/nitro/app";
import { isPublicAssetURL } from "#internal/nitro/virtual/public-assets";
Expand Down Expand Up @@ -69,4 +71,20 @@ export default {
body,
});
},
scheduled(event: any, env: CFPagesEnv, context: ExecutionContext) {
if (import.meta._tasks) {
globalThis.__env__ = env;
context.waitUntil(
runCronTasks(event.cron, {
context: {
cloudflare: {
env,
context,
},
},
payload: {},
})
);
}
},
};
7 changes: 6 additions & 1 deletion src/runtime/entries/deno-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "#internal/nitro/virtual/polyfill";
import destr from "destr";
import wsAdapter from "crossws/adapters/deno";
import { nitroApp } from "../app";
import { useRuntimeConfig } from "#internal/nitro";
import { startScheduleRunner, useRuntimeConfig } from "#internal/nitro";

if (Deno.env.get("DEBUG")) {
addEventListener("unhandledrejection", (event: any) =>
Expand Down Expand Up @@ -67,4 +67,9 @@ async function handler(request: Request, info: any) {
});
}

// Scheduled tasks
if (import.meta._tasks) {
startScheduleRunner();
}

export default {};
11 changes: 8 additions & 3 deletions src/runtime/entries/nitro-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
import wsAdapter from "crossws/adapters/node";
import { nitroApp } from "../app";
import { trapUnhandledNodeErrors } from "../utils";
import { runTask } from "../task";
import { tasks } from "#internal/nitro/virtual/tasks";
import { runTask, startScheduleRunner } from "../task";
import { tasks, scheduledTasks } from "#internal/nitro/virtual/tasks";

const server = new Server(toNodeListener(nitroApp.h3App));

Expand Down Expand Up @@ -67,6 +67,7 @@ nitroApp.router.get(
);
return {
tasks: Object.fromEntries(_tasks),
scheduledTasks,
};
})
);
Expand All @@ -91,10 +92,14 @@ trapUnhandledNodeErrors();
async function onShutdown(signal?: NodeJS.Signals) {
await nitroApp.hooks.callHook("close");
}

parentPort.on("message", async (msg) => {
if (msg && msg.event === "shutdown") {
await onShutdown();
parentPort.postMessage({ event: "exit" });
}
});

// Scheduled tasks
if (import.meta._tasks) {
startScheduleRunner();
}
7 changes: 6 additions & 1 deletion src/runtime/entries/node-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import wsAdapter from "crossws/adapters/node";
import { nitroApp } from "../app";
import { setupGracefulShutdown } from "../shutdown";
import { trapUnhandledNodeErrors } from "../utils";
import { useRuntimeConfig } from "#internal/nitro";
import { startScheduleRunner, useRuntimeConfig } from "#internal/nitro";

const cert = process.env.NITRO_SSL_CERT;
const key = process.env.NITRO_SSL_KEY;
Expand Down Expand Up @@ -58,4 +58,9 @@ if (import.meta._websocket) {
server.on("upgrade", handleUpgrade);
}

// Scheduled tasks
if (import.meta._tasks) {
startScheduleRunner();
}

export default {};
6 changes: 6 additions & 0 deletions src/runtime/entries/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "#internal/nitro/virtual/polyfill";
import { toNodeListener } from "h3";
import { nitroApp } from "../app";
import { trapUnhandledNodeErrors } from "../utils";
import { startScheduleRunner } from "../task";

export const listener = toNodeListener(nitroApp.h3App);

Expand All @@ -15,3 +16,8 @@ export const handler = listener;

// Trap unhandled errors
trapUnhandledNodeErrors();

// Scheduled tasks
if (import.meta._tasks) {
startScheduleRunner();
}
Loading

0 comments on commit 32f1e6e

Please sign in to comment.