Use retryTransaction instead of $transaction

This commit is contained in:
Konstantin Wohlwend 2025-07-31 15:43:48 -07:00
parent 7fe0dbf742
commit 9399b84f97
7 changed files with 15 additions and 6 deletions

View File

@ -1,6 +1,6 @@
import { listPermissions } from "@/lib/permissions";
import { Tenancy } from "@/lib/tenancies";
import { getPrismaClientForTenancy } from "@/prisma-client";
import { getPrismaClientForTenancy, retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { SmartRequestAuth } from "@/route-handlers/smart-request";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
@ -57,7 +57,7 @@ async function ensureUserCanManageApiKeys(
// Check team API key permissions
if (options.teamId !== undefined) {
const userId = auth.user.id;
const hasManageApiKeysPermission = await prisma.$transaction(async (tx) => {
const hasManageApiKeysPermission = await retryTransaction(prisma, async (tx) => {
const permissions = await listPermissions(tx, {
scope: 'team',
tenancy: auth.tenancy,

View File

@ -1,7 +1,7 @@
import { getSharedEmailConfig, sendEmail } from "@/lib/emails";
import { listPermissions } from "@/lib/permissions";
import { getTenancy } from "@/lib/tenancies";
import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client";
import { getPrismaClientForTenancy, globalPrismaClient, retryTransaction } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { KnownErrors } from "@stackframe/stack-shared";
import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
@ -27,7 +27,7 @@ export const POST = createSmartRouteHandler({
async handler({ body }) {
// Get the API key and revoke it. We use a transaction to ensure we do not send emails multiple times.
// We don't support revoking API keys in tenancies with non-global source of truth atm.
const updatedApiKey = await globalPrismaClient.$transaction(async (tx) => {
const updatedApiKey = await retryTransaction(globalPrismaClient, async (tx) => {
// Find the API key in the database
const apiKey = await tx.projectApiKey.findUnique({
where: {
@ -116,7 +116,7 @@ export const POST = createSmartRouteHandler({
const prisma = await getPrismaClientForTenancy(tenancy);
const userIdsWithManageApiKeysPermission = await prisma.$transaction(async (tx) => {
const userIdsWithManageApiKeysPermission = await retryTransaction(prisma, async (tx) => {
if (!updatedApiKey.teamId) {
throw new StackAssertionError("Team ID not specified in team API key");
}

View File

@ -285,6 +285,7 @@ import.meta.vitest?.test("applies migration while running concurrent queries", r
}));
import.meta.vitest?.test("applies migration while running an interactive transaction", runTest(async ({ expect, prismaClient, dbURL }) => {
// eslint-disable-next-line no-restricted-syntax
return await prismaClient.$transaction(async (tx, ...args) => {
await runMigrationNeeded({
prismaClient,
@ -304,6 +305,7 @@ import.meta.vitest?.test("applies migration while running an interactive transac
import.meta.vitest?.test("applies migration while running concurrent interactive transactions", runTest(async ({ expect, prismaClient, dbURL }) => {
const runTransactionWithMigration = async (testValue: string) => {
// eslint-disable-next-line no-restricted-syntax
return await prismaClient.$transaction(async (tx) => {
await runMigrationNeeded({
prismaClient,

View File

@ -39,6 +39,7 @@ async function getAppliedMigrations(options: {
prismaClient: PrismaClient,
schema: string,
}) {
// eslint-disable-next-line no-restricted-syntax
const [_1, _2, _3, appliedMigrations] = await options.prismaClient.$transaction([
options.prismaClient.$executeRaw`SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_ID})`,
options.prismaClient.$executeRaw(Prisma.sql`
@ -143,6 +144,7 @@ export async function applyMigrations(options: {
VALUES (${migration.migrationName}, clock_timestamp())
`);
try {
// eslint-disable-next-line no-restricted-syntax
await options.prismaClient.$transaction(transaction);
} catch (e) {
const error = getMigrationError(e);

View File

@ -234,7 +234,7 @@ export async function createOrUpdateProject(
// Update owner metadata
const internalEnvironmentConfig = await rawQuery(globalPrismaClient, getRenderedEnvironmentConfigQuery({ projectId: "internal", branchId: DEFAULT_BRANCH_ID }));
const prisma = await getPrismaClientForSourceOfTruth(internalEnvironmentConfig.sourceOfTruth, DEFAULT_BRANCH_ID);
await prisma.$transaction(async (tx) => {
await retryTransaction(prisma, async (tx) => {
for (const userId of options.ownerIds ?? []) {
const projectUserTx = await tx.projectUser.findUnique({
where: {

View File

@ -129,6 +129,7 @@ export async function retryTransaction<T>(client: PrismaClient, fn: (tx: PrismaC
return await traceSpan(`transaction attempt #${attemptIndex}`, async (attemptSpan) => {
const attemptRes = await (async () => {
try {
// eslint-disable-next-line no-restricted-syntax
return Result.ok(await client.$transaction(async (tx, ...args) => {
let res;
try {

View File

@ -103,6 +103,10 @@ module.exports = {
"Identifier[name='localeCompare']",
message: "Use stringCompare() from utils/strings.tsx instead of String.prototype.localeCompare.",
},
{
selector: "CallExpression > MemberExpression[property.name='$transaction']",
message: "Calling .$transaction is disallowed. Use retryTransaction() instead.",
},
],
"@typescript-eslint/no-misused-promises": [
"error",