Fix for debugging with webstorm the old way (if desired), updating the builder middleware to be more multi-dev capable, ignoring 409s when attempting to update the last updated at for apps (if multiple devs hit at same time, only use one) also updating writethrough cache to retry once, with the extended TTL on locks, plus the multi-dev collab it can take a minute to update usage quota doc when a lot of updates occur at once.

This commit is contained in:
mike12345567 2023-05-30 17:41:20 +01:00
parent 4e16c34366
commit 7c7bd4d5cb
5 changed files with 62 additions and 32 deletions

View File

@ -44,7 +44,7 @@ async function put(
if (updateDb) { if (updateDb) {
const lockResponse = await locks.doWithLock( const lockResponse = await locks.doWithLock(
{ {
type: LockType.TRY_ONCE, type: LockType.TRY_TWICE,
name: LockName.PERSIST_WRITETHROUGH, name: LockName.PERSIST_WRITETHROUGH,
resource: key, resource: key,
ttl: 15000, ttl: 15000,

View File

@ -4,10 +4,10 @@ import { LockOptions, LockType } from "@budibase/types"
import * as context from "../context" import * as context from "../context"
import env from "../environment" import env from "../environment"
const getClient = async ( async function getClient(
type: LockType, type: LockType,
opts?: Redlock.Options opts?: Redlock.Options
): Promise<Redlock> => { ): Promise<Redlock> {
if (type === LockType.CUSTOM) { if (type === LockType.CUSTOM) {
return newRedlock(opts) return newRedlock(opts)
} }
@ -18,6 +18,9 @@ const getClient = async (
case LockType.TRY_ONCE: { case LockType.TRY_ONCE: {
return newRedlock(OPTIONS.TRY_ONCE) return newRedlock(OPTIONS.TRY_ONCE)
} }
case LockType.TRY_TWICE: {
return newRedlock(OPTIONS.TRY_TWICE)
}
case LockType.DEFAULT: { case LockType.DEFAULT: {
return newRedlock(OPTIONS.DEFAULT) return newRedlock(OPTIONS.DEFAULT)
} }
@ -35,6 +38,9 @@ const OPTIONS = {
// immediately throws an error if the lock is already held // immediately throws an error if the lock is already held
retryCount: 0, retryCount: 0,
}, },
TRY_TWICE: {
retryCount: 1,
},
TEST: { TEST: {
// higher retry count in unit tests // higher retry count in unit tests
// due to high contention. // due to high contention.
@ -62,7 +68,7 @@ const OPTIONS = {
}, },
} }
const newRedlock = async (opts: Redlock.Options = {}) => { export async function newRedlock(opts: Redlock.Options = {}) {
let options = { ...OPTIONS.DEFAULT, ...opts } let options = { ...OPTIONS.DEFAULT, ...opts }
const redisWrapper = await getLockClient() const redisWrapper = await getLockClient()
const client = redisWrapper.getClient() const client = redisWrapper.getClient()
@ -81,22 +87,26 @@ type RedlockExecution<T> =
| SuccessfulRedlockExecution<T> | SuccessfulRedlockExecution<T>
| UnsuccessfulRedlockExecution | UnsuccessfulRedlockExecution
export const doWithLock = async <T>( function getLockName(opts: LockOptions) {
opts: LockOptions,
task: () => Promise<T>
): Promise<RedlockExecution<T>> => {
const redlock = await getClient(opts.type, opts.customOptions)
let lock
try {
// determine lock name // determine lock name
// by default use the tenantId for uniqueness, unless using a system lock // by default use the tenantId for uniqueness, unless using a system lock
const prefix = opts.systemLock ? "system" : context.getTenantId() const prefix = opts.systemLock ? "system" : context.getTenantId()
let name: string = `lock:${prefix}_${opts.name}` let name: string = `lock:${prefix}_${opts.name}`
// add additional unique name if required // add additional unique name if required
if (opts.resource) { if (opts.resource) {
name = name + `_${opts.resource}` name = name + `_${opts.resource}`
} }
return name
}
export async function doWithLock<T>(
opts: LockOptions,
task: () => Promise<T>
): Promise<RedlockExecution<T>> {
const redlock = await getClient(opts.type, opts.customOptions)
let lock
try {
const name = getLockName(opts)
// create the lock // create the lock
lock = await redlock.lock(name, opts.ttl) lock = await redlock.lock(name, opts.ttl)

View File

@ -9,8 +9,8 @@ import {
checkDebounce, checkDebounce,
setDebounce, setDebounce,
} from "../utilities/redis" } from "../utilities/redis"
import { db as dbCore, cache, permissions } from "@budibase/backend-core" import { db as dbCore, cache } from "@budibase/backend-core"
import { BBContext, Database } from "@budibase/types" import { UserCtx, Database } from "@budibase/types"
const DEBOUNCE_TIME_SEC = 30 const DEBOUNCE_TIME_SEC = 30
@ -23,7 +23,7 @@ const DEBOUNCE_TIME_SEC = 30
* through the authorized middleware * * through the authorized middleware *
****************************************************/ ****************************************************/
async function checkDevAppLocks(ctx: BBContext) { async function checkDevAppLocks(ctx: UserCtx) {
const appId = ctx.appId const appId = ctx.appId
// if any public usage, don't proceed // if any public usage, don't proceed
@ -42,7 +42,7 @@ async function checkDevAppLocks(ctx: BBContext) {
} }
} }
async function updateAppUpdatedAt(ctx: BBContext) { async function updateAppUpdatedAt(ctx: UserCtx) {
const appId = ctx.appId const appId = ctx.appId
// if debouncing skip this update // if debouncing skip this update
// get methods also aren't updating // get methods also aren't updating
@ -50,6 +50,7 @@ async function updateAppUpdatedAt(ctx: BBContext) {
return return
} }
await dbCore.doWithDB(appId, async (db: Database) => { await dbCore.doWithDB(appId, async (db: Database) => {
try {
const metadata = await db.get(DocumentType.APP_METADATA) const metadata = await db.get(DocumentType.APP_METADATA)
metadata.updatedAt = new Date().toISOString() metadata.updatedAt = new Date().toISOString()
@ -60,10 +61,18 @@ async function updateAppUpdatedAt(ctx: BBContext) {
await cache.app.invalidateAppMetadata(appId, metadata) await cache.app.invalidateAppMetadata(appId, metadata)
// 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)
} catch (err: any) {
// if a 409 occurs, then multiple clients connected at the same time - ignore
if (err?.status === 409) {
return
} else {
throw err
}
}
}) })
} }
export default async function builder(ctx: BBContext) { export default async function builder(ctx: UserCtx) {
const appId = ctx.appId const appId = ctx.appId
// this only functions within an app context // this only functions within an app context
if (!appId) { if (!appId) {

View File

@ -35,11 +35,21 @@ export const getComponentLibraryManifest = async (library: string) => {
const filename = "manifest.json" const filename = "manifest.json"
if (env.isDev() || env.isTest()) { if (env.isDev() || env.isTest()) {
const path = join(TOP_LEVEL_PATH, "packages/client", filename) const paths = [
join(TOP_LEVEL_PATH, "packages/client", filename),
join(process.cwd(), "client", filename),
]
for (let path of paths) {
if (fs.existsSync(path)) {
// always load from new so that updates are refreshed // always load from new so that updates are refreshed
delete require.cache[require.resolve(path)] delete require.cache[require.resolve(path)]
return require(path) return require(path)
} }
}
throw new Error(
`Unable to find ${filename} in development environment (may need to build).`
)
}
if (!appId) { if (!appId) {
throw new Error("No app ID found - cannot get component libraries") throw new Error("No app ID found - cannot get component libraries")

View File

@ -6,6 +6,7 @@ export enum LockType {
* No retries will take place and no error will be thrown. * No retries will take place and no error will be thrown.
*/ */
TRY_ONCE = "try_once", TRY_ONCE = "try_once",
TRY_TWICE = "try_twice",
DEFAULT = "default", DEFAULT = "default",
DELAY_500 = "delay_500", DELAY_500 = "delay_500",
CUSTOM = "custom", CUSTOM = "custom",