diff --git a/packages/server/src/api/controllers/row/staticFormula.ts b/packages/server/src/api/controllers/row/staticFormula.ts index a75a6cd2cc..a6a02952d3 100644 --- a/packages/server/src/api/controllers/row/staticFormula.ts +++ b/packages/server/src/api/controllers/row/staticFormula.ts @@ -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 diff --git a/packages/server/src/sdk/app/rows/tests/internal.spec.ts b/packages/server/src/sdk/app/rows/tests/internal.spec.ts index 877bd1e6dc..9f7f68ca7b 100644 --- a/packages/server/src/sdk/app/rows/tests/internal.spec.ts +++ b/packages/server/src/sdk/app/rows/tests/internal.spec.ts @@ -145,6 +145,7 @@ describe("sdk >> rows >> internal", () => { lastID: 1, }, }, + _rev: expect.stringMatching("2-.*"), }, row: { ...row, diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index e69cfa471a..c44326ff42 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -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 } diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts index 18fbeae719..407199860b 100644 --- a/packages/types/src/documents/app/table/schema.ts +++ b/packages/types/src/documents/app/table/schema.ts @@ -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 +} diff --git a/packages/types/src/sdk/locks.ts b/packages/types/src/sdk/locks.ts index c7c028a135..d455e95353 100644 --- a/packages/types/src/sdk/locks.ts +++ b/packages/types/src/sdk/locks.ts @@ -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", }