Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(jobs): Mostly nitpicky changes to the Jobs PR #11328

Merged
merged 10 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
38 changes: 19 additions & 19 deletions docs/docs/background-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A typical create-user flow could look something like this:

![jobs-before](/img/background-jobs/jobs-before.png)

If we want the email to be send asynchronously, we can shuttle that process off into a **background job**:
If we want the email to be sent asynchronously, we can shuttle that process off into a **background job**:

![jobs-after](/img/background-jobs/jobs-after.png)

Expand All @@ -18,7 +18,7 @@ Each job is completely self-contained and has everything it needs to perform its

### Overview

There are three components to the background job system in Redwood:
There are three components to the Background Job system in Redwood:

1. Scheduling
2. Storage
Expand Down Expand Up @@ -135,7 +135,7 @@ To get started with jobs, run the setup command:
yarn rw setup jobs
```

This will add a new model to your Prisma schema, and create a configuration file at `api/src/lib/jobs.js` (or `.ts` for a Typescript project). You'll need to run migrations in order to actually create the model in your database:
This will add a new model to your Prisma schema, and create a configuration file at `api/src/lib/jobs.js` (or `.ts` for a TypeScript project). You'll need to run migrations in order to actually create the model in your database:

```bash
yarn rw prisma migrate dev
Expand Down Expand Up @@ -317,7 +317,7 @@ Because we're using the `PrismaAdapter` here all jobs are stored in the database

:::

The `handler` column contains the name of the job, file path to find it, and the arguments its `perform()` function will receive. Where did the `name` and `path` come from? We have a babel plugin that adds them to your job when they are built.
The `handler` column contains the name of the job, file path to find it, and the arguments its `perform()` function will receive. Where did the `name` and `path` come from? We have a babel plugin that adds them to your job when they are built!

:::warning Jobs Must Be Built

Expand All @@ -339,7 +339,7 @@ The runner is a sort of overseer that doesn't do any work itself, but spawns wor

It checks the `BackgroundJob` table every few seconds for a new job and, if it finds one, locks it so that no other workers can have it, then calls your `perform()` function, passing it the arguments you gave when you scheduled it.

If the job succeeds then by default it's removed the database (using the `PrismaAdapter`, other adapters behavior may vary). If the job fails, the job is un-locked in the database, the `runAt` is set to an incremental backoff time in the future, and `lastError` is updated with the error that occurred. The job will now be picked up in the future once the `runAt` time has passed and it'll try again.
If the job succeeds then by default it's removed from the database (using the `PrismaAdapter`, other adapters behavior may vary). If the job fails, the job is un-locked in the database, the `runAt` is set to an incremental backoff time in the future, and `lastError` is updated with the error that occurred. The job will now be picked up in the future once the `runAt` time has passed and it'll try again.

To stop the runner (and the workers it started), press `Ctrl-C` (or send `SIGINT`). The workers will gracefully shut down, waiting for their work to complete before exiting. If you don't wait to wait, hit `Ctrl-C` again (or send `SIGTERM`),

Expand All @@ -349,7 +349,7 @@ There are a couple of additional modes that `rw jobs` can run in:
yarn rw jobs workoff
```

This mode will execute all jobs that eligible to run, then stop itself.
This mode will execute all jobs that are eligible to run, then stop itself.

```bash
yarn rw jobs start
Expand All @@ -367,7 +367,7 @@ The rest of this doc describes more advanced usage, like:
- Starting more than one worker
- Having some workers focus on only certain queues
- Configuring individual workers to use different adapters
- Manually workers without the job runner monitoring them
- Manually start workers without the job runner monitoring them
- And more!

## Configuration
Expand Down Expand Up @@ -434,7 +434,7 @@ export const jobs = new JobManager({
})
```

- `db`: **[required]** an instance of `PrismaClient` that the adapter will use to store, find and update the status of jobs. In most cases this will be the `db` variable exported from `api/src/lib/db.js`. This must be set in order for the adapter to be initialized!
- `db`: **[required]** an instance of `PrismaClient` that the adapter will use to store, find and update the status of jobs. In most cases this will be the `db` variable exported from `api/src/lib/db.{js,ts}`. This must be set in order for the adapter to be initialized!
- `model`: the name of the model that was created to store jobs. This defaults to `BackgroundJob`.
- `logger`: events that occur within the adapter will be logged using this. This defaults to `console` but the `logger` exported from `api/src/lib/logger` works great.

Expand Down Expand Up @@ -482,8 +482,8 @@ export const SendWelcomeEmailJob = jobs.createJob({
})
```

