await prisma.$transaction(async (tx) => {
const order = await tx.orders.create({ data: orderData });
// Oare crapă de la Promise.all paralel în interiorul tranzacției?
await Promise.all(
items.map(item =>
tx.stocks.update({
where: { id: item.id },
data: { quantity: { decrement: item.qty } }
})
)
);
await tx.logs.create({
data: { orderId: order.id, status: 'SUCCESS' }
});
}, {
maxWait: 10000,
timeout: 15000
});Salutare tuturor. Dau cu capul de pragul de sus de trei zile cu o problemă în Prisma și simt că îmi pierd mințile. Pe local totul merge brici, dar în producție îmi crapă tranzacțiile complet random.
Avem un serviciu mediu, cam 15k useri activi zilnic și în jur de 200-300 de request-uri pe minut pe endpoint-ul de checkout. Stack-ul e clasic: Next.js (App Router) rulat în ECS, Postgres pe AWS RDS (pe un t4g.medium care stă relaxat la 12% CPU) și Prisma v5.12.0.
Problema care mă disperă
La finalul fluxului de checkout, am o tranzacție interactivă ($transaction) care trebuie să fie atomică. Creează o comandă, scade stocul pentru produsele cumpărate și salvează un log de audit. Nimic SF.
Din când în când – cam la 1% din tranzacții – Sentry se aprinde ca un pom de Crăciun:
PrismaClientKnownRequestError: Transaction already closed: Transaction is no longer usable...
Partea ciudată e că eroarea nu apare la timeout (adică după cele 5-10 secunde configurate), ci uneori crapă instant, după doar 150ms.
Ce am încercat deja (și n-a mers)
1. Am umblat la timeout-uri. Inițial am crezut că e un spike de rețea sau baza de date e temporar ocupată. Am crescut parametrii tranzacției grosolan, doar ca să elimin ipoteza asta. Am pus maxWait: 15000 și timeout: 20000. Nicio schimbare. Crapă la fel de random, uneori imediat după primul insert.
2. Connection Pooling (PgBouncer). Serverless-ul ne-a forțat să folosim un pooler. Avem conexiunea rutată prin PgBouncer în mod transaction. Am citit pe GitHub că Prisma are niște bube istorice când lucrează cu PgBouncer și tranzacții interactive, pentru că Prisma încearcă să țină conexiunea deschisă dar PgBouncer o poate elibera prematur dacă detectează inactivitate sau alte query-uri paralele. Am încercat să trec temporar pe conexiune directă (fără PgBouncer) pe un environment de staging cu trafic simulat. Problema a scăzut ca frecvență, dar tot a apărut de vreo 2 ori în 5000 de request-uri.
3. Am curățat callback-ul de tranzacție. Înainte aveam niște apeluri de API externe (trimis mail prin Resend, calcul de taxe prin third-party) în interiorul $transaction. Știu, e anti-pattern grav. Le-am scos pe toate. Acum am doar query-uri pure de bază de date. Tot se întâmplă.
Codul cu pricina
Suspectez că problema ar putea fi de la Promise.all-ul ăla din interior. Dacă unul dintre update-uri e mai lent, oare Prisma închide automat tranzacția pe celelalte fire de execuție?
Trade-off-ul cu Prisma
Îmi place Prisma la nebunie pentru DX. Scrii schema, ai tipuri peste tot, autocompletion-ul te salvează de o grămadă de bug-uri stupide. Dar când vine vorba de debugging pe probleme de conexiune sau tranzacții sub load, ești complet pe cont propriu. Logurile lor de query (query, info, warn) nu îți spun de ce s-a închis conexiunea, doar că s-a închis. Dacă foloseam direct SQL sau un query builder mai low-level ca Knex, probabil vedeam eroarea exactă din driverul de Postgres.
A mai lovit cineva problema asta cu „Transaction already closed” în producție pe Postgres? Să fie o problemă de conexiuni concurente din Next.js (care tot deschide instanțe noi de Prisma Client la cold start-uri)? Orice idee e binevenită, că mie mi s-au terminat opțiunile.