1
0
Fork 0
mirror of synced 2024-07-07 07:15:43 +12:00

Attempt to fix race condition when creating multiple auto ID columns in parallel.

This commit is contained in:
Sam Rose 2024-05-03 14:14:10 +01:00
parent f783cb12a5
commit af516427fc
No known key found for this signature in database
5 changed files with 54 additions and 41 deletions

View file

@ -1,20 +1,11 @@
import { getRowParams } from "../../../db/utils"
import {
outputProcessing,
processAutoColumn,
processFormulas,
} from "../../../utilities/rowProcessor"
import { context, locks } from "@budibase/backend-core"
import {
Table,
Row,
LockType,
LockName,
FormulaType,
FieldType,
} from "@budibase/types"
import { context } from "@budibase/backend-core"
import { Table, Row, FormulaType, FieldType } from "@budibase/types"
import * as linkRows from "../../../db/linkedRows"
import sdk from "../../../sdk"
import isEqual from "lodash/isEqual"
import { cloneDeep } from "lodash/fp"
@ -151,30 +142,7 @@ export async function finaliseRow(
// if another row has been written since processing this will
// handle the auto ID clash
if (oldTable && !isEqual(oldTable, table)) {
try {
await db.put(table)
} catch (err: any) {
if (err.status === 409) {
// Some conflicts with the autocolumns occurred, we need to refetch the table and recalculate
await locks.doWithLock(
{
type: LockType.AUTO_EXTEND,
name: LockName.PROCESS_AUTO_COLUMNS,
resource: table._id,
},
async () => {
const latestTable = await sdk.tables.getTable(table._id!)
let response = processAutoColumn(null, latestTable, row, {
reprocessing: true,
})
await db.put(response.table)
row = response.row
}
)
} else {
throw err
}
}
await db.put(table)
}
const response = await db.put(row)
// for response, calculate the formulas for the enriched row

View file

@ -145,6 +145,7 @@ describe("sdk >> rows >> internal", () => {
lastID: 1,
},
},
_rev: expect.stringMatching("2-.*"),
},
row: {
...row,

View file

@ -1,6 +1,6 @@
import * as linkRows from "../../db/linkedRows"
import { processFormulas, fixAutoColumnSubType } from "./utils"
import { objectStore, utils } from "@budibase/backend-core"
import { context, objectStore, utils } from "@budibase/backend-core"
import { InternalTables } from "../../db/utils"
import { TYPE_TRANSFORM_MAP } from "./map"
import {
@ -9,6 +9,7 @@ import {
Row,
RowAttachment,
Table,
isAutoColumnField,
} from "@budibase/types"
import { cloneDeep } from "lodash/fp"
import {
@ -25,7 +26,44 @@ type AutoColumnProcessingOpts = {
noAutoRelationships?: boolean
}
const BASE_AUTO_ID = 1
// Returns the next auto ID for a column in a table. On success, the table will
// be updated which is why it gets returned. The nextID returned is guaranteed
// to be given only to you, and if you don't use it it's gone forever (a gap
// will be left in the auto ID sequence).
//
// This function can throw if it fails to generate an auto ID after so many
// attempts.
async function getNextAutoId(
table: Table,
column: string
): Promise<{ table: Table; nextID: number }> {
const db = context.getAppDB()
for (let attempt = 0; attempt < 5; attempt++) {
const schema = table.schema[column]
if (!isAutoColumnField(schema)) {
throw new Error(`Column ${column} is not an auto column`)
}
schema.lastID = (schema.lastID || 0) + 1
try {
const resp = await db.put(table)
table._rev = resp.rev
return { table, nextID: schema.lastID }
} catch (e: any) {
if (e.status !== 409) {
throw e
}
// We wait for a random amount of time before retrying. The randomness
// makes it less likely for multiple requests modifying this table to
// collide.
await new Promise(resolve =>
setTimeout(resolve, Math.random() * 1.2 ** attempt * 1000)
)
table = await db.get(table._id)
}
}
throw new Error("Failed to generate an auto ID")
}
/**
* This will update any auto columns that are found on the row/table with the correct information based on
@ -37,7 +75,7 @@ const BASE_AUTO_ID = 1
* @returns The updated row and table, the table may need to be updated
* for automatic ID purposes.
*/
export function processAutoColumn(
export async function processAutoColumn(
userId: string | null | undefined,
table: Table,
row: Row,
@ -79,8 +117,9 @@ export function processAutoColumn(
break
case AutoFieldSubType.AUTO_ID:
if (creating) {
schema.lastID = !schema.lastID ? BASE_AUTO_ID : schema.lastID + 1
row[key] = schema.lastID
const { table: newTable, nextID } = await getNextAutoId(table, key)
table = newTable
row[key] = nextID
}
break
}

View file

@ -219,3 +219,9 @@ export function isAttachmentField(
): field is AttachmentFieldMetadata {
return field.type === FieldType.ATTACHMENTS
}
export function isAutoColumnField(
field: FieldSchema
): field is AutoColumnFieldMetadata {
return field.type === FieldType.AUTO
}

View file

@ -21,7 +21,6 @@ export enum LockName {
PERSIST_WRITETHROUGH = "persist_writethrough",
QUOTA_USAGE_EVENT = "quota_usage_event",
APP_MIGRATION = "app_migrations",
PROCESS_AUTO_COLUMNS = "process_auto_columns",
PROCESS_USER_INVITE = "process_user_invite",
}