- `queue` : **[required]** the name of the queue that this job will be placed in. Must be one of the strings you assigned to `queues` array when you set up the `JobManager`.
- `priority` : within a queue you can have jobs that are more or less important. The workers will pull jobs off the queue with a higher priority before working on ones with a lower priority. A lower number is _higher_ in priority than a higher number. ie. the workers will work on a job with a priority of `1` before they work on one with a priority of `100`. If you don't override it here, the default priority is `50`.
- `queue` : **[required]** the name of the queue that this job will be placed in. Must be one of the strings you assigned to the `queues` array when you set up the `JobManager`.
- `priority` : within a queue you can have jobs that are more or less important. The workers will pull jobs off the queue with a higher priority before working on ones with a lower priority. A lower number is _higher_ in priority than a higher number. Ie. the workers will work on a job with a priority of `1` before they work on one with a priority of `100`. If you don't override it here, the default priority is `50`.

### Worker Config

Expand All @@ -501,7 +501,7 @@ export const jobs = new JobManager({
maxAttempts: 24,
maxRuntime: 14_400,
deleteFailedJobs: true,
deleteFailedJobs: false,
deleteSuccessfulJobs: false,
sleepDelay: 5,
},
],
Expand All @@ -518,7 +518,7 @@ This is an array of objects. Each object represents the config for a single "gro
- `maxRuntime` : the maximum amount of time, in seconds, to try running a job before another worker will pick it up and try again. It's up to you to make sure your job doesn't run for longer than this amount of time! Default: `14_400` (4 hours).
- `deleteFailedJobs` : when a job has failed (maximum number of retries has occurred) you can keep the job in the database, or delete it. Default: `false`.
- `deleteSuccessfulJobs` : when a job has succeeded, you can keep the job in the database, or delete it. It's generally assumed that your jobs _will_ succeed so it usually makes sense to clear them out and keep the queue lean. Default: `true`.
- `sleepDelay` : the amount of time, in seconds, to check the queue for another job to run. Too low and you'll be thrashing your storage system looking for jobs, too high and you start to have a long delay before any job is run. Default: `5`.
- `sleepDelay` : the amount of time, in seconds, to wait before checkng the queue for another job to run. Too low and you'll be thrashing your storage system looking for jobs, too high and you start to have a long delay before any job is run. Default: `5`.

See the next section for advanced usage examples, like multiple worker groups.

Expand All @@ -536,7 +536,7 @@ These modes are ideal when you're creating a job and want to be sure it runs cor
yarn rw jobs work
```

This process will stay attached the console and continually look for new jobs and execute them as they are found. The log level is set to `debug` by default so you'll see everything. Pressing `Ctrl-C` to cancel the process (sending `SIGINT`) will start a graceful shutdown: the workers will complete any work they're in the middle of before exiting. To cancel immediately, hit `Ctrl-C` again (or send `SIGTERM`) and they'll stop in the middle of what they're doing. Note that this could leave locked jobs in the database, but they will be picked back up again if a new worker starts with the same name as the one that locked the process. They'll also be picked up automatically after `maxRuntime` has expired, even if they are still locked.
This process will stay attached to the console and continually look for new jobs and execute them as they are found. The log level is set to `debug` by default so you'll see everything. Pressing `Ctrl-C` to cancel the process (sending `SIGINT`) will start a graceful shutdown: the workers will complete any work they're in the middle of before exiting. To cancel immediately, hit `Ctrl-C` again (or send `SIGTERM`) and they'll stop in the middle of what they're doing. Note that this could leave locked jobs in the database, but they will be picked back up again if a new worker starts with the same name as the one that locked the process. They'll also be picked up automatically after `maxRuntime` has expired, even if they are still locked.

:::caution Long running jobs

Expand Down Expand Up @@ -649,7 +649,7 @@ export const jobs = new JobManager({
})
```

Here, we have 2 workers working on the "default" queue and 1 worker looking at the "email" queue (which will only try a job once, wait 30 seconds for it to finish, and delete the job if it fails). You can also have different worker groups using different adapters. For example, you may have store and work on some jobs in your database using the `PrismaAdapter` and some jobs/workers using a `RedisAdapter`.
Here, we have 2 workers working on the "default" queue and 1 worker looking at the "email" queue (which will only try a job once, wait 30 seconds for it to finish, and delete the job if it fails). You can also have different worker groups using different adapters. For example, you may store and work on some jobs in your database using the `PrismaAdapter` and some jobs/workers using a `RedisAdapter`.

:::info

Expand Down Expand Up @@ -687,7 +687,7 @@ For maximum reliability you should take a look at the [Advanced Job Workers](#ad

:::info

Of course if you have a process monitor system watching your workers you'll to use the process monitor's version of the `restart` command each time you deploy!
Of course if you have a process monitor system watching your workers you'll want to use the process monitor's version of the `restart` command each time you deploy!

:::

Expand All @@ -710,19 +710,19 @@ The job runner started with `yarn rw jobs start` runs this same command behind t
### Flags

- `--index` : a number that represents the index of the `workers` config array you passed to the `JobManager`. Setting this to `0`, for example, uses the first object in the array to set all config options for the worker.
- `--id` : a number identifier that's set as part of the process name. Starting a worker with `--id=0` and then inspecting your process list will show one running named `rw-job-worker.queue-name.0`. Using `yarn rw-jobs-worker` only ever starts a single instance, so if your config had a `count` of `2` you'd need to run the command twice, once with `--id=0` and a second time with `--id=1`.
- `--id` : a number identifier that's set as part of the process name. Starting a worker with `--id=0` and then inspecting your process list will show one worker running named `rw-job-worker.queue-name.0`. Using `yarn rw-jobs-worker` only ever starts a single instance, so if your config had a `count` of `2` you'd need to run the command twice, once with `--id=0` and a second time with `--id=1`.
- `--workoff` : a boolean that will execute all currently available jobs and then cause the worker to exit. Defaults to `false`
- `--clear` : a boolean that starts a worker to remove all jobs from all queues. Defaults to `false`

Your process monitor can now restart the workers automatically if they crash since the monitor using the worker script itself and not the wrapping job runner.
Your process monitor can now restart the workers automatically if they crash since the monitor is using the worker script itself and not the wrapping job runner.

### What Happens if a Worker Crashes?

If a worker crashes because of circumstances outside of your control the job will remained locked in the storage system: the worker couldn't finish work and clean up after itself. When this happens, the job will be picked up again immediately if a new worker starts with the same process title, otherwise when `maxRuntime` has passed it's eligible for any worker to pick up and re-lock.

## Creating Your Own Adapter

We'd love the community to contribute adapters for Redwood Job! Take a look at the source for `BaseAdapter` for what's absolutely required, and then the source for `PrismaAdapter` to see a concrete implementation.
We'd love the community to contribute adapters for Redwood Jobs! Take a look at the source for `BaseAdapter` for what's absolutely required, and then the source for `PrismaAdapter` to see a concrete implementation.

The general gist of the required functions:

Expand All @@ -740,5 +740,5 @@ There's still more to add to background jobs! Our current TODO list:
- More adapters: Redis, SQS, RabbitMQ...
- RW Studio integration: monitor the state of your outstanding jobs
- Baremetal integration: if jobs are enabled, monitor the workers with pm2
- Recurring jobs
- Recurring jobs (like cron jobs)
- Lifecycle hooks: `beforePerform()`, `afterPerform()`, `afterSuccess()`, `afterFailure()`
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import fsPath from 'node:path'

import type { PluginObj, types } from '@babel/core'
import type { PluginObj, types as babelTypes } from '@babel/core'

import { getPaths } from '@redwoodjs/project-config'

// This plugin is responsible for injecting the import path and name of a job
// into the object that is passed to createJob. This is later used by adapters
// and workers to import the job.

export default function ({ types: _t }: { types: typeof types }): PluginObj {
Copy link
Member Author

@Tobbe Tobbe Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following our code standards of only using underscore (_foo) for unused variables

export default function ({ types }: { types: typeof babelTypes }): PluginObj {
const paths = getPaths()
return {
name: 'babel-plugin-redwood-job-path-injector',
Expand Down Expand Up @@ -93,15 +93,15 @@ export default function ({ types: _t }: { types: typeof types }): PluginObj {
}
// Add a property to the object expression
firstArg.properties.push(
_t.objectProperty(
_t.identifier('path'),
_t.stringLiteral(importPathWithoutExtension),
types.objectProperty(
types.identifier('path'),
types.stringLiteral(importPathWithoutExtension),
),
)
firstArg.properties.push(
_t.objectProperty(
_t.identifier('name'),
_t.stringLiteral(importName),
types.objectProperty(
types.identifier('name'),
types.stringLiteral(importName),
),
)
},
Expand Down
8 changes: 3 additions & 5 deletions packages/cli/src/commands/generate/job/__tests__/job.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import path from 'path'

import { describe, it, expect } from 'vitest'

// @ts-expect-error - Jobs is a JavaScript file
import * as jobGenerator from '../job'

// Should be refactored as it's repeated
type WordFilesType = { [key: string]: string }

describe('Single word default files', async () => {
const files: WordFilesType = await jobGenerator.files({
const files = await jobGenerator.files({
name: 'Sample',
queueName: 'default',
tests: true,
Expand Down Expand Up @@ -64,7 +62,7 @@ describe('multi-word files', () => {
})

describe('generation of js files', async () => {
const jsFiles: WordFilesType = await jobGenerator.files({
const jsFiles = await jobGenerator.files({
name: 'Sample',
queueName: 'default',
tests: true,
Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/generate/job/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { prepareForRollback } from '../../../lib/rollback'
import { yargsDefaults } from '../helpers'
import { validateName, templateForComponentFile } from '../helpers'

// Makes sure the name ends up looking like: `WelcomeNotice` even if the user
// called it `welcome-notice` or `welcomeNoticeJob` or anything else
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought "anything else" was maybe a little too over reaching 😆

// Try to make the name end up looking like: `WelcomeNotice` even if the user
// called it `welcome-notice` or `welcomeNoticeJob` or something like that
const normalizeName = (name) => {
return changeCase.pascalCase(name).replace(/Job$/, '')
}
Expand Down Expand Up @@ -107,7 +107,8 @@ export const description = 'Generate a Background Job'
// This could be built using createYargsForComponentGeneration;
// however, functions shouldn't have a `stories` option. createYargs...
// should be reversed to provide `yargsDefaults` as the default configuration
// and accept a configuration such as its CURRENT default to append onto a command.
// and accept a configuration such as its CURRENT default to append onto a
// command.
export const builder = (yargs) => {
yargs
.positional('name', {
Expand Down Expand Up @@ -137,7 +138,8 @@ export const builder = (yargs) => {
)}`,
)

// Add default options, includes '--typescript', '--javascript', '--force', ...
// Add default options. This includes '--typescript', '--javascript',
// '--force', ...
Object.entries(yargsDefaults).forEach(([option, config]) => {
yargs.option(option, config)
})
Expand All @@ -156,7 +158,7 @@ export const handler = async ({ name, force, ...rest }) => {

let queueName = 'default'

// Attempt to read the first queue in the users job config file
// Attempt to read the first queue in the user's job config file
try {
const jobsManagerFile = getPaths().api.distJobsConfig
const jobManager = await import(pathToFileURL(jobsManagerFile).href)
Expand All @@ -170,9 +172,8 @@ export const handler = async ({ name, force, ...rest }) => {
{
title: 'Generating job files...',
task: async () => {
return writeFilesTask(await files({ name, queueName, ...rest }), {
overwriteExisting: force,
})
const jobFiles = await files({ name, queueName, ...rest })
return writeFilesTask(jobFiles, { overwriteExisting: force })
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/setup/jobs/jobsHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const tasks = async ({ force }) => {
fs.mkdirSync(getPaths().api.jobs)
} catch (e) {
// ignore directory already existing
if (!e.message.match('file already exists')) {
if (!/file already exists/.test(e.message)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@cannikin cannikin Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I hate this syntax, it feels backwards to me. In real life I'd say "Is this car red?" Not "Is the color red what this car is?" GROSS If you start with a regex object (because it was passed in as an argument, or you constructed the regex from other variables) you can use test(), but if you start with the string you can use match().

But if it's more performant or whatever, we can make it test() instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't argue with that... I also think it reads backwards 🙁 But it is the recommended way!
BUT I changed the code to pass { recursive: true }, which makes it so that no error is thrown if the directory already exists. So now we don't have to worry about any error messages 🎉

throw new Error(e)
}
}
Expand Down
File renamed without changes.
5 changes: 3 additions & 2 deletions packages/jobs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"dist"
],
"scripts": {
"build": "tsx ./build.mts",
"build": "tsx ./build.ts",
"build:pack": "yarn pack -o redwoodjs-jobs.tgz",
"build:types": "tsc --build --verbose ./tsconfig.build.json",
"build:types-cjs": "tsc --build --verbose ./tsconfig.cjs.json",
Expand All @@ -44,7 +44,8 @@
},
"dependencies": {
"@redwoodjs/cli-helpers": "workspace:*",
"@redwoodjs/project-config": "workspace:*"
"@redwoodjs/project-config": "workspace:*",
"type-fest": "4.24.0"
},
"devDependencies": {
"@prisma/client": "5.18.0",
Expand Down
Loading
Loading