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

feat(gatsby): node persistence #31371

Merged
merged 51 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c74494a
wip: use lmdb-store
vladar May 7, 2021
66ef8b7
fix ts issue
vladar May 11, 2021
2be27a7
remove shared structures key for now for simplicity
vladar May 12, 2021
c4f1141
fix graphql-runner tests
vladar May 12, 2021
6dada20
run CI checks with node 14
vladar May 12, 2021
c331ef6
Use node 14 in windows tests
vladar May 12, 2021
2a6cd08
run wp integration tests on node14
vladar May 12, 2021
658bde8
change cypress image (the previous one fails to record video)
vladar May 12, 2021
98c96d4
temporary skip unrelated failing tests
vladar May 12, 2021
dc6a8cf
temporary skip another unrelated failing tests
vladar May 12, 2021
63695e9
disable node and nodesByType reducers
vladar May 12, 2021
1139ec2
try WP tests with executor: node to confirm the issue is default mach…
vladar May 12, 2021
c2926ba
specify image for WP tests
vladar May 12, 2021
5e5e0a3
Specify data in .cache (#31403)
abhiaiyer91 May 13, 2021
d49d194
Use Array.from for getNodes
vladar May 17, 2021
cc63a44
update lmdb-store
vladar May 17, 2021
f4227f1
Revert "update lmdb-store"
vladar May 17, 2021
181f199
bump lmdb-store
vladar May 17, 2021
27ee42c
bump lmdb-store
vladar May 18, 2021
c4bb247
add GATSBY_EXPERIMENTAL_STRICT_MODE flag
vladar May 25, 2021
a8f99be
Merge branch 'master' into vladar/node-persistence
vladar May 25, 2021
2e2772e
restore state persist
vladar May 25, 2021
540569c
restore skipped tests
vladar May 25, 2021
8be725f
update state persistence tests
vladar May 25, 2021
3450216
re-enable more tests
vladar May 25, 2021
482cca6
re-enable another test
vladar May 25, 2021
5968138
remove comment
vladar May 25, 2021
259d6f4
debug tests
vladar May 25, 2021
1b4c12e
do not rely on named snapshots in tests
vladar May 25, 2021
bb6ef2c
Revert "debug tests"
vladar May 25, 2021
a325e98
revert circleci config
vladar May 25, 2021
a2649cb
circleci: unit tests for strict mode
vladar May 25, 2021
0b7b750
Move lmdb-store to devDependencies
vladar May 26, 2021
dfc7030
try running tests in strict mode
vladar May 26, 2021
e337de7
debug circleci
vladar May 26, 2021
4992627
fix yarn.lock
vladar May 26, 2021
a9252ac
run "yarn rebuild" for strict mode tests
vladar May 26, 2021
1cced53
"yarn rebuild" -> "npm rebuild"
vladar May 26, 2021
a880d40
fix some tests
vladar May 26, 2021
2b8b131
allow setting the strict mode via flags in config
vladar May 26, 2021
29dd70d
fix errors in tests caused by lazy initialization of the store
vladar May 27, 2021
8fddac8
add node counts to datastore
vladar May 27, 2021
f05f40b
renamed flag; moved conditional require calls to datastore initializa…
vladar May 28, 2021
b9488de
cleanup
vladar May 28, 2021
e6479b5
Allow this flag only for Node14.10+
vladar May 28, 2021
38ee163
Address review comments:
vladar May 31, 2021
fb33c7d
fix test
vladar May 31, 2021
d5f885e
move logic from index.ts to datastore.ts
vladar May 31, 2021
6c7bedd
revert unneeded test
vladar May 31, 2021
78c13eb
Change a workaround for "Cache exists but contains no nodes..."
vladar Jun 1, 2021
e7238df
add deprecation comments
vladar Jun 2, 2021
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
25 changes: 25 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,20 @@ aliases:
- /blog.+/

test_template: &test_template
parameters:
npm_rebuild:
type: boolean
default: false
steps:
- checkout
- run: ./scripts/assert-changed-files.sh "packages/*|.circleci/*"
- <<: *attach_to_bootstrap
- when:
condition: << parameters.npm_rebuild >>
steps:
- run: npm rebuild
- run: yarn list react
- run: yarn why lmdb-store
- run:
command: node --max-old-space-size=2048 ./node_modules/.bin/jest -w 1 --ci
environment:
Expand Down Expand Up @@ -230,6 +239,14 @@ jobs:
image: "14"
<<: *test_template

unit_tests_node14_strict_mode:
executor:
name: node
image: "14"
environment:
GATSBY_EXPERIMENTAL_STRICT_MODE: 1
<<: *test_template

integration_tests_gatsby_source_wordpress:
machine: true
steps:
Expand Down Expand Up @@ -626,6 +643,14 @@ workflows:
- lint
- typecheck
- bootstrap
- unit_tests_node14_strict_mode:
<<: *ignore_docs
# rebuild to get correct version of lmdb-store (compiled for node14 not node12)
npm_rebuild: true
requires:
- lint
- typecheck
- bootstrap
- integration_tests_gatsby_source_wordpress:
<<: *e2e-test-workflow
- integration_tests_long_term_caching:
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
"cross-env": "^7.0.3",
"documentation": "^13.1.0",
"enhanced-resolve": "^4.2.0",
"lmdb-store": "^1.5.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"rimraf": "^3.0.2",
Expand Down
51 changes: 51 additions & 0 deletions packages/gatsby/src/datastore/datastore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { IDataStore } from "./types"
import { emitter } from "../redux"

let dataStore: IDataStore
let isLmdb = isLmdbStoreFlagSet()

export function getDataStore(): IDataStore {
if (!dataStore) {
if (isLmdb) {
const { setupLmdbStore } = require(`./lmdb/lmdb-datastore`)
dataStore = setupLmdbStore()
} else {
const { setupInMemoryStore } = require(`./in-memory/in-memory-datastore`)
dataStore = setupInMemoryStore()
}
}
return dataStore
}

export function isLmdbStore(): boolean {
return isLmdb
}

export function detectLmdbStore(): boolean {
const flagIsSet = isLmdbStoreFlagSet()

if (dataStore && isLmdb !== flagIsSet) {
throw new Error(
`GATSBY_EXPERIMENTAL_LMDB_STORE flag had changed after the data store was initialized.` +
`(original value: ${isLmdb ? `true` : `false`}, ` +
`new value: ${flagIsSet ? `true` : `false`})`
)
}
isLmdb = flagIsSet
return flagIsSet
}

function isLmdbStoreFlagSet(): boolean {
return (
Boolean(process.env.GATSBY_EXPERIMENTAL_LMDB_STORE) &&
process.env.GATSBY_EXPERIMENTAL_LMDB_STORE !== `false` &&
process.env.GATSBY_EXPERIMENTAL_LMDB_STORE !== `0`
)
}

// It is possible that the store is not initialized yet when calling `DELETE_CACHE`.
// The code below ensures we wipe cache from the proper store
// (mostly relevant for tests)
emitter.on(`DELETE_CACHE`, () => {
getDataStore()
})
67 changes: 67 additions & 0 deletions packages/gatsby/src/datastore/in-memory/in-memory-datastore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { IDataStore } from "../types"
import { store } from "../../redux"
import { IGatsbyNode } from "../../redux/types"

/**
* @deprecated
*/
function getNodes(): Array<IGatsbyNode> {
const nodes = store.getState().nodes
if (nodes) {
return Array.from(nodes.values())
} else {
return []
}
}

/**
* @deprecated
*/
function getNodesByType(type: string): Array<IGatsbyNode> {
const nodes = store.getState().nodesByType.get(type)
if (nodes) {
return Array.from(nodes.values())
} else {
return []
}
}

function getNode(id: string): IGatsbyNode | undefined {
return store.getState().nodes.get(id)
}

function getTypes(): Array<string> {
// Note: sorting to match the output of the LMDB version (where keys are sorted by default)
return Array.from(store.getState().nodesByType.keys()).sort()
}

function countNodes(typeName?: string): number {
if (!typeName) {
return store.getState().nodes.size
}
const nodes = store.getState().nodesByType.get(typeName)
return nodes ? nodes.size : 0
}

const readyPromise = Promise.resolve(undefined)

/**
* Returns promise that resolves when the store is ready for reads
* (the in-memory store is always ready)
*/
function ready(): Promise<void> {
return readyPromise
}

export function setupInMemoryStore(): IDataStore {
return {
getNode,
getTypes,
countNodes,
ready,

// deprecated:
getNodes,
getNodesByType,
}
}
1 change: 1 addition & 0 deletions packages/gatsby/src/datastore/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getDataStore, isLmdbStore, detectLmdbStore } from "./datastore"
173 changes: 173 additions & 0 deletions packages/gatsby/src/datastore/lmdb/lmdb-datastore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { ArrayLikeIterable, RootDatabase, open } from "lmdb-store"
// import { performance } from "perf_hooks"
import { ActionsUnion, IGatsbyNode } from "../../redux/types"
import { updateNodes } from "./updates/nodes"
import { updateNodesByType } from "./updates/nodes-by-type"
import { IDataStore, ILmdbDatabases } from "../types"
import { emitter, replaceReducer } from "../../redux"

