return await this.prisma.$transaction(
async (tx) => {
const product = await tx.product.findUnique({
where: { id: productId },
});
if (product.stock < quantity) {
throw new BadRequestException('Stoc insuficient');
}
await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantity } },
});
// Crapă uneori direct aici cu 'Transaction already closed'
return await tx.order.create({
data: { productId, quantity, userId },
});
},
{
maxWait: 5000,
timeout: 10000,
}
);Salutare tuturor. Am dat peste o problemă urâtă rău pe un microserviciu de checkout (Node.js 20 cu NestJS și Prisma 5.12) și sincer am epuizat toate variantele clasice de debugging.
Avem o eroare de Transaction already closed care apare complet random în producție. Se întâmplă cam de 40-50 de ori pe zi la un volum de 12.000 RPM, dar în staging (unde avem traficul sintetic generat cu k6) nu am reușit să o reproduc neam, oricât am tras de ea.
Cum arată codul și ce facem acolo
Scenariul e destul de standard: procesăm o comandă, scădem stocul pentru vreo 3-5 produse și salvăm un log de audit. Totul e împachetat într-o tranzacție interactivă Prisma (prisma.$transaction(async (tx) => { ... })).
Nivelul de procesare e destul de simplu, nu facem fețuiri de API-uri externe în interiorul tranzacției (știu regula de aur, nu blochezi DB-ul așteptând HTTP responses). Codul e foarte simplu, un findUnique, două update-uri și un create la final.
Ce am încercat deja și NU a rezolvat problema
-
Am crescut timeout-ul explicit. Am pus
timeout: 10000șimaxWait: 5000în opțiunile tranzacției. Mă gândeam că poate e lag de rețea. Degeaba, eroarea apare la fel de des și uneori crapă după doar 200ms, nu după cele 10 secunde setate. -
Am verificat conexiunile de PostgreSQL și PgBouncer. Suntem pe RDS Postgres 15. Avem PgBouncer în fața bazei de date în session mode. CPU-ul pe DB e la sub 20%, iar numărul de conexiuni active e pe la 30 din maxim 200. Nicio urmă de query-uri blocate (
pg_stat_activitye curat). -
Am scanat codul după
await-uri lipsă. Am verificat de trei ori cu ESLint și la mână dacă nu cumva scapă vreunPromiseneașteptat în interiorul callback-ului de tranzacție care să meargă în fundal și să încerce să foloseascătxdupă ce callback-ul a dat return. Totul pare curat. -
Am făcut upgrade la Prisma. Eram pe v5.8.0, am urcat la 5.12.0 sperând să fie un bug rezolvat în engine-ul de Rust. Nicio diferență.
Suspiciunea mea: Event Loop Lag sau Rust Engine Issue?
Interactive transactions în Prisma funcționează puțin diferit față de un driver nativ cum e pg. Cât timp callback-ul tău rulează, Engine-ul de Rust ține conexiunea deschisă și pasează mesaje peste IPC/HTTP local cu procesul Node.js.
Bănuiala mea e că avem spike-uri scurte de Event Loop Lag pe pod-urile de Kubernetes. Dacă un Garbage Collection sau o altă operație CPU-intensive blochează Event Loop-ul Node.js pentru 2-3 secunde, Engine-ul de Rust crede că JS-ul a abandoned tranzacția și îi dă rollback client-side. Când Event Loop-ul își revine și execută următorul await tx.product.update(), Prisma aruncă Transaction already closed pentru că socket-ul intern e închis.
Trade-off-ul masiv la tranzacțiile interactive din Prisma e fix dependența asta strânsă de sănătatea Event Loop-ului JS din timpul executării. Dacă eram pe un SQL chior (BEGIN ... COMMIT), conexiunea rămânea pur și simplu în așteptare pe serverul de DB.
A mai lovit cineva zidul ăsta în prod la trafic mare? Cum ați izolat problema — ați trecut pe tranzacții secvențiale (prisma.$transaction([ ... ])) sau ați găsit vreun tweak la nivel de OS / Node flag-uri?
Orice idee e binevenită că deja îmi vine să rescriu tot modulul pe pg-promise.