return await prisma.$transaction(async (tx) => {
const product = await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantity } }
});
if (product.stock < 0) {
throw new Error('Stoc insuficient');
}
const order = await tx.order.create({
data: { userId, total: product.price * quantity }
});
// Apel extern în interiorul tranzacției
const invoice = await billingService.generateInvoice(order.id);
await tx.auditLog.create({
data: { action: 'ORDER_CREATED', orderId: order.id, invoiceId: invoice.id }
});
return order;
}, {
maxWait: 10000,
timeout: 15000
});Salutare tuturor! De vreo trei săptămâni mă chinui cu o problemă extrem de enervantă pe un API scris în Node.js cu Express și Prisma ORM. Apare exclusiv în producție, cam de 15-20 de ori pe zi la un volum de 8.000 de utilizatori activi, pe un endpoint critic de checkout.
Eroarea exactă din Sentry este PrismaClientKnownRequestError: Transaction already closed. Partea frustrantă e că pe local sau pe mediul de staging nu reușesc să o reproduc sub nicio formă, indiferent ce scripturi de load testing dau cu k6.
Contextul și cum arată codul
Folosim o tranzacție interactivă (prisma.$transaction) pentru că avem nevoie de atomicitate strictă. În interiorul ei facem vreo patru pași: verificăm și scădem stocul unui produs, creăm comanda în bază, salvăm un log de audit și emitem un apel către un serviciu extern de facturare.
Am izolat bucata de cod responsabilă și arată cam așa:
Ce am încercat deja și n-a funcționat
-
Ajustat timeout-ul tranzacției: Am crezut inițial că e un timeout clasic. Implicit, Prisma închide tranzacția interactivă după 5000ms. Am urcat parametrii
timeoutla 15000ms șimaxWaitla 10000ms. Rezultatul? Eroarea apare în continuare exact în aceleași condiții, fără să aștepte 15 secunde. -
Verificat connection pool-ul: Am presupus că rămânem fără conexiuni în PostgreSQL sub sarcină. Am mărit
connection_limitla 30 în string-ul de conexiune și am pus un PgBouncer în fața bazei. Utilization-ul pe bază e undeva la 35-40%, deci serverul de DB nu e sufocat și nu taie conexiunile violent. -
Audit de async/await: Am citit pe forumurile lor că dacă scapi un call asincron fără
awaitîn interiorul tranzacției, callback-ul principal se poate termina mai devreme, iar Prisma va închide tranzacția în timp ce apelul uitat încearcă încă să execute query-uri. Am luat fiecare linie la mână. Nu avemforEachcu async, avem doar buclefor...ofobișnuite și toate promisiunile auawaitcurat în față.
Ipoteza mea și unde m-am blocat
Singurul suspect rămas e apelul către API-ul extern de facturare pe care îl facem chiar în mijlocul tranzacției. Dacă acel serviciu are un spike de latență sau dă socket hang up, s-ar putea ca driverul de Postgres sau Prisma Engine (scris în Rust) să abandoneze tranzacția intern, marcând-o ca închisă înainte ca Node.js să prindă eroarea de rețea.
Pe de altă parte, știu că e o practică proastă să ții un HTTP call în interiorul unei tranzacții SQL, pentru că blochezi conexiunea din pool. Totuși, dacă scot apelul în afara tranzacției și crapă facturarea, rămân cu stocul scăzut și comanda creată fără factură, ceea ce înseamnă că trebuie să scriu logică manuală de rollback (compensating transactions).
Cineva care s-a mai lovit de asta în producție: există vreun flag ascuns de debug în Prisma Engine ca să văd exact secunda când tranzacția e marcată ca fiind closed? Sau ați rezolvat genul ăsta de race-condition mutând complet I/O-ul extern într-o coadă de fundal gen BullMQ?
Cum ați aborda problema asta fără să rescriu tot fluxul de checkout?