Skip to content

Commit 0eac672

Browse files
GatsbyJS Botvladar
GatsbyJS Bot
andauthoredApr 1, 2021
fix(gatsby): fix incorrect intersection of filtered results (#30594) (#30619)
* add failing test * actually failing test * make test independent of other tests * add invariant for filter intersection assumptions * add failing integration test * fix test 🤦‍ * actually fix this heisenbug * update redux state snapshot * perf: mutate status state directly Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com> * only newly created nodes should get a counter * tweak comment Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com> Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com> (cherry picked from commit e432c23) Co-authored-by: Vladimir Razuvaev <vladimir.razuvaev@gmail.com>

File tree

8 files changed

+178
-35
lines changed

8 files changed

+178
-35
lines changed
 

‎integration-tests/artifacts/__tests__/index.js

+53-28
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,39 @@ const path = require(`path`)
33
const { murmurhash } = require(`babel-plugin-remove-graphql-queries`)
44
const { readPageData } = require(`gatsby/dist/utils/page-data`)
55
const { stripIgnoredCharacters } = require(`gatsby/graphql`)
6-
const fs = require(`fs`)
6+
const fs = require(`fs-extra`)
77

88
jest.setTimeout(100000)
99

1010
const publicDir = path.join(process.cwd(), `public`)
1111

1212
const gatsbyBin = path.join(`node_modules`, `.bin`, `gatsby`)
1313

14+
const manifest = {}
15+
16+
function runGatsbyWithRunTestSetup(runNumber = 1) {
17+
return function beforeAllImpl() {
18+
return new Promise(resolve => {
19+
const gatsbyProcess = spawn(gatsbyBin, [`build`], {
20+
stdio: [`inherit`, `inherit`, `inherit`, `inherit`],
21+
env: {
22+
...process.env,
23+
NODE_ENV: `production`,
24+
ARTIFACTS_RUN_SETUP: runNumber.toString(),
25+
},
26+
})
27+
28+
gatsbyProcess.on(`exit`, () => {
29+
manifest[runNumber] = fs.readJSONSync(
30+
path.join(process.cwd(), `.cache`, `build-manifest-for-test-1.json`)
31+
)
32+
33+
resolve()
34+
})
35+
})
36+
}
37+
}
38+
1439
const titleQuery = `
1540
{
1641
site {
@@ -90,6 +115,24 @@ function assertFileExistenceForPagePaths({ pagePaths, type, shouldExist }) {
90115
)
91116
}
92117

118+
function assertNodeCorrectness(runNumber) {
119+
describe(`node correctness`, () => {
120+
it(`nodes do not have repeating counters`, () => {
121+
const seenCounters = new Map()
122+
const duplicates = []
123+
// Just a convenience step to display node ids with duplicate counters
124+
manifest[runNumber].allNodeCounters.forEach(([id, counter]) => {
125+
if (seenCounters.has(counter)) {
126+
duplicates.push({ counter, nodeIds: [id, seenCounters.get(counter)] })
127+
}
128+
seenCounters.set(counter, id)
129+
})
130+
expect(manifest[runNumber].allNodeCounters.length).toBeGreaterThan(0)
131+
expect(duplicates).toEqual([])
132+
})
133+
})
134+
}
135+
93136
beforeAll(async done => {
94137
const gatsbyCleanProcess = spawn(gatsbyBin, [`clean`], {
95138
stdio: [`inherit`, `inherit`, `inherit`, `inherit`],
@@ -105,20 +148,9 @@ beforeAll(async done => {
105148
})
106149

107150
describe(`First run`, () => {
108-
beforeAll(async done => {
109-
const gatsbyProcess = spawn(gatsbyBin, [`build`], {
110-
stdio: [`inherit`, `inherit`, `inherit`, `inherit`],
111-
env: {
112-
...process.env,
113-
NODE_ENV: `production`,
114-
RUN_FOR_STALE_PAGE_ARTIFICATS: `1`,
115-
},
116-
})
151+
const runNumber = 1
117152

118-
gatsbyProcess.on(`exit`, exitCode => {
119-
done()
120-
})
121-
})
153+
beforeAll(runGatsbyWithRunTestSetup(runNumber))
122154

123155
describe(`Static Queries`, () => {
124156
test(`are written correctly when inline`, async () => {
@@ -272,26 +304,17 @@ describe(`First run`, () => {
272304
})
273305
})
274306
})
307+
308+
assertNodeCorrectness(runNumber)
275309
})
276310

277311
describe(`Second run`, () => {
312+
const runNumber = 2
313+
278314
const expectedPages = [`stale-pages/stable`, `stale-pages/only-in-second`]
279315
const unexpectedPages = [`stale-pages/only-in-first`]
280316

281-
beforeAll(async done => {
282-
const gatsbyProcess = spawn(gatsbyBin, [`build`], {
283-
stdio: [`inherit`, `inherit`, `inherit`, `inherit`],
284-
env: {
285-
...process.env,
286-
NODE_ENV: `production`,
287-
RUN_FOR_STALE_PAGE_ARTIFICATS: `2`,
288-
},
289-
})
290-
291-
gatsbyProcess.on(`exit`, exitCode => {
292-
done()
293-
})
294-
})
317+
beforeAll(runGatsbyWithRunTestSetup(runNumber))
295318

296319
describe(`html files`, () => {
297320
const type = `html`
@@ -332,4 +355,6 @@ describe(`Second run`, () => {
332355
})
333356
})
334357
})
358+
359+
assertNodeCorrectness(runNumber)
335360
})

‎integration-tests/artifacts/gatsby-node.js

+49-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,36 @@
1-
const isFirstRun = process.env.RUN_FOR_STALE_PAGE_ARTIFICATS !== `2`
1+
const path = require(`path`)
2+
const fs = require(`fs-extra`)
3+
4+
const runNumber = parseInt(process.env.ARTIFACTS_RUN_SETUP, 10) || 1
5+
6+
const isFirstRun = runNumber === 1
7+
8+
exports.sourceNodes = ({ actions, createContentDigest, reporter, getNode }) => {
9+
reporter.info(`Using test setup #${runNumber}`)
10+
11+
function createNodeHelper(type, nodePartial) {
12+
const node = {
13+
template: `default`,
14+
...nodePartial,
15+
internal: {
16+
type,
17+
contentDigest: createContentDigest(nodePartial),
18+
},
19+
}
20+
actions.createNode(node)
21+
}
22+
23+
for (let prevRun = 1; prevRun < runNumber; prevRun++) {
24+
const node = getNode(`node-created-in-run-${prevRun}`)
25+
if (node) {
26+
actions.touchNode(node)
27+
}
28+
}
29+
createNodeHelper(`NodeCounterTest`, {
30+
id: `node-created-in-run-${runNumber}`,
31+
label: `Node created in run ${runNumber}`,
32+
})
33+
}
234

