Migration locks and add optional preventRetry option
This commit is contained in:
parent
eae5223fe3
commit
9f2620dd7a
|
@ -19,6 +19,7 @@
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"emitter-listener": "^1.1.2",
|
"emitter-listener": "^1.1.2",
|
||||||
"ioredis": "^4.27.1",
|
"ioredis": "^4.27.1",
|
||||||
|
"redlock": "^4.0.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa-passport": "^4.1.4",
|
"koa-passport": "^4.1.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
"@types/tar-fs": "^2.0.1",
|
"@types/tar-fs": "^2.0.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@types/semver": "^7.0.0",
|
"@types/semver": "^7.0.0",
|
||||||
|
"@types/redlock": "^4.0.0",
|
||||||
"ioredis-mock": "^5.5.5",
|
"ioredis-mock": "^5.5.5",
|
||||||
"jest": "^27.0.3",
|
"jest": "^27.0.3",
|
||||||
"koa": "2.7.0",
|
"koa": "2.7.0",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Client: require("./src/redis"),
|
Client: require("./src/redis"),
|
||||||
utils: require("./src/redis/utils"),
|
utils: require("./src/redis/utils"),
|
||||||
|
clients: require("./src/redis/authRedis"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,15 @@ exports.runMigration = async (migration, options = {}) => {
|
||||||
log(
|
log(
|
||||||
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}`
|
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (migration.preventRetry) {
|
||||||
|
// eagerly set the completion date
|
||||||
|
// so that we never run this migration twice even upon failure
|
||||||
|
doc[migrationName] = Date.now()
|
||||||
|
const response = await db.put(doc)
|
||||||
|
doc._rev = response.rev
|
||||||
|
}
|
||||||
|
|
||||||
// run the migration with tenant context
|
// run the migration with tenant context
|
||||||
if (migrationType === exports.MIGRATION_TYPES.APP) {
|
if (migrationType === exports.MIGRATION_TYPES.APP) {
|
||||||
await context.doInAppContext(db.name, async () => {
|
await context.doInAppContext(db.name, async () => {
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
const Client = require("./index")
|
const Client = require("./index")
|
||||||
const utils = require("./utils")
|
const utils = require("./utils")
|
||||||
|
const { getRedlock } = require("./redlock")
|
||||||
|
|
||||||
let userClient, sessionClient, appClient, cacheClient
|
let userClient, sessionClient, appClient, cacheClient
|
||||||
|
let migrationsRedlock
|
||||||
|
|
||||||
|
// turn retry off so that only one instance can ever hold the lock
|
||||||
|
const migrationsRedlockConfig = { retryCount: 0 }
|
||||||
|
|
||||||
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()
|
appClient = await new Client(utils.Databases.APP_METADATA).init()
|
||||||
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
|
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
|
||||||
|
// pass the underlying ioredis client to redlock
|
||||||
|
migrationsRedlock = getRedlock(
|
||||||
|
cacheClient.getClient(),
|
||||||
|
migrationsRedlockConfig
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("exit", async () => {
|
process.on("exit", async () => {
|
||||||
|
@ -42,4 +52,10 @@ module.exports = {
|
||||||
}
|
}
|
||||||
return cacheClient
|
return cacheClient
|
||||||
},
|
},
|
||||||
|
getMigrationsRedlock: async () => {
|
||||||
|
if (!migrationsRedlock) {
|
||||||
|
await init()
|
||||||
|
}
|
||||||
|
return migrationsRedlock
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,10 @@ class RedisWrapper {
|
||||||
this._db = db
|
this._db = db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getClient() {
|
||||||
|
return CLIENT
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
CLOSED = false
|
CLOSED = false
|
||||||
init()
|
init()
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Redlock from "redlock"
|
||||||
|
|
||||||
|
export const getRedlock = (redisClient: any, opts = { retryCount: 10 }) => {
|
||||||
|
return new Redlock([redisClient], {
|
||||||
|
// the expected clock drift; for more details
|
||||||
|
// see http://redis.io/topics/distlock
|
||||||
|
driftFactor: 0.01, // multiplied by lock ttl to determine drift time
|
||||||
|
|
||||||
|
// the max number of times Redlock will attempt
|
||||||
|
// to lock a resource before erroring
|
||||||
|
retryCount: opts.retryCount,
|
||||||
|
|
||||||
|
// the time in ms between attempts
|
||||||
|
retryDelay: 200, // time in ms
|
||||||
|
|
||||||
|
// the max time in ms randomly added to retries
|
||||||
|
// to improve performance under high contention
|
||||||
|
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
|
||||||
|
retryJitter: 200, // time in ms
|
||||||
|
})
|
||||||
|
}
|
|
@ -829,6 +829,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@babel/types" "^7.3.0"
|
||||||
|
|
||||||
|
"@types/bluebird@*":
|
||||||
|
version "3.5.36"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.36.tgz#00d9301d4dc35c2f6465a8aec634bb533674c652"
|
||||||
|
integrity sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==
|
||||||
|
|
||||||
"@types/body-parser@*":
|
"@types/body-parser@*":
|
||||||
version "1.19.2"
|
version "1.19.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
||||||
|
@ -895,13 +900,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
|
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
|
||||||
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==
|
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==
|
||||||
|
|
||||||
"@types/ioredis@^4.27.1":
|
|
||||||
version "4.28.10"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff"
|
|
||||||
integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/node" "*"
|
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
||||||
|
@ -993,6 +991,21 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||||
|
|
||||||
|
"@types/redis@^2.8.0":
|
||||||
|
version "2.8.32"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11"
|
||||||
|
integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/redlock@^4.0.0":
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/redlock/-/redlock-4.0.3.tgz#aeab5fe5f0d433a125f6dcf9a884372ac0cddd4b"
|
||||||
|
integrity sha512-mcvvrquwREbAqyZALNBIlf49AL9Aa324BG+J/Dv4TAP8g+nxQMBI4/APNqqS99QEY7VTNT9XvsaczCVGK8uNnQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/bluebird" "*"
|
||||||
|
"@types/redis" "^2.8.0"
|
||||||
|
|
||||||
"@types/semver@^7.0.0":
|
"@types/semver@^7.0.0":
|
||||||
version "7.3.9"
|
version "7.3.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc"
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc"
|
||||||
|
@ -1397,6 +1410,11 @@ bl@^4.0.3:
|
||||||
inherits "^2.0.4"
|
inherits "^2.0.4"
|
||||||
readable-stream "^3.4.0"
|
readable-stream "^3.4.0"
|
||||||
|
|
||||||
|
bluebird@^3.7.2:
|
||||||
|
version "3.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
boom@2.x.x:
|
boom@2.x.x:
|
||||||
version "2.10.1"
|
version "2.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
||||||
|
@ -4835,6 +4853,13 @@ redis-parser@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
redis-errors "^1.0.0"
|
redis-errors "^1.0.0"
|
||||||
|
|
||||||
|
redlock@^4.0.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redlock/-/redlock-4.2.0.tgz#c26590768559afd5fff76aa1133c94b411ff4f5f"
|
||||||
|
integrity sha512-j+oQlG+dOwcetUt2WJWttu4CZVeRzUrcVcISFmEmfyuwCVSJ93rDT7YSgg7H7rnxwoRyk/jU46kycVka5tW7jA==
|
||||||
|
dependencies:
|
||||||
|
bluebird "^3.7.2"
|
||||||
|
|
||||||
regenerator-runtime@^0.13.4:
|
regenerator-runtime@^0.13.4:
|
||||||
version "0.13.9"
|
version "0.13.9"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { migrations } from "@budibase/backend-core"
|
import { migrations, redis } from "@budibase/backend-core"
|
||||||
|
|
||||||
// migration functions
|
// migration functions
|
||||||
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
||||||
|
@ -15,6 +15,7 @@ export interface Migration {
|
||||||
opts?: object
|
opts?: object
|
||||||
fn: Function
|
fn: Function
|
||||||
silent?: boolean
|
silent?: boolean
|
||||||
|
preventRetry?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,21 +67,63 @@ export const MIGRATIONS: Migration[] = [
|
||||||
opts: { all: true },
|
opts: { all: true },
|
||||||
fn: backfill.app.run,
|
fn: backfill.app.run,
|
||||||
silent: !!env.SELF_HOSTED, // reduce noisy logging
|
silent: !!env.SELF_HOSTED, // reduce noisy logging
|
||||||
|
preventRetry: !!env.SELF_HOSTED, // only ever run once
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: migrations.MIGRATION_TYPES.GLOBAL,
|
type: migrations.MIGRATION_TYPES.GLOBAL,
|
||||||
name: "event_global_backfill",
|
name: "event_global_backfill",
|
||||||
fn: backfill.global.run,
|
fn: backfill.global.run,
|
||||||
silent: !!env.SELF_HOSTED, // reduce noisy logging
|
silent: !!env.SELF_HOSTED, // reduce noisy logging
|
||||||
|
preventRetry: !!env.SELF_HOSTED, // only ever run once
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: migrations.MIGRATION_TYPES.INSTALLATION,
|
type: migrations.MIGRATION_TYPES.INSTALLATION,
|
||||||
name: "event_installation_backfill",
|
name: "event_installation_backfill",
|
||||||
fn: backfill.installation.run,
|
fn: backfill.installation.run,
|
||||||
silent: !!env.SELF_HOSTED, // reduce noisy logging
|
silent: !!env.SELF_HOSTED, // reduce noisy logging
|
||||||
|
preventRetry: !!env.SELF_HOSTED, // only ever run once
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const migrate = async (options?: MigrationOptions) => {
|
export const migrate = async (options?: MigrationOptions) => {
|
||||||
await migrations.runMigrations(MIGRATIONS, options)
|
if (env.SELF_HOSTED) {
|
||||||
|
// self host runs migrations on startup
|
||||||
|
// make sure only a single instance runs them
|
||||||
|
await migrateWithLock(options)
|
||||||
|
} else {
|
||||||
|
await migrations.runMigrations(MIGRATIONS, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrateWithLock = async (options?: MigrationOptions) => {
|
||||||
|
// get a new lock client
|
||||||
|
|
||||||
|
const redlock = await redis.clients.getMigrationsRedlock()
|
||||||
|
// lock for 15 minutes
|
||||||
|
const ttl = 1000 * 60 * 15
|
||||||
|
|
||||||
|
let migrationLock
|
||||||
|
|
||||||
|
// acquire lock
|
||||||
|
try {
|
||||||
|
migrationLock = await redlock.lock("migrations", ttl)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.name === "LockError") {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run migrations
|
||||||
|
try {
|
||||||
|
await migrations.runMigrations(MIGRATIONS, options)
|
||||||
|
} finally {
|
||||||
|
// release lock
|
||||||
|
try {
|
||||||
|
await migrationLock.unlock()
|
||||||
|
} catch (e) {
|
||||||
|
console.error("unable to release migration lock")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue