await prisma.$transaction(async (tx) => {
const cart = await tx.cart.findUniqueOrThrow({
where: { id: cartId },
});
const order = await tx.order.create({
data: { userId: cart.userId, total: cart.total },
});
// Aici crapă complet random cu "Transaction already closed"
await tx.cartItem.deleteMany({
where: { cartId: cart.id },
});
return order;
}, {
maxWait: 10000, // 10s
timeout: 15000, // 15s
});Salutare! Îmi vine să dau cu tastatura de perete de vreo trei zile și am zis să cer o a doua opinie aici, pe forum. Pe un proiect cu vreo 12k utilizatori activi zilnic, ne tot lovim în producție de celebra eroare: Transaction already closed. Apare complet random, de vreo 10-15 ori pe zi, și ne dă peste cap fluxul de checkout exact când lumea vrea să plătească.
Am tot săpat prin loguri, am citit zeci de thread-uri pe GitHub și pur și simplu am rămas fără idei. Partea cea mai frustrantă e că în staging, chiar și sub stress test intens cu K6, totul merge perfect. Nu reușesc să o reproduc local sub nicio formă.
Ce am încercat deja și n-a mers
Prima dată am crezut că e o problemă clasică de timeout de conexiune sau bază de date gâtuită. Postgres-ul nostru rulează pe AWS RDS (un db.t4g.medium care stă lejer în 20% utilizare CPU).
Așa că am trecut la acțiune cu setările standard:
- Am mărit
connection_limitla 25 în connection string și am urcatpool_timeoutla 30 de secunde. Nicio schimbare, numărul de erori în Sentry a rămas identic. - Am verificat timpii de execuție ai query-urilor. Cea mai lungă scriere din interiorul tranzacției durează maxim 600ms. Implicit, timeout-ul pe tranzacții interactive în Prisma este de 5000ms. Suntem mult sub limită.
- Am verificat tot codul linie cu linie să mă asigur că nu avem
await-uri uitate sau promisiuni care rulează în paralel aiurea în interiorul tranzacției. Toate operațiile folosesc instanța corectă detxprimită ca parametru.
Logurile din Sentry sunt extrem de inutile în cazul ăsta. Stack trace-ul ne trimite mereu la ultima interogare din bloc (ștergerea coșului), dar fără să ne spună de ce naiba s-a închis conexiunea înainte. Am activat temporar și logarea completă de query-uri pe producție, dar tot ce vedem este un DEALLOCATE trimis către Postgres, urmat instant de eroare.
Ipoteza mea curentă (și de ce e de rahat)
Bănuiesc că problema are legătură cu modul în care Node.js gestionează event loop-ul sub load mare. Dacă event loop-ul este blocat de vreo operație CPU-intensive (cum ar fi o criptare de parolă cu bcrypt sau o parsare mare de JSON) chiar în timp ce Prisma așteaptă răspunsul de la baza de date, tranzacția poate expira pe partea de client (în query engine-ul de Rust al Prisma), chiar dacă Postgres e liber și gata de treabă.
Este un trade-off destul de nasol la Prisma. DX-ul e genial, scrii cod curat și rapid, dar când vine vorba de debugging fin pe Node.js event loop blocking combinat cu un motor extern scris în Rust, devine un coșmar să prinzi vinovatul.
Am încercat să setăm și maxWait și timeout expliciți de 15 secunde pe tranzacție, dar tot primim eroarea asta, uneori chiar la doar 2 secunde de la inițierea tranzacției. Ceea ce n-are niciun sens matematic.
A mai lovit cineva ciudățenia asta în producție? Cum ați izolat-o? Orice idee e binevenită, că mie mi s-au terminat argumentele la daily-ul de dimineață.