335
exports.createPages = ({ actions }) => {
436
function createPageHelper(dummyId) {
@@ -22,3 +54,19 @@ exports.createPages = ({ actions }) => {
2254
createPageHelper(`only-in-second`)
2355
}
2456
}
57+
58+
let counter = 1
59+
exports.onPostBuild = ({ getNodes }) => {
60+
console.log(`[test] onPostBuild`)
61+
62+
fs.writeJSONSync(
63+
path.join(
64+
process.cwd(),
65+
`.cache`,
66+
`build-manifest-for-test-${counter++}.json`
67+
),
68+
{
69+
allNodeCounters: getNodes().map(node => [node.id, node.internal.counter]),
70+
}
71+
)
72+
}

‎packages/gatsby/src/redux/__tests__/__snapshots__/index.js.snap

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Object {
7979
"staticQueriesByTemplate": Map {},
8080
"staticQueryComponents": Map {},
8181
"status": Object {
82+
"LAST_NODE_COUNTER": 0,
8283
"PLUGINS_HASH": "",
8384
"plugins": Object {},
8485
},

‎packages/gatsby/src/redux/__tests__/run-fast-filters.js

+52
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const {
33
applyFastFilters,
44
} = require(`../run-fast-filters`)
55
const { store } = require(`../index`)
6+
const { getNode } = require(`../nodes`)
67
const { createDbQueriesFromObject } = require(`../../db/common/query`)
78
const { actions } = require(`../actions`)
89
const {
@@ -459,3 +460,54 @@ describe(`applyFastFilters`, () => {
459460
expect(result.length).toBe(2)
460461
})
461462
})
463+
464+
describe(`edge cases (yay)`, () => {
465+
beforeAll(() => {
466+
store.dispatch({ type: `DELETE_CACHE` })
467+
mockNodes().forEach(node =>
468+
actions.createNode(node, { name: `test` })(store.dispatch)
469+
)
470+
})
471+
472+
it(`throws when node counters are messed up`, () => {
473+
const filter = {
474+
slog: { $eq: `def` }, // matches id_2 and id_4
475+
deep: { flat: { search: { chain: { $eq: 500 } } } }, // matches id_2
476+
}
477+
478+
const result = applyFastFilters(
479+
createDbQueriesFromObject(filter),
480+
[typeName],
481+
new Map()
482+
)
483+
484+
// Sanity-check
485+
expect(result.length).toEqual(1)
486+
expect(result[0].id).toEqual(`id_2`)
487+
488+
// After process restart node.internal.counter is reset and conflicts with counters from the previous run
489+
// in some situations this leads to incorrect intersection of filtered results.
490+
// Below we set node.internal.counter to same value that existing node id_4 has and leads
491+
// to bad intersection of filtered results
492+
const badNode = {
493+
id: `bad-node`,
494+
deep: { flat: { search: { chain: 500 } } },
495+
internal: {
496+
type: typeName,
497+
contentDigest: `bad-node`,
498+
counter: getNode(`id_4`).internal.counter,
499+
},
500+
}
501+
store.dispatch({
502+
type: `CREATE_NODE`,
503+
payload: badNode,
504+
})
505+
506+
const run = () =>
507+
applyFastFilters(createDbQueriesFromObject(filter), [typeName], new Map())
508+
509+
expect(run).toThrow(
510+
`Invariant violation: inconsistent node counters detected`
511+
)
512+
})
513+
})

‎packages/gatsby/src/redux/actions/public.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -531,9 +531,17 @@ actions.deleteNodes = (nodes: any[], plugin: Plugin) => {
531531
return deleteNodesAction
532532
}
533533

534-
// We add a counter to internal to make sure we maintain insertion order for
535-
// backends that don't do that out of the box
536-
let NODE_COUNTER = 0
534+
// We add a counter to node.internal for fast comparisons/intersections
535+
// of various node slices. The counter must increase even across builds.
536+
function getNextNodeCounter() {
537+
const lastNodeCounter = store.getState().status.LAST_NODE_COUNTER ?? 0
538+
if (lastNodeCounter >= Number.MAX_SAFE_INTEGER) {
539+
throw new Error(
540+
`Could not create more nodes. Maximum node count is reached: ${lastNodeCounter}`
541+
)
542+
}
543+
return lastNodeCounter + 1
544+
}
537545

538546
const typeOwners = {}
539547

@@ -633,9 +641,6 @@ const createNode = (
633641
node.internal = {}
634642
}
635643

636-
NODE_COUNTER++
637-
node.internal.counter = NODE_COUNTER
638-
639644
// Ensure the new node has a children array.
640645
if (!node.array && !_.isArray(node.children)) {
641646
node.children = []
@@ -793,6 +798,8 @@ const createNode = (
793798
.map(createDeleteAction)
794799
}
795800

801+
node.internal.counter = getNextNodeCounter()
802+
796803
updateNodeAction = {
797804
...actionOptions,
798805
type: `CREATE_NODE`,

‎packages/gatsby/src/redux/nodes.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,11 @@ export function intersectNodesByCounter(
11041104
} else if (counterA > counterB) {
11051105
pointerB++
11061106
} else {
1107+
if (nodeA !== nodeB) {
1108+
throw new Error(
1109+
`Invariant violation: inconsistent node counters detected`
1110+
)
1111+
}
11071112
// nodeA===nodeB. Make sure we didn't just add this node already.
11081113
// Since input arrays are sorted, the same node should be grouped
11091114
// back to back, so even if both input arrays contained the same node

‎packages/gatsby/src/redux/reducers/status.ts

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ActionsUnion, IGatsbyState } from "../types"
33

44
const defaultState: IGatsbyState["status"] = {
55
PLUGINS_HASH: ``,
6+
LAST_NODE_COUNTER: 0,
67
plugins: {},
78
}
89

@@ -42,6 +43,9 @@ export const statusReducer = (
4243
),
4344
},
4445
}
46+
case `CREATE_NODE`:
47+
state.LAST_NODE_COUNTER = action.payload.internal.counter
48+
return state
4549
default:
4650
return state
4751
}

‎packages/gatsby/src/redux/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export interface IGatsbyState {
208208
status: {
209209
plugins: Record<string, IGatsbyPlugin>
210210
PLUGINS_HASH: Identifier
211+
LAST_NODE_COUNTER: number
211212
}
212213
queries: {
213214
byNode: Map<Identifier, Set<Identifier>>

0 commit comments

Comments
 (0)
Please sign in to comment.