|
|
@@ -1,9 +1,9 @@
|
|
|
---
|
|
|
-title: "Scheduled Tasks"
|
|
|
+title: 'Scheduled Tasks'
|
|
|
showtoc: true
|
|
|
---
|
|
|
|
|
|
-Scheduled tasks are a way of executing some code at pre-defined intervals. There are many examples of work that can be done using scheduled tasks,
|
|
|
+Scheduled tasks are a way of executing some code at pre-defined intervals. There are many examples of work that can be done using scheduled tasks,
|
|
|
such as:
|
|
|
|
|
|
- Generating a sitemap
|
|
|
@@ -11,131 +11,284 @@ such as:
|
|
|
- Sending abandoned cart emails
|
|
|
- Cleaning up old data
|
|
|
|
|
|
-In Vendure you can create scheduled tasks by defining a [standalone script](/guides/developer-guide/stand-alone-scripts/) which can then
|
|
|
-be executed via any scheduling mechanism you like, such as a cron job or similar mechanism provided by your hosting provider.
|
|
|
+Since Vendure v3.3, there is a built-in mechanism which allows you to define scheduled tasks in a convenient and powerful way.
|
|
|
|
|
|
-## Creating a Scheduled Task
|
|
|
+:::info
|
|
|
+All the information on page applies to Vendure v3.3+
|
|
|
+
|
|
|
+For older versions, there is no built-in support for scheduled tasks, but you can
|
|
|
+instead use a [stand-alone script](/guides/developer-guide/stand-alone-scripts/) triggered by a cron job.
|
|
|
+:::
|
|
|
+
|
|
|
+## Setting up the DefaultSchedulerPlugin
|
|
|
|
|
|
-Let's imagine that you have created a plugin that exposes a `SitemapService` which generates a sitemap for your store. You want to run this
|
|
|
-task every night at midnight.
|
|
|
+In your Vendure config, import and add the [DefaultSchedulerPlugin](/reference/typescript-api/scheduled-tasks/default-scheduler-plugin) to your
|
|
|
+plugins array. If you created your project with a version newer than v3.3, this should already be configured.
|
|
|
|
|
|
-First we need to create a standalone script which will run the task. This script will look something like this:
|
|
|
+```ts title="vendure-config.ts"
|
|
|
+import { DefaultSchedulerPlugin, VendureConfig } from '@vendure/core';
|
|
|
|
|
|
-```ts title="scheduled-tasks.ts"
|
|
|
-import { bootstrapWorker, Logger, RequestContextService } from '@vendure/core';
|
|
|
-import { SitemapService } from './plugins/sitemap';
|
|
|
+export const config: VendureConfig = {
|
|
|
+ // ...
|
|
|
+ plugins: [DefaultSchedulerPlugin.init()],
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+When you first add this plugin to your config, you'll need to [generate a migration](/guides/developer-guide/migrations/) because the
|
|
|
+plugin will make use of a new database table in order to guarantee only-once execution of tasks.
|
|
|
|
|
|
-import { config } from './vendure-config';
|
|
|
+You can then start adding tasks. Vendure ships with a task that will clean up old sessions from the database.
|
|
|
+
|
|
|
+:::note
|
|
|
+The `cleanSessionsTask` task is actually configured by default from v3.3+, so normally you won't have to specify this
|
|
|
+manually unless you wish to change any of the default configuration using the `.configure()` method.
|
|
|
+:::
|
|
|
|
|
|
-if (require.main === module) {
|
|
|
- generateSitemap()
|
|
|
- .then(() => process.exit(0))
|
|
|
- .catch(err => {
|
|
|
- Logger.error(err);
|
|
|
- process.exit(1);
|
|
|
+```ts title="vendure-config.ts"
|
|
|
+import { cleanSessionsTask, DefaultSchedulerPlugin, VendureConfig } from '@vendure/core';
|
|
|
+
|
|
|
+export const config: VendureConfig = {
|
|
|
+ // ...
|
|
|
+ schedulerOptions: {
|
|
|
+ tasks: [
|
|
|
+ // Use the task as is
|
|
|
+ cleanSessionsTask,
|
|
|
+ // or further configure the task
|
|
|
+ cleanSessionsTask.configure({
|
|
|
+ // Run the task every day at 3:00am
|
|
|
+ // The default schedule is every day at 00:00am
|
|
|
+ schedule: cron => cron.everyDayAt(3, 0),
|
|
|
+ params: {
|
|
|
+ // How many sessions to process in each batch
|
|
|
+ // Default: 10_000
|
|
|
+ batchSize: 5_000,
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ plugins: [DefaultSchedulerPlugin.init()],
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+## Creating a Scheduled Task
|
|
|
+
|
|
|
+Let's imagine that you have created a `SitemapPlugin` that exposes a `SitemapService` which generates a sitemap for your store. You want to run this
|
|
|
+task every night at midnight.
|
|
|
+
|
|
|
+Inside the plugin, you would first define a new [ScheduledTask](/reference/typescript-api/scheduled-tasks/scheduled-task) instance:
|
|
|
+
|
|
|
+```ts title="/plugins/sitemap/config/generate-sitemap-task.ts"
|
|
|
+import { ScheduledTask, RequestContextService } from '@vendure/core';
|
|
|
+
|
|
|
+import { SitemapService } from '../services/sitemap.service';
|
|
|
+
|
|
|
+export const generateSitemapTask = new ScheduledTask({
|
|
|
+ // Give your task a unique ID
|
|
|
+ id: 'generate-sitemap',
|
|
|
+ // A human-readable description of the task
|
|
|
+ description: 'Generates a sitemap file',
|
|
|
+ // Params can be used to further configure aspects of the
|
|
|
+ // task. They get passed in to the `execute` function as the
|
|
|
+ // second argument.
|
|
|
+ // They can be later modified using the `.configure()` method on the instance
|
|
|
+ params: {
|
|
|
+ shopBaseUrl: 'https://www.myshop.com',
|
|
|
+ },
|
|
|
+ // Define a default schedule. This can be modified using the
|
|
|
+ // `.configure()` method on the instance later.
|
|
|
+ schedule: cron => cron.everyDayAt(0, 0),
|
|
|
+ // This is the function that will be executed per the schedule.
|
|
|
+ async execute(injector, params) {
|
|
|
+ // Using `app.get()` we can grab an instance of _any_ provider defined in the
|
|
|
+ // Vendure core as well as by our plugins.
|
|
|
+ const sitemapService = app.get(SitemapService);
|
|
|
+
|
|
|
+ // For most service methods, we'll need to pass a RequestContext object.
|
|
|
+ // We can use the RequestContextService to create one.
|
|
|
+ const ctx = await app.get(RequestContextService).create({
|
|
|
+ apiType: 'admin',
|
|
|
});
|
|
|
-}
|
|
|
|
|
|
-async function generateSitemap() {
|
|
|
- // This will bootstrap an instance of the Vendure Worker, providing
|
|
|
- // us access to all of the services defined in the Vendure core.
|
|
|
- // (but without the unnecessary overhead of the API layer).
|
|
|
- const { app } = await bootstrapWorker(config);
|
|
|
-
|
|
|
- // Using `app.get()` we can grab an instance of _any_ provider defined in the
|
|
|
- // Vendure core as well as by our plugins.
|
|
|
- const sitemapService = app.get(SitemapService);
|
|
|
-
|
|
|
- // For most service methods, we'll need to pass a RequestContext object.
|
|
|
- // We can use the RequestContextService to create one.
|
|
|
- const ctx = await app.get(RequestContextService).create({
|
|
|
- apiType: 'admin',
|
|
|
- });
|
|
|
-
|
|
|
- await sitemapService.generateSitemap(ctx);
|
|
|
-
|
|
|
- Logger.info(`Completed sitemap generation`);
|
|
|
-}
|
|
|
+ // Here's the actual work we want to perform.
|
|
|
+ const result = await sitemapService.generateSitemap(ctx);
|
|
|
+
|
|
|
+ // The return value from the `execute` function will be available
|
|
|
+ // as the `lastResult` property when viewing tasks.
|
|
|
+ return { result };
|
|
|
+ },
|
|
|
+});
|
|
|
```
|
|
|
|
|
|
-### Schedule the task
|
|
|
+## Using a task
|
|
|
+
|
|
|
+Now that the task has been defined, we need to tell Vendure to use it.
|
|
|
+
|
|
|
+To do so we need to add it to the [schedulerOptions.tasks](/reference/typescript-api/scheduled-tasks/scheduler-options#tasks) array.
|
|
|
+
|
|
|
+### Adding directly in Vendure config
|
|
|
+
|
|
|
+This can be done directly in your Vendure config file:
|
|
|
+
|
|
|
+```ts title="vendure-config.ts"
|
|
|
+import { cleanSessionsTask, DefaultSchedulerPlugin, VendureConfig } from '@vendure/core';
|
|
|
+
|
|
|
+// highlight-next-line
|
|
|
+import { SitemapPlugin, generateSitemapTask } from './plugins/sitemap';
|
|
|
+
|
|
|
+export const config: VendureConfig = {
|
|
|
+ // ...
|
|
|
+ schedulerOptions: {
|
|
|
+ tasks: [
|
|
|
+ cleanSessionsTask,
|
|
|
+ // highlight-start
|
|
|
+ // Here's an example of overriding the
|
|
|
+ // default params using the `configure()` method.
|
|
|
+ generateSitemapTask.configure({
|
|
|
+ params: {
|
|
|
+ shopBaseUrl: 'https://www.shoes.com'
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ // highlight-end
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ plugins: [
|
|
|
+ // highlight-next-line
|
|
|
+ SitemapPlugin,
|
|
|
+ DefaultSchedulerPlugin.init()
|
|
|
+ ],
|
|
|
+};
|
|
|
+```
|
|
|
|
|
|
-Each hosting provider has its own way of scheduling tasks. A common way is to use a cron job.
|
|
|
-For example, to run the above script every night at midnight, you could add the following line to your crontab:
|
|
|
+### Adding in plugin configuration function
|
|
|
+
|
|
|
+An alternative is that a plugin can automatically add the task to the config using the
|
|
|
+plugin's [configuration function](/reference/typescript-api/plugin/vendure-plugin-metadata#configuration), which allows plugins to alter the Vendure config.
|
|
|
+
|
|
|
+This allows a plugin to encapsulate any scheduled tasks so that the plugin consumer only needs to add the plugin, and not worry about
|
|
|
+separately adding the task to the tasks array.
|
|
|
+
|
|
|
+```ts title="src/plugins/sitemap/sitemap.plugin.ts"
|
|
|
+import { VendurePlugin, PluginCommonModule, Type, ScheduledTask, VendureConfig } from '@vendure/core';
|
|
|
+
|
|
|
+import { PLUGIN_OPTIONS } from './constants';
|
|
|
+import { SitemapPluginOptions } from './types';
|
|
|
+import { SitemapService } from './services/sitemap.service';
|
|
|
+import { generateSitemapTask } from './config/generate-sitemap-task';
|
|
|
+
|
|
|
+@VendurePlugin({
|
|
|
+ imports: [PluginCommonModule],
|
|
|
+ providers: [SitemapService],
|
|
|
+ configuration: (config: VendureConfig) => {
|
|
|
+ // highlight-start
|
|
|
+ // Add the task to the schedulerOptions.tasks array
|
|
|
+ config.schedulerOptions.tasks.push(
|
|
|
+ generateSitemapTask.configure({
|
|
|
+ params: {
|
|
|
+ shopBaseUrl: SitemapPlugin.options.shopBaseUrl,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ );
|
|
|
+ // highlight-end
|
|
|
+ return config;
|
|
|
+ },
|
|
|
+})
|
|
|
+export class SitemapPlugin {
|
|
|
+ static options: SitemapPluginOptions;
|
|
|
+
|
|
|
+ static init(options?: SitemapPluginOptions) {
|
|
|
+ this.options = {
|
|
|
+ shopBaseUrl: '',
|
|
|
+ ...(options ?? {}),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
|
|
|
-```bash
|
|
|
-0 0 * * * node /path/to/scheduled-tasks.js
|
|
|
+This plugin can now be consumed like this:
|
|
|
+
|
|
|
+```ts title="vendure-config.ts"
|
|
|
+import { DefaultSchedulerPlugin, VendureConfig } from '@vendure/core';
|
|
|
+
|
|
|
+// highlight-next-line
|
|
|
+import { SitemapPlugin } from './plugins/sitemap';
|
|
|
+
|
|
|
+export const config: VendureConfig = {
|
|
|
+ // ...
|
|
|
+ plugins: [
|
|
|
+ // highlight-start
|
|
|
+ SitemapPlugin.init({
|
|
|
+ shopBaseUrl: 'https://www.shoes.com'
|
|
|
+ }),
|
|
|
+ // highlight-end
|
|
|
+ DefaultSchedulerPlugin.init()
|
|
|
+ ],
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-This will run the script `/path/to/scheduled-tasks.js` every night at midnight.
|
|
|
+## How scheduled tasks work
|
|
|
|
|
|
-### Long-running tasks
|
|
|
+The key problems solved by Vendure's task scheduler are:
|
|
|
|
|
|
-What if the scheduled task does a significant amount of work that would take many minutes to complete? In this case
|
|
|
-you should consider using the [job queue](/guides/developer-guide/worker-job-queue/#using-job-queues-in-a-plugin) to
|
|
|
-execute the work on the worker.
|
|
|
+- Ensuring that a task is only run a single time per scheduled execution, even when you have multiple instances of servers and workers running.
|
|
|
+- Keeping scheduled task work away from the server instances, so that it does not affect API responsiveness.
|
|
|
|
|
|
-Taking the above example, let's now imagine that the `SitemapService` exposes a `triggerGenerate()` method which
|
|
|
-adds a new job to the job queue. The job queue will then execute the task in the background, allowing the scheduled
|
|
|
-task to complete quickly.
|
|
|
+The first problem is handled by the [SchedulerStrategy](/reference/typescript-api/scheduled-tasks/scheduler-strategy), which implements a locking
|
|
|
+mechanism to ensure that the task is executed only once.
|
|
|
|
|
|
-```ts title="scheduled-tasks.ts"
|
|
|
-import { bootstrapWorker, Logger, RequestContextService } from '@vendure/core';
|
|
|
-import { SitemapService } from './plugins/sitemap';
|
|
|
+The second problem is handled by having tasks only executed on worker processes.
|
|
|
|
|
|
-import { config } from './vendure-config';
|
|
|
+## Scheduled tasks vs job queue
|
|
|
|
|
|
-if (require.main === module) {
|
|
|
- generateSitemap()
|
|
|
- .then(() => process.exit(0))
|
|
|
- .catch(err => {
|
|
|
- Logger.error(err);
|
|
|
- process.exit(1);
|
|
|
- });
|
|
|
-}
|
|
|
+There is some overlap between the use of a scheduled task and a [job queue job](/guides/developer-guide/worker-job-queue/). They both perform some
|
|
|
+task on the worker, independent of requests coming in to the server.
|
|
|
|
|
|
-async function generateSitemap() {
|
|
|
- const { app } = await bootstrapWorker(config);
|
|
|
- const sitemapService = app.get(SitemapService);
|
|
|
- const ctx = await app.get(RequestContextService).create({
|
|
|
- apiType: 'admin',
|
|
|
- });
|
|
|
-
|
|
|
- await sitemapService.triggerGenerate(ctx);
|
|
|
+The first difference is that jobs must be triggered explicitly, whereas scheduled tasks are triggered automatically according to the schedule.
|
|
|
|
|
|
- Logger.info(`Sitemap generation triggered`);
|
|
|
-}
|
|
|
-```
|
|
|
+Secondly, jobs are put in a _queue_ and executed once any prior pending jobs have been processed. On the other hand, scheduled tasks are executed
|
|
|
+as soon as the schedule dictates.
|
|
|
+
|
|
|
+It is possible to combine the two: namely, you can define a scheduled task which adds a job to the job queue. This is, for instance, how the
|
|
|
+built-in [cleanSessionsTask](/reference/typescript-api/scheduled-tasks/clean-sessions-task) works. This pattern is something you should
|
|
|
+consider if the scheduled task may take a significant amount of time or resources and you want to let the job queue manage that.
|
|
|
+
|
|
|
+It also has the advantage of giving you a record of results for that work that has been put on the job queue, whereas scheduled tasks
|
|
|
+only record that result of the last execution.
|
|
|
|
|
|
-## Using @nestjs/schedule
|
|
|
|
|
|
-NestJS provides a [dedicated package for scheduling tasks](https://docs.nestjs.com/techniques/task-scheduling), called `@nestjs/schedule`.
|
|
|
+## A note on @nestjs/schedule
|
|
|
+
|
|
|
+NestJS provides a [dedicated package for scheduling tasks](https://docs.nestjs.com/techniques/task-scheduling), called `@nestjs/schedule`.
|
|
|
|
|
|
You can also use this approach to schedule tasks, but you need to aware of a very important caveat:
|
|
|
|
|
|
:::warning
|
|
|
When using `@nestjs/schedule`, any method decorated with the `@Cron()` decorator will run
|
|
|
-on _all_ instances of the application. This means it will run on the server _and_ on the
|
|
|
+on _all_ instances of the application. This means it will run on the server _and_ on the
|
|
|
worker. If you are running multiple instances, then it will run on all instances.
|
|
|
+
|
|
|
+This is the specific issue solved by the built-in ScheduledTask system described above.
|
|
|
+Therefore it is not recommended to use the `@nestjs/schedule` package under normal
|
|
|
+circumstances.
|
|
|
:::
|
|
|
|
|
|
-You can, for instance, inject the ProcessContext into the service and check if the current instance is the worker or the server.
|
|
|
+You can, for instance, inject the [ProcessContext](/reference/typescript-api/common/process-context) into the service and check if the current instance is the worker or the server.
|
|
|
|
|
|
```ts
|
|
|
import { Injectable } from '@nestjs/common';
|
|
|
import { Cron } from '@nestjs/schedule';
|
|
|
|
|
|
-
|
|
|
@Injectable()
|
|
|
export class SitemapService {
|
|
|
constructor(private processContext: ProcessContext) {}
|
|
|
|
|
|
@Cron('0 0 * * *')
|
|
|
async generateSitemap() {
|
|
|
+ // highlight-start
|
|
|
if (this.processContext.isWorker) {
|
|
|
// Only run on the worker
|
|
|
await this.triggerGenerate();
|
|
|
}
|
|
|
+ // highlight-end
|
|
|
}
|
|
|
}
|
|
|
```
|