Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make auto ID row creation in parallel more robust. #13606

Merged
merged 6 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 3 additions & 35 deletions packages/server/src/api/controllers/row/staticFormula.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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
}
)
Comment on lines -157 to -173
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This locking is no longer required, and didn't work anyway. You'd need to wrap the entire row creation in this lock for it to consistently prevent 409s, and that could be prohibitively expensive.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good cleanup :)

} else {
throw err
}
}
await db.put(table)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need this? Are we not persisting the table already for the autoid?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily. If the table doesn't have an auto ID column it won't have been persisted.

}
const response = await db.put(row)
// for response, calculate the formulas for the enriched row
Expand Down
14 changes: 8 additions & 6 deletions packages/server/src/sdk/app/rows/tests/internal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ describe("sdk >> rows >> internal", () => {
lastID: 1,
},
},
_rev: expect.stringMatching("2-.*"),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we're saving the table and updating the _rev on it in our new getNextAutoId function, this needed adding to this test.

},
row: {
...row,
Expand Down Expand Up @@ -189,7 +190,6 @@ describe("sdk >> rows >> internal", () => {
type: FieldType.AUTO,
subtype: AutoFieldSubType.AUTO_ID,
autocolumn: true,
lastID: 0,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to remove this because the code handles it being undefined. May as well exercise that.

},
},
})
Expand All @@ -199,7 +199,7 @@ describe("sdk >> rows >> internal", () => {
await internalSdk.save(table._id!, row, config.getUser()._id)
}
await Promise.all(
makeRows(10).map(row =>
makeRows(20).map(row =>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to up the number of rows to be a bit more sure we're avoiding conflicts.

internalSdk.save(table._id!, row, config.getUser()._id)
)
)
Expand All @@ -209,19 +209,21 @@ describe("sdk >> rows >> internal", () => {
})

const persistedRows = await config.getRows(table._id!)
expect(persistedRows).toHaveLength(20)
expect(persistedRows).toHaveLength(30)
expect(persistedRows).toEqual(
expect.arrayContaining(
Array.from({ length: 20 }).map((_, i) =>
Array.from({ length: 30 }).map((_, i) =>
expect.objectContaining({ id: i + 1 })
)
)
)

const persistedTable = await config.getTable(table._id)
expect((table.schema.id as AutoColumnFieldMetadata).lastID).toBe(0)
expect(
(table.schema.id as AutoColumnFieldMetadata).lastID
).toBeUndefined()
expect((persistedTable.schema.id as AutoColumnFieldMetadata).lastID).toBe(
20
30
)
})
})
Expand Down
49 changes: 44 additions & 5 deletions packages/server/src/utilities/rowProcessor/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,6 +9,7 @@ import {
Row,
RowAttachment,
Table,
isAutoColumnField,
} from "@budibase/types"
import { cloneDeep } from "lodash/fp"
import {
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions packages/types/src/documents/app/table/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,9 @@ export function isAttachmentField(
): field is AttachmentFieldMetadata {
return field.type === FieldType.ATTACHMENTS
}

export function isAutoColumnField(
field: FieldSchema
): field is AutoColumnFieldMetadata {
samwho marked this conversation as resolved.
Show resolved Hide resolved
return field.type === FieldType.AUTO
}
1 change: 0 additions & 1 deletion packages/types/src/sdk/locks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}

Expand Down
Loading