Merge pull request #3392 from Budibase/fix/app-list-perf
Caching app metadata to improve application fetch performance
This commit is contained in:
commit
1bfe8527e6
|
@ -1,3 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
user: require("./src/cache/user"),
|
user: require("./src/cache/user"),
|
||||||
|
app: require("./src/cache/appMetadata"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
const redis = require("../redis/authRedis")
|
||||||
|
const { getCouch } = require("../db")
|
||||||
|
const { DocumentTypes } = require("../db/constants")
|
||||||
|
|
||||||
|
const EXPIRY_SECONDS = 3600
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default populate app metadata function
|
||||||
|
*/
|
||||||
|
const populateFromDB = async (appId, CouchDB = null) => {
|
||||||
|
if (!CouchDB) {
|
||||||
|
CouchDB = getCouch()
|
||||||
|
}
|
||||||
|
const db = new CouchDB(appId, { skip_setup: true })
|
||||||
|
return db.get(DocumentTypes.APP_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the requested app metadata by id.
|
||||||
|
* Use redis cache to first read the app metadata.
|
||||||
|
* If not present fallback to loading the app metadata directly and re-caching.
|
||||||
|
* @param {*} appId the id of the app to get metadata from.
|
||||||
|
* @returns {object} the app metadata.
|
||||||
|
*/
|
||||||
|
exports.getAppMetadata = async (appId, CouchDB = null) => {
|
||||||
|
const client = await redis.getAppClient()
|
||||||
|
// try cache
|
||||||
|
let metadata = await client.get(appId)
|
||||||
|
if (!metadata) {
|
||||||
|
metadata = await populateFromDB(appId, CouchDB)
|
||||||
|
client.store(appId, metadata, EXPIRY_SECONDS)
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.invalidateAppMetadata = async appId => {
|
||||||
|
const client = await redis.getAppClient()
|
||||||
|
await client.delete(appId)
|
||||||
|
}
|
|
@ -6,6 +6,9 @@ const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants")
|
||||||
const { getTenantId, getTenantIDFromAppID } = require("../tenancy")
|
const { getTenantId, getTenantIDFromAppID } = require("../tenancy")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { getCouch } = require("./index")
|
const { getCouch } = require("./index")
|
||||||
|
const { getAppMetadata } = require("../cache/appMetadata")
|
||||||
|
|
||||||
|
const NO_APP_ERROR = "No app provided"
|
||||||
|
|
||||||
const UNICODE_MAX = "\ufff0"
|
const UNICODE_MAX = "\ufff0"
|
||||||
|
|
||||||
|
@ -45,14 +48,23 @@ function getDocParams(docType, docId = null, otherProps = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.isDevAppID = appId => {
|
exports.isDevAppID = appId => {
|
||||||
|
if (!appId) {
|
||||||
|
throw NO_APP_ERROR
|
||||||
|
}
|
||||||
return appId.startsWith(exports.APP_DEV_PREFIX)
|
return appId.startsWith(exports.APP_DEV_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.isProdAppID = appId => {
|
exports.isProdAppID = appId => {
|
||||||
|
if (!appId) {
|
||||||
|
throw NO_APP_ERROR
|
||||||
|
}
|
||||||
return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId)
|
return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDevApp(app) {
|
function isDevApp(app) {
|
||||||
|
if (!app) {
|
||||||
|
throw NO_APP_ERROR
|
||||||
|
}
|
||||||
return exports.isDevAppID(app.appId)
|
return exports.isDevAppID(app.appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,16 +244,16 @@ exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
|
||||||
if (idsOnly) {
|
if (idsOnly) {
|
||||||
return appDbNames
|
return appDbNames
|
||||||
}
|
}
|
||||||
const appPromises = appDbNames.map(db =>
|
const appPromises = appDbNames.map(app =>
|
||||||
// skip setup otherwise databases could be re-created
|
// skip setup otherwise databases could be re-created
|
||||||
new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA)
|
getAppMetadata(app, CouchDB)
|
||||||
)
|
)
|
||||||
if (appPromises.length === 0) {
|
if (appPromises.length === 0) {
|
||||||
return []
|
return []
|
||||||
} else {
|
} else {
|
||||||
const response = await Promise.allSettled(appPromises)
|
const response = await Promise.allSettled(appPromises)
|
||||||
const apps = response
|
const apps = response
|
||||||
.filter(result => result.status === "fulfilled")
|
.filter(result => result.status === "fulfilled" && result.value != null)
|
||||||
.map(({ value }) => value)
|
.map(({ value }) => value)
|
||||||
if (!all) {
|
if (!all) {
|
||||||
return apps.filter(app => {
|
return apps.filter(app => {
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
const Client = require("./index")
|
const Client = require("./index")
|
||||||
const utils = require("./utils")
|
const utils = require("./utils")
|
||||||
|
|
||||||
let userClient, sessionClient
|
let userClient, sessionClient, appClient
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
userClient = await new Client(utils.Databases.USER_CACHE).init()
|
userClient = await new Client(utils.Databases.USER_CACHE).init()
|
||||||
sessionClient = await new Client(utils.Databases.SESSIONS).init()
|
sessionClient = await new Client(utils.Databases.SESSIONS).init()
|
||||||
|
appClient = await new Client(utils.Databases.APP_METADATA).init()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("exit", async () => {
|
process.on("exit", async () => {
|
||||||
if (userClient) await userClient.finish()
|
if (userClient) await userClient.finish()
|
||||||
if (sessionClient) await sessionClient.finish()
|
if (sessionClient) await sessionClient.finish()
|
||||||
|
if (appClient) await appClient.finish()
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -26,4 +28,10 @@ module.exports = {
|
||||||
}
|
}
|
||||||
return sessionClient
|
return sessionClient
|
||||||
},
|
},
|
||||||
|
getAppClient: async () => {
|
||||||
|
if (!appClient) {
|
||||||
|
await init()
|
||||||
|
}
|
||||||
|
return appClient
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ exports.Databases = {
|
||||||
SESSIONS: "session",
|
SESSIONS: "session",
|
||||||
USER_CACHE: "users",
|
USER_CACHE: "users",
|
||||||
FLAGS: "flags",
|
FLAGS: "flags",
|
||||||
|
APP_METADATA: "appMetadata",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
|
|
@ -45,6 +45,7 @@ const {
|
||||||
} = require("../../utilities/fileSystem/clientLibrary")
|
} = require("../../utilities/fileSystem/clientLibrary")
|
||||||
const { getTenantId, isMultiTenant } = require("@budibase/auth/tenancy")
|
const { getTenantId, isMultiTenant } = require("@budibase/auth/tenancy")
|
||||||
const { syncGlobalUsers } = require("./user")
|
const { syncGlobalUsers } = require("./user")
|
||||||
|
const { app: appCache } = require("@budibase/auth/cache")
|
||||||
|
|
||||||
const URL_REGEX_SLASH = /\/|\\/g
|
const URL_REGEX_SLASH = /\/|\\/g
|
||||||
|
|
||||||
|
@ -319,6 +320,7 @@ exports.delete = async ctx => {
|
||||||
}
|
}
|
||||||
// make sure the app/role doesn't stick around after the app has been deleted
|
// make sure the app/role doesn't stick around after the app has been deleted
|
||||||
await removeAppFromUserRoles(ctx, ctx.params.appId)
|
await removeAppFromUserRoles(ctx, ctx.params.appId)
|
||||||
|
await appCache.invalidateAppMetadata(ctx.params.appId)
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = result
|
ctx.body = result
|
||||||
|
@ -387,7 +389,10 @@ const updateAppPackage = async (ctx, appPackage, appId) => {
|
||||||
// Redis, shouldn't ever store it
|
// Redis, shouldn't ever store it
|
||||||
delete newAppPackage.lockedBy
|
delete newAppPackage.lockedBy
|
||||||
|
|
||||||
return await db.put(newAppPackage)
|
const response = await db.put(newAppPackage)
|
||||||
|
// remove any cached metadata, so that it will be updated
|
||||||
|
await appCache.invalidateAppMetadata(appId)
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
const createEmptyAppPackage = async (ctx, app) => {
|
const createEmptyAppPackage = async (ctx, app) => {
|
||||||
|
|
|
@ -6,6 +6,7 @@ const {
|
||||||
disableAllCrons,
|
disableAllCrons,
|
||||||
enableCronTrigger,
|
enableCronTrigger,
|
||||||
} = require("../../../automations/utils")
|
} = require("../../../automations/utils")
|
||||||
|
const { app: appCache } = require("@budibase/auth/cache")
|
||||||
|
|
||||||
// the max time we can wait for an invalidation to complete before considering it failed
|
// the max time we can wait for an invalidation to complete before considering it failed
|
||||||
const MAX_PENDING_TIME_MS = 30 * 60000
|
const MAX_PENDING_TIME_MS = 30 * 60000
|
||||||
|
@ -103,6 +104,7 @@ async function deployApp(deployment) {
|
||||||
appDoc.appId = productionAppId
|
appDoc.appId = productionAppId
|
||||||
appDoc.instance._id = productionAppId
|
appDoc.instance._id = productionAppId
|
||||||
await db.put(appDoc)
|
await db.put(appDoc)
|
||||||
|
await appCache.invalidateAppMetadata(productionAppId)
|
||||||
console.log("New app doc written successfully.")
|
console.log("New app doc written successfully.")
|
||||||
await initDeployedApp(productionAppId)
|
await initDeployedApp(productionAppId)
|
||||||
console.log("Deployed app initialised, setting deployment to successful")
|
console.log("Deployed app initialised, setting deployment to successful")
|
||||||
|
|
|
@ -6,6 +6,7 @@ const { request } = require("../../utilities/workerRequests")
|
||||||
const { clearLock } = require("../../utilities/redis")
|
const { clearLock } = require("../../utilities/redis")
|
||||||
const { Replication } = require("@budibase/auth").db
|
const { Replication } = require("@budibase/auth").db
|
||||||
const { DocumentTypes } = require("../../db/utils")
|
const { DocumentTypes } = require("../../db/utils")
|
||||||
|
const { app: appCache } = require("@budibase/auth/cache")
|
||||||
|
|
||||||
async function redirect(ctx, method, path = "global") {
|
async function redirect(ctx, method, path = "global") {
|
||||||
const { devPath } = ctx.params
|
const { devPath } = ctx.params
|
||||||
|
@ -106,6 +107,7 @@ exports.revert = async ctx => {
|
||||||
appDoc.appId = appId
|
appDoc.appId = appId
|
||||||
appDoc.instance._id = appId
|
appDoc.instance._id = appId
|
||||||
await db.put(appDoc)
|
await db.put(appDoc)
|
||||||
|
await appCache.invalidateAppMetadata(appId)
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "Reverted changes successfully.",
|
message: "Reverted changes successfully.",
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ const {
|
||||||
const CouchDB = require("../db")
|
const CouchDB = require("../db")
|
||||||
const { DocumentTypes } = require("../db/utils")
|
const { DocumentTypes } = require("../db/utils")
|
||||||
const { PermissionTypes } = require("@budibase/auth/permissions")
|
const { PermissionTypes } = require("@budibase/auth/permissions")
|
||||||
|
const { app: appCache } = require("@budibase/auth/cache")
|
||||||
|
|
||||||
const DEBOUNCE_TIME_SEC = 30
|
const DEBOUNCE_TIME_SEC = 30
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ async function updateAppUpdatedAt(ctx) {
|
||||||
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
||||||
metadata.updatedAt = new Date().toISOString()
|
metadata.updatedAt = new Date().toISOString()
|
||||||
await db.put(metadata)
|
await db.put(metadata)
|
||||||
|
await appCache.invalidateAppMetadata(appId)
|
||||||
// set a new debounce record with a short TTL
|
// set a new debounce record with a short TTL
|
||||||
await setDebounce(appId, DEBOUNCE_TIME_SEC)
|
await setDebounce(appId, DEBOUNCE_TIME_SEC)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ exports.objectStoreUrl = () => {
|
||||||
* via a specific endpoint (under /api/assets/client).
|
* via a specific endpoint (under /api/assets/client).
|
||||||
* @param {string} appId In production we need the appId to look up the correct bucket, as the
|
* @param {string} appId In production we need the appId to look up the correct bucket, as the
|
||||||
* version of the client lib may differ between apps.
|
* version of the client lib may differ between apps.
|
||||||
|
* @param {string} version The version to retrieve.
|
||||||
* @return {string} The URL to be inserted into appPackage response or server rendered
|
* @return {string} The URL to be inserted into appPackage response or server rendered
|
||||||
* app index file.
|
* app index file.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -43,11 +43,7 @@ exports.save = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseBooleanParam = param => {
|
const parseBooleanParam = param => {
|
||||||
if (param && param === "false") {
|
return !(param && param === "false")
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.adminUser = async ctx => {
|
exports.adminUser = async ctx => {
|
||||||
|
|
Loading…
Reference in New Issue