const rootDbFile =
process.env.NODE_ENV === `test`
? `test-datastore-${process.env.JEST_WORKER_ID}`
: `datastore`

let rootDb
let databases

function getRootDb(): RootDatabase {
if (!rootDb) {
rootDb = open({
name: `root`,
path: process.cwd() + `/.cache/data/` + rootDbFile,
sharedStructuresKey: Symbol.for(`structures`),
compression: true,
})
}
return rootDb
}

function getDatabases(): ILmdbDatabases {
if (!databases) {
const rootDb = getRootDb()
databases = {
nodes: rootDb.openDB({
name: `nodes`,
cache: true,
}),
nodesByType: rootDb.openDB({
name: `nodesByType`,
dupSort: true,
}),
}
}
return databases
}

/**
* @deprecated
*/
function getNodes(): Array<IGatsbyNode> {
// const start = performance.now()
const result = Array.from<IGatsbyNode>(iterateNodes())
// const timeTotal = performance.now() - start
// console.warn(
// `getNodes() is deprecated, use iterateNodes() instead; ` +
// `array length: ${result.length}; time(ms): ${timeTotal}`
// )
return result ?? []
}

/**
* @deprecated
*/
function getNodesByType(type: string): Array<IGatsbyNode> {
// const start = performance.now()
const result = Array.from<IGatsbyNode>(iterateNodesByType(type))
// const timeTotal = performance.now() - start
// console.warn(
// `getNodesByType() is deprecated, use iterateNodesByType() instead; ` +
// `array length: ${result.length}; time(ms): ${timeTotal}`
// )
return result ?? []
}

function iterateNodes(): ArrayLikeIterable<IGatsbyNode> {
// Additionally fetching items by id to leverage lmdb-store cache
const nodesDb = getDatabases().nodes
return nodesDb
.getKeys({ snapshot: false })
.map(nodeId => getNode(nodeId)!)
.filter(Boolean)
}

function iterateNodesByType(type: string): ArrayLikeIterable<IGatsbyNode> {
const nodesByType = getDatabases().nodesByType
return nodesByType
.getValues(type)
.map(nodeId => getNode(nodeId)!)
.filter(Boolean)
}

function getNode(id: string): IGatsbyNode | undefined {
if (!id) return undefined
const { nodes } = getDatabases()
return nodes.get(id)
}

function getTypes(): Array<string> {
return getDatabases().nodesByType.getKeys({}).asArray
}

function countNodes(typeName?: string): number {
if (!typeName) {
const stats = getDatabases().nodes.getStats()
// @ts-ignore
return Number(stats.entryCount || 0)
}

const { nodesByType } = getDatabases()
let count = 0
nodesByType.getValues(typeName).forEach(() => {
count++
})
return count
}

let lastOperationPromise: Promise<any> = Promise.resolve()

function updateDataStore(action: ActionsUnion): void {
switch (action.type) {
case `DELETE_CACHE`: {
const dbs = getDatabases()
// Force sync commit
dbs.nodes.transactionSync(() => {
dbs.nodes.clear()
dbs.nodesByType.clear()
})
break
}
case `CREATE_NODE`:
case `ADD_FIELD_TO_NODE`:
case `ADD_CHILD_NODE_TO_PARENT_NODE`:
case `DELETE_NODE`: {
const dbs = getDatabases()
lastOperationPromise = Promise.all([
updateNodes(dbs.nodes, action),
updateNodesByType(dbs.nodesByType, action),
])
}
}
}

/**
* Resolves when all the data is synced
*/
async function ready(): Promise<void> {
await lastOperationPromise
}

export function setupLmdbStore(): IDataStore {
const lmdbDatastore = {
getNode,
getTypes,
countNodes,
iterateNodes,
iterateNodesByType,
updateDataStore,
ready,

// deprecated:
getNodes,
getNodesByType,
}
replaceReducer({
nodes: (state = new Map(), _) => state,
nodesByType: (state = new Map(), _) => state,
})
emitter.on(`*`, action => {
if (action) {
updateDataStore(action)
}
})
return lmdbDatastore
}
22 changes: 22 additions & 0 deletions packages/gatsby/src/datastore/lmdb/updates/nodes-by-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ActionsUnion } from "../../../redux/types"
import { ILmdbDatabases } from "../../types"

export function updateNodesByType(
nodesByTypeDb: ILmdbDatabases["nodesByType"],
action: ActionsUnion
): Promise<boolean> | boolean {
switch (action.type) {
case `CREATE_NODE`:
case `ADD_FIELD_TO_NODE`:
case `ADD_CHILD_NODE_TO_PARENT_NODE`: {
// nodesByType db uses dupSort, so `put` will effectively append an id
return nodesByTypeDb.put(action.payload.internal.type, action.payload.id)
}
case `DELETE_NODE`: {
return action.payload
? nodesByTypeDb.remove(action.payload.internal.type, action.payload.id)
: false
}
}
return false
}
Loading