From 4dd8fdbd04b1963105c28c5b5cd88e53b91c8a34 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 25 Oct 2023 15:23:57 +0200 Subject: [PATCH] [backend] Refactor database transactions This moves all code that isn't a direct call to transactionalEntityManager to outside of the transaction blocks, and removes all transaction blocks that were unnecessary --- packages/backend/src/misc/fetch-meta.ts | 59 +++--- .../src/remote/activitypub/models/person.ts | 169 +++++++++--------- .../backend/src/server/api/common/signup.ts | 97 +++++----- .../api/endpoints/admin/accounts/hosted.ts | 25 ++- .../server/api/endpoints/admin/update-meta.ts | 25 ++- .../src/services/create-system-user.ts | 91 +++++----- packages/backend/src/services/note/create.ts | 44 ++--- 7 files changed, 248 insertions(+), 262 deletions(-) diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts index 4fb69f923..3d66f2d63 100644 --- a/packages/backend/src/misc/fetch-meta.ts +++ b/packages/backend/src/misc/fetch-meta.ts @@ -1,6 +1,7 @@ import { db } from "@/db/postgre.js"; import { Meta } from "@/models/entities/meta.js"; import push from 'web-push'; +import { Metas } from "@/models/index.js"; let cache: Meta; @@ -33,41 +34,31 @@ export function metaToPugArgs(meta: Meta): object { export async function fetchMeta(noCache = false): Promise { if (!noCache && cache) return cache; - return await db.transaction(async (transactionalEntityManager) => { - // New IDs are prioritized because multiple records may have been created due to past bugs. - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: "DESC", - }, - }); - - const meta = metas[0]; - - if (meta) { - cache = meta; - return meta; - } else { - const { publicKey, privateKey } = push.generateVAPIDKeys(); - - // If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert. - const saved = await transactionalEntityManager - .upsert( - Meta, - { - id: "x", - swPublicKey: publicKey, - swPrivateKey: privateKey, - }, - ["id"], - ) - .then((x) => - transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]), - ); - - cache = saved; - return saved; - } + // New IDs are prioritized because multiple records may have been created due to past bugs. + const meta = await Metas.findOne({ + where: {}, + order: { + id: "DESC", + }, }); + + if (meta) { + cache = meta; + return meta; + } + + const { publicKey, privateKey } = push.generateVAPIDKeys(); + const data = { + id: "x", + swPublicKey: publicKey, + swPrivateKey: privateKey, + }; + + // If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert. + await Metas.upsert(data, ["id"]); + + cache = await Metas.findOneByOrFail({ id: data.id }); + return cache; } setInterval(() => { diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index e474e1f54..0812f7e7d 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -294,80 +294,77 @@ export async function createPerson( } } - // Create user - let user: IRemoteUser; + // Prepare objects + let user = new User({ + id: genId(), + avatarId: null, + bannerId: null, + createdAt: new Date(), + lastFetchedAt: new Date(), + name: truncate(person.name, nameLength), + isLocked: !!person.manuallyApprovesFollowers, + movedToUri: person.movedTo, + alsoKnownAs: person.alsoKnownAs, + isExplorable: !!person.discoverable, + username: person.preferredUsername, + usernameLower: person.preferredUsername!.toLowerCase(), + host, + inbox: person.inbox, + sharedInbox: + person.sharedInbox || + (person.endpoints ? person.endpoints.sharedInbox : undefined), + followersUri: person.followers + ? getApId(person.followers) + : undefined, + followersCount: + followersCount !== undefined + ? followersCount + : person.followers && + typeof person.followers !== "string" && + isCollectionOrOrderedCollection(person.followers) + ? person.followers.totalItems + : undefined, + followingCount: + followingCount !== undefined + ? followingCount + : person.following && + typeof person.following !== "string" && + isCollectionOrOrderedCollection(person.following) + ? person.following.totalItems + : undefined, + featured: person.featured ? getApId(person.featured) : undefined, + uri: person.id, + tags, + isBot, + isCat: (person as any).isCat === true, + }) as IRemoteUser; + + const profile = new UserProfile({ + userId: user.id, + description: person.summary + ? await htmlToMfm(truncate(person.summary, summaryLength), person.tag) + : null, + url: url, + fields, + birthday: bday ? bday[0] : null, + location: person["vcard:Address"] || null, + userHost: host, + }); + + const publicKey = person.publicKey + ? new UserPublickey({ + userId: user.id, + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem, + }) + : null; + try { - // Start transaction + // Save the objects atomically using a db transaction, note that we should never run any code in a transaction block directly await db.transaction(async (transactionalEntityManager) => { - user = (await transactionalEntityManager.save( - new User({ - id: genId(), - avatarId: null, - bannerId: null, - createdAt: new Date(), - lastFetchedAt: new Date(), - name: truncate(person.name, nameLength), - isLocked: !!person.manuallyApprovesFollowers, - movedToUri: person.movedTo, - alsoKnownAs: person.alsoKnownAs, - isExplorable: !!person.discoverable, - username: person.preferredUsername, - usernameLower: person.preferredUsername!.toLowerCase(), - host, - inbox: person.inbox, - sharedInbox: - person.sharedInbox || - (person.endpoints ? person.endpoints.sharedInbox : undefined), - followersUri: person.followers - ? getApId(person.followers) - : undefined, - followersCount: - followersCount !== undefined - ? followersCount - : person.followers && - typeof person.followers !== "string" && - isCollectionOrOrderedCollection(person.followers) - ? person.followers.totalItems - : undefined, - followingCount: - followingCount !== undefined - ? followingCount - : person.following && - typeof person.following !== "string" && - isCollectionOrOrderedCollection(person.following) - ? person.following.totalItems - : undefined, - featured: person.featured ? getApId(person.featured) : undefined, - uri: person.id, - tags, - isBot, - isCat: (person as any).isCat === true, - }), - )) as IRemoteUser; - - await transactionalEntityManager.save( - new UserProfile({ - userId: user.id, - description: person.summary - ? await htmlToMfm(truncate(person.summary, summaryLength), person.tag) - : null, - url: url, - fields, - birthday: bday ? bday[0] : null, - location: person["vcard:Address"] || null, - userHost: host, - }), - ); - - if (person.publicKey) { - await transactionalEntityManager.save( - new UserPublickey({ - userId: user.id, - keyId: person.publicKey.id, - keyPem: person.publicKey.publicKeyPem, - }), - ); - } + await transactionalEntityManager.save(user); + await transactionalEntityManager.save(profile); + if (publicKey) await transactionalEntityManager.save(publicKey); }); } catch (e) { // duplicate key error @@ -754,21 +751,23 @@ export async function updateFeatured(userId: User["id"], resolver?: Resolver, li .map((item) => limit(() => resolveNote(item, resolver, limiter))), ); - await db.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.delete(UserNotePining, { + // Prepare the objects + // For now, generate the id at a different time and maintain the order. + const data: Partial[] = []; + let td = 0; + for (const note of featuredNotes.filter((note) => note != null)) { + td -= 1000; + data.push({ + id: genId(new Date(Date.now() + td)), + createdAt: new Date(), userId: user.id, + noteId: note!.id, }); + } - // For now, generate the id at a different time and maintain the order. - let td = 0; - for (const note of featuredNotes.filter((note) => note != null)) { - td -= 1000; - transactionalEntityManager.insert(UserNotePining, { - id: genId(new Date(Date.now() + td)), - createdAt: new Date(), - userId: user.id, - noteId: note!.id, - }); - } + // Save the objects atomically using a db transaction, note that we should never run any code in a transaction block directly + await db.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); + await transactionalEntityManager.insert(UserNotePining, data); }); } diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts index 6beae2c78..19d4f17aa 100644 --- a/packages/backend/src/server/api/common/signup.ts +++ b/packages/backend/src/server/api/common/signup.ts @@ -84,58 +84,55 @@ export async function signup(opts: { ), ); - let account!: User; - - // Start transaction - await db.transaction(async (transactionalEntityManager) => { - const exist = await transactionalEntityManager.findOneBy(User, { - usernameLower: username.toLowerCase(), - host: IsNull(), - }); - - if (exist) throw new Error(" the username is already used"); - - account = await transactionalEntityManager.save( - new User({ - id: genId(), - createdAt: new Date(), - username: username, - usernameLower: username.toLowerCase(), - host: toPunyNullable(host), - token: secret, - isAdmin: - (await Users.countBy({ - host: IsNull(), - isAdmin: true, - })) === 0, - }), - ); - - await transactionalEntityManager.save( - new UserKeypair({ - publicKey: keyPair[0], - privateKey: keyPair[1], - userId: account.id, - }), - ); - - await transactionalEntityManager.save( - new UserProfile({ - userId: account.id, - autoAcceptFollowed: true, - password: hash, - }), - ); - - await transactionalEntityManager.save( - new UsedUsername({ - createdAt: new Date(), - username: username.toLowerCase(), - }), - ); + const exist = await Users.findOneBy({ + usernameLower: username.toLowerCase(), + host: IsNull(), }); - usersChart.update(account, true); + if (exist) throw new Error("The username is already in use"); + // Prepare objects + const user = new User({ + id: genId(), + createdAt: new Date(), + username: username, + usernameLower: username.toLowerCase(), + host: toPunyNullable(host), + token: secret, + isAdmin: + (await Users.countBy({ + host: IsNull(), + isAdmin: true, + })) === 0, + }); + + const userKeypair = new UserKeypair({ + publicKey: keyPair[0], + privateKey: keyPair[1], + userId: user.id, + }); + + const userProfile = new UserProfile({ + userId: user.id, + autoAcceptFollowed: true, + password: hash, + }); + + const usedUsername = new UsedUsername({ + createdAt: new Date(), + username: username.toLowerCase(), + }); + + // Save the objects atomically using a db transaction, note that we should never run any code in a transaction block directly + await db.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.save(user); + await transactionalEntityManager.save(userKeypair); + await transactionalEntityManager.save(userProfile); + await transactionalEntityManager.save(usedUsername); + }); + + const account = await Users.findOneByOrFail({ id: user.id }); + + usersChart.update(account, true); return { account, secret }; } diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts index a7b6e95c2..a5423e154 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts @@ -3,6 +3,7 @@ import { Meta } from "@/models/entities/meta.js"; import { insertModerationLog } from "@/services/insert-moderation-log.js"; import { db } from "@/db/postgre.js"; import define from "../../../define.js"; +import { Metas } from "@/models/index.js"; export const meta = { tags: ["admin"], @@ -106,21 +107,19 @@ export default define(meta, paramDef, async (ps, me) => { if (config.summalyProxyUrl !== undefined) { set.summalyProxy = config.summalyProxyUrl; } - await db.transaction(async (transactionalEntityManager) => { - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: "DESC", - }, - }); - const meta = metas[0]; - - if (meta) { - await transactionalEntityManager.update(Meta, meta.id, set); - } else { - await transactionalEntityManager.save(Meta, set); - } + const meta = await Metas.findOne({ + where: {}, + order: { + id: "DESC", + }, }); + + if (meta) + await Metas.update(meta.id, set); + else + await Metas.save(set); + insertModerationLog(me, "updateMeta"); } return hosted; diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 89f657ef4..e4f9b51a7 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -2,6 +2,7 @@ import { Meta } from "@/models/entities/meta.js"; import { insertModerationLog } from "@/services/insert-moderation-log.js"; import { db } from "@/db/postgre.js"; import define from "../../define.js"; +import { Metas } from "@/models/index.js"; export const meta = { tags: ["admin"], @@ -546,21 +547,17 @@ export default define(meta, paramDef, async (ps, me) => { } } - await db.transaction(async (transactionalEntityManager) => { - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: "DESC", - }, - }); - - const meta = metas[0]; - - if (meta) { - await transactionalEntityManager.update(Meta, meta.id, set); - } else { - await transactionalEntityManager.save(Meta, set); - } + const meta = await Metas.findOne({ + where: {}, + order: { + id: "DESC", + }, }); + if (meta) + await Metas.update(meta.id, set); + else + await Metas.save(set); + insertModerationLog(me, "updateMeta"); }); diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts index 24536090a..60901e186 100644 --- a/packages/backend/src/services/create-system-user.ts +++ b/packages/backend/src/services/create-system-user.ts @@ -9,8 +9,9 @@ import { UserKeypair } from "@/models/entities/user-keypair.js"; import { UsedUsername } from "@/models/entities/used-username.js"; import { db } from "@/db/postgre.js"; import { hashPassword } from "@/misc/password.js"; +import { Users } from "@/models/index.js"; -export async function createSystemUser(username: string) { +export async function createSystemUser(username: string): Promise { const password = uuid(); // Generate hash of password @@ -23,49 +24,51 @@ export async function createSystemUser(username: string) { let account!: User; - // Start transaction - await db.transaction(async (transactionalEntityManager) => { - const exist = await transactionalEntityManager.findOneBy(User, { - usernameLower: username.toLowerCase(), - host: IsNull(), - }); - - if (exist) throw new Error("the user is already exists"); - - account = await transactionalEntityManager - .insert(User, { - id: genId(), - createdAt: new Date(), - username: username, - usernameLower: username.toLowerCase(), - host: null, - token: secret, - isAdmin: false, - isLocked: true, - isExplorable: false, - isBot: true, - }) - .then((x) => - transactionalEntityManager.findOneByOrFail(User, x.identifiers[0]), - ); - - await transactionalEntityManager.insert(UserKeypair, { - publicKey: keyPair.publicKey, - privateKey: keyPair.privateKey, - userId: account.id, - }); - - await transactionalEntityManager.insert(UserProfile, { - userId: account.id, - autoAcceptFollowed: false, - password: hash, - }); - - await transactionalEntityManager.insert(UsedUsername, { - createdAt: new Date(), - username: username.toLowerCase(), - }); + const exist = await Users.findOneBy({ + usernameLower: username.toLowerCase(), + host: IsNull(), }); - return account; + if (exist) throw new Error("the user is already exists"); + + // Prepare objects + const user = { + id: genId(), + createdAt: new Date(), + username: username, + usernameLower: username.toLowerCase(), + host: null, + token: secret, + isAdmin: false, + isLocked: true, + isExplorable: false, + isBot: true, + }; + + const userKeypair = { + publicKey: keyPair.publicKey, + privateKey: keyPair.privateKey, + userId: user.id, + }; + + const userProfile = { + userId: user.id, + autoAcceptFollowed: false, + password: hash, + }; + + const usedUsername = { + createdAt: new Date(), + username: username.toLowerCase(), + } + + // Save the objects atomically using a db transaction, note that we should never run any code in a transaction block directly + await db.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.insert(User, user); + await transactionalEntityManager.insert(UserKeypair, userKeypair); + await transactionalEntityManager.insert(UserProfile, userProfile); + await transactionalEntityManager.insert(UsedUsername, usedUsername); + }); + + return Users.findOneByOrFail({ id: user.id }); } diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 9a549dfa2..c41897c34 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -756,30 +756,30 @@ async function insertNote( // 投稿を作成 try { if (insert.hasPoll) { - // Start transaction + // Prepare objects + if (!data.poll) throw new Error("Empty poll data"); + + let expiresAt: Date | null; + if (!data.poll.expiresAt || isNaN(data.poll.expiresAt.getTime())) { + expiresAt = null; + } else { + expiresAt = data.poll.expiresAt; + } + + const poll = new Poll({ + noteId: insert.id, + choices: data.poll.choices, + expiresAt, + multiple: data.poll.multiple, + votes: new Array(data.poll.choices.length).fill(0), + noteVisibility: insert.visibility, + userId: user.id, + userHost: user.host, + }); + + // Save the objects atomically using a db transaction, note that we should never run any code in a transaction block directly await db.transaction(async (transactionalEntityManager) => { - if (!data.poll) throw new Error("Empty poll data"); - await transactionalEntityManager.insert(Note, insert); - - let expiresAt: Date | null; - if (!data.poll.expiresAt || isNaN(data.poll.expiresAt.getTime())) { - expiresAt = null; - } else { - expiresAt = data.poll.expiresAt; - } - - const poll = new Poll({ - noteId: insert.id, - choices: data.poll.choices, - expiresAt, - multiple: data.poll.multiple, - votes: new Array(data.poll.choices.length).fill(0), - noteVisibility: insert.visibility, - userId: user.id, - userHost: user.host, - }); - await transactionalEntityManager.insert(Poll, poll); }); } else {