Skip to content

Commit 039c601

Browse files
authored
feat(gatsby): enable Jobs API v2 (#19858)
Added a new action called createJobV2 to support the new api. createJob is still available to keep backward-compatibility. I'll add a deprecation message when creatJobV2 is fully operational. The createJobV2 needs a job object that contains 4 arguments name, inputPaths, outputDir & args. These args are used to create a unique content digest to make sure a job is deterministic. InputPaths are converted into relative paths before sending it to the worker as they need to be filesystem agnostic. More info on why can be found in #19831
1 parent 65a6833 commit 039c601

File tree

28 files changed

+1210
-27
lines changed

28 files changed

+1210
-27
lines changed

e2e-tests/production-runtime/gatsby-config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ module.exports = {
1919
icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
2020
},
2121
},
22+
`gatsby-plugin-local-worker`,
2223
].concat(process.env.TEST_PLUGIN_OFFLINE ? [`gatsby-plugin-offline`] : []),
2324
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const path = require(`path`)
2+
3+
exports.onPostBootstrap = async ({ actions, store }) => {
4+
const result = await actions.createJobV2({
5+
name: `TEST_JOB`,
6+
inputPaths: [],
7+
outputDir: path.join(store.getState().program.directory, `./public`),
8+
args: {
9+
result: `hello`,
10+
},
11+
})
12+
13+
if (result.result !== `hello`) {
14+
throw new Error(`the result of our worker is wrong`)
15+
}
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout))
2+
3+
exports.TEST_JOB = async ({ args }) => {
4+
await sleep(5000)
5+
6+
return {
7+
result: args.result,
8+
}
9+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// noop
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "gatsby-plugin-local-worker",
3+
"version": "1.0.0"
4+
}

packages/gatsby/index.d.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,12 @@ export interface Actions {
804804

805805
/** @see https://www.gatsbyjs.org/docs/actions/#createPage */
806806
createPage<TContext = Record<string, unknown>>(
807-
args: { path: string; matchPath?: string; component: string; context: TContext },
807+
args: {
808+
path: string
809+
matchPath?: string
810+
component: string
811+
context: TContext
812+
},
808813
plugin?: ActionPlugin,
809814
option?: ActionOptions
810815
): void
@@ -878,6 +883,17 @@ export interface Actions {
878883
plugin?: ActionPlugin
879884
): void
880885

886+
/** @see https://www.gatsbyjs.org/docs/actions/#createJobV2 */
887+
createJobV2(
888+
job: {
889+
name: string
890+
inputPaths: string[]
891+
outputDir: string
892+
args: Record<string, unknown>
893+
},
894+
plugin?: ActionPlugin
895+
): Promise<unknown>
896+
881897
/** @see https://www.gatsbyjs.org/docs/actions/#setJob */
882898
setJob(
883899
job: Record<string, unknown> & { id: string },

packages/gatsby/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"graphql": "^14.5.8",
8282
"graphql-compose": "^6.3.7",
8383
"graphql-playground-middleware-express": "^1.7.12",
84+
"hasha": "^5.1.0",
8485
"invariant": "^2.2.4",
8586
"is-relative": "^1.0.0",
8687
"is-relative-url": "^3.0.0",
@@ -104,6 +105,7 @@
104105
"opentracing": "^0.14.4",
105106
"optimize-css-assets-webpack-plugin": "^5.0.3",
106107
"parseurl": "^1.3.3",
108+
"p-defer": "^3.0.0",
107109
"physical-cpu-count": "^2.0.0",
108110
"pnp-webpack-plugin": "^1.5.0",
109111
"postcss-flexbugs-fixes": "^3.3.1",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`remove-stale-jobs should enqueue pending jobs 1`] = `
4+
Array [
5+
undefined,
6+
]
7+
`;
8+
9+
exports[`remove-stale-jobs should remove stale jobs from complete cache 1`] = `
10+
Array [
11+
Object {
12+
"payload": Object {
13+
"contentDigest": "1234",
14+
},
15+
"plugin": undefined,
16+
"traceId": undefined,
17+
"type": "REMOVE_STALE_JOB_V2",
18+
},
19+
]
20+
`;
21+
22+
exports[`remove-stale-jobs should remove stale jobs from pending cache 1`] = `
23+
Array [
24+
Object {
25+
"payload": Object {
26+
"contentDigest": "1234",
27+
},
28+
"plugin": undefined,
29+
"traceId": undefined,
30+
"type": "REMOVE_STALE_JOB_V2",
31+
},
32+
]
33+
`;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
jest.mock(`../../utils/jobs-manager`)
2+
3+
const { isJobStale } = require(`../../utils/jobs-manager`)
4+
const { internalActions, publicActions } = require(`../../redux/actions`)
5+
6+
jest.spyOn(internalActions, `removeStaleJob`)
7+
8+
const removeStaleJobs = require(`../remove-stale-jobs`)
9+
10+
describe(`remove-stale-jobs`, () => {
11+
let state
12+
13+
beforeEach(() => {
14+
state = {
15+
jobsV2: {
16+
complete: new Map(),
17+
incomplete: new Map(),
18+
},
19+
}
20+
21+
publicActions.createJobV2 = jest.fn()
22+
internalActions.removeStaleJob.mockClear()
23+
})
24+
25+
it(`should remove stale jobs from complete cache`, () => {
26+
const job = {
27+
inputPaths: [`/src/myfile.js`],
28+
}
29+
30+
state.jobsV2.complete.set(`1234`, job)
31+
32+
isJobStale.mockReturnValue(true)
33+
34+
expect(removeStaleJobs(state)).toMatchSnapshot()
35+
expect(internalActions.removeStaleJob).toHaveBeenCalledTimes(1)
36+
expect(internalActions.removeStaleJob).toHaveBeenCalledWith(`1234`)
37+
expect(publicActions.createJobV2).not.toHaveBeenCalled()
38+
})
39+
40+
it(`should remove stale jobs from pending cache`, () => {
41+
const data = {
42+
job: {
43+
inputPaths: [`/src/myfile.js`],
44+
contentDigest: `1234`,
45+
},
46+
plugin: {
47+
name: `test`,
48+
version: `1.0.0`,
49+
},
50+
}
51+
52+
state.jobsV2.incomplete.set(`1234`, data)
53+
54+
isJobStale.mockReturnValue(true)
55+
56+
expect(removeStaleJobs(state)).toMatchSnapshot()
57+
expect(internalActions.removeStaleJob).toHaveBeenCalledTimes(1)
58+
expect(internalActions.removeStaleJob).toHaveBeenCalledWith(`1234`)
59+
expect(publicActions.createJobV2).not.toHaveBeenCalled()
60+
})
61+
62+
it(`should enqueue pending jobs`, () => {
63+
const data = {
64+
job: {
65+
inputPaths: [`/src/myfile.js`],
66+
contentDigest: `1234`,
67+
},
68+
plugin: {
69+
name: `test`,
70+
version: `1.0.0`,
71+
},
72+
}
73+
74+
state.jobsV2.incomplete.set(`1234`, data)
75+
76+
isJobStale.mockReturnValue(false)
77+
78+
expect(removeStaleJobs(state)).toMatchSnapshot()
79+
expect(internalActions.removeStaleJob).toHaveBeenCalledTimes(0)
80+
expect(publicActions.createJobV2).toHaveBeenCalledTimes(1)
81+
expect(publicActions.createJobV2).toHaveBeenCalledWith(
82+
data.job,
83+
data.plugin
84+
)
85+
})
86+
})

packages/gatsby/src/bootstrap/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const report = require(`gatsby-cli/lib/reporter`)
1919
const getConfigFile = require(`./get-config-file`)
2020
const tracer = require(`opentracing`).globalTracer()
2121
const preferDefault = require(`./prefer-default`)
22+
const removeStaleJobs = require(`./remove-stale-jobs`)
2223
// Add `util.promisify` polyfill for old node versions
2324
require(`util.promisify/shim`)()
2425

@@ -160,6 +161,9 @@ module.exports = async (args: BootstrapArgs) => {
160161

161162
activity.end()
162163

164+
// run stale jobs
165+
store.dispatch(removeStaleJobs(store.getState()))
166+
163167
activity = report.activityTimer(`load plugins`, { parentSpan: bootstrapSpan })
164168
activity.start()
165169
const flattenedPlugins = await loadPlugins(config, program.directory)
@@ -229,7 +233,7 @@ module.exports = async (args: BootstrapArgs) => {
229233
.createHash(`md5`)
230234
.update(JSON.stringify(pluginVersions.concat(hashes)))
231235
.digest(`hex`)
232-
let state = store.getState()
236+
const state = store.getState()
233237
const oldPluginsHash = state && state.status ? state.status.PLUGINS_HASH : ``
234238

235239
// Check if anything has changed. If it has, delete the site's .cache
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { isJobStale } = require(`../utils/jobs-manager`)
2+
const { publicActions, internalActions } = require(`../redux/actions`)
3+
4+
module.exports = state => {
5+
const actions = []
6+
7+
// If any of our finished jobs are stale we remove them to keep our cache small
8+
state.jobsV2.complete.forEach((job, contentDigest) => {
9+
if (isJobStale(job)) {
10+
actions.push(internalActions.removeStaleJob(contentDigest))
11+
}
12+
})
13+
14+
// If any of our pending jobs do not have an existing inputPath or the inputPath changed
15+
// we remove it from the queue as they would fail anyway
16+
state.jobsV2.incomplete.forEach(({ job, plugin }) => {
17+
if (isJobStale(job)) {
18+
actions.push(internalActions.removeStaleJob(job.contentDigest))
19+
} else {
20+
actions.push(publicActions.createJobV2(job, plugin))
21+
}
22+
})
23+
24+
return actions
25+
}

packages/gatsby/src/commands/build.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const queryUtil = require(`../query`)
1616
const appDataUtil = require(`../utils/app-data`)
1717
const WorkerPool = require(`../utils/worker/pool`)
1818
const { structureWebpackErrors } = require(`../utils/webpack-error-utils`)
19+
const {
20+
waitUntilAllJobsComplete: waitUntilAllJobsV2Complete,
21+
} = require(`../utils/jobs-manager`)
1922

2023
type BuildArgs = {
2124
directory: string,
@@ -25,8 +28,8 @@ type BuildArgs = {
2528
openTracingConfigFile: string,
2629
}
2730

28-
const waitJobsFinished = () =>
29-
new Promise((resolve, reject) => {
31+
const waitUntilAllJobsComplete = () => {
32+
const jobsV1Promise = new Promise(resolve => {
3033
const onEndJob = () => {
3134
if (store.getState().jobs.active.length === 0) {
3235
resolve()
@@ -37,6 +40,12 @@ const waitJobsFinished = () =>
3740
onEndJob()
3841
})
3942

43+
return Promise.all([
44+
jobsV1Promise,
45+
waitUntilAllJobsV2Complete(),
46+
]).then(() => {})
47+
}
48+
4049
module.exports = async function build(program: BuildArgs) {
4150
const publicDir = path.join(program.directory, `public`)
4251
initTracer(program.openTracingConfigFile)
@@ -128,9 +137,13 @@ module.exports = async function build(program: BuildArgs) {
128137
`BOOTSTRAP_QUERY_RUNNING_FINISHED`
129138
)
130139

131-
await waitJobsFinished()
140+
await db.saveState()
141+
142+
await waitUntilAllJobsComplete()
132143

144+
// we need to save it again to make sure our latest state has been saved
133145
await db.saveState()
146+
134147
const pagePaths = [...store.getState().pages.keys()]
135148
activity = report.createProgress(
136149
`Building static HTML for pages`,
@@ -176,6 +189,9 @@ module.exports = async function build(program: BuildArgs) {
176189
parentSpan: buildSpan,
177190
})
178191

192+
// Make sure we saved the latest state so we have all jobs cached
193+
await db.saveState()
194+
179195
report.info(`Done building in ${process.uptime()} sec`)
180196

181197
buildSpan.finish()

packages/gatsby/src/commands/develop.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import {
5454
structureWebpackErrors,
5555
} from "../utils/webpack-error-utils"
5656

57+
import { waitUntilAllJobsComplete as waitUntilAllJobsV2Complete } from "../utils/jobs-manager"
58+
5759
interface ICert {
5860
keyPath: string
5961
certPath: string
@@ -75,6 +77,24 @@ interface IProgram {
7577
ssl?: ICert
7678
}
7779

80+
const waitUntilAllJobsComplete = (): Promise<void> => {
81+
const jobsV1Promise = new Promise(resolve => {
82+
const onEndJob = (): void => {
83+
if (store.getState().jobs.active.length === 0) {
84+
resolve()
85+
emitter.off(`END_JOB`, onEndJob)
86+
}
87+
}
88+
emitter.on(`END_JOB`, onEndJob)
89+
onEndJob()
90+
})
91+
92+
return Promise.all([
93+
jobsV1Promise,
94+
waitUntilAllJobsV2Complete(),
95+
]).then(() => {})
96+
}
97+
7898
// const isInteractive = process.stdout.isTTY
7999

80100
// Watch the static directory and copy files to public as they're added or
@@ -88,18 +108,6 @@ onExit(() => {
88108
telemetry.trackCli(`DEVELOP_STOP`)
89109
})
90110

91-
const waitJobsFinished = (): Promise<void> =>
92-
new Promise(resolve => {
93-
const onEndJob = (): void => {
94-
if (store.getState().jobs.active.length === 0) {
95-
resolve()
96-
emitter.off(`END_JOB`, onEndJob)
97-
}
98-
}
99-
emitter.on(`END_JOB`, onEndJob)
100-
onEndJob()
101-
})
102-
103111
type ActivityTracker = any // TODO: Replace this with proper type once reporter is typed
104112

105113
interface IServer {
@@ -421,7 +429,9 @@ module.exports = async (program: IProgram): Promise<void> => {
421429
`BOOTSTRAP_QUERY_RUNNING_FINISHED`
422430
)
423431

424-
await waitJobsFinished()
432+
await db.saveState()
433+
434+
await waitUntilAllJobsComplete()
425435
requiresWriter.startListener()
426436
db.startAutosave()
427437
queryUtil.startListeningToDevelopQueue()
@@ -457,9 +467,9 @@ module.exports = async (program: IProgram): Promise<void> => {
457467
})
458468

459469
const isUnspecifiedHost = host === `0.0.0.0` || host === `::`
460-
let prettyHost = host,
461-
lanUrlForConfig,
462-
lanUrlForTerminal
470+
let prettyHost = host
471+
let lanUrlForConfig
472+
let lanUrlForTerminal
463473
if (isUnspecifiedHost) {
464474
prettyHost = `localhost`
465475

0 commit comments

Comments
 (0)