return await prisma.$transaction(
async (tx) => {
const product = await tx.product.findUnique({
where: { id: productId },
});
if (!product || product.stock < quantity) {
throw new Error('STOK_INSUFFICIENT');
}
await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantity } },
});
return await tx.order.create({
data: { userId, productId, quantity },
});
},
{
maxWait: 10000,
timeout: 15000,
}
);Salutare. Mă confrunt de vreo 3 zile cu o chestie super enervantă pe un serviciu de Node.js (NestJS + Prisma 5.11) care duce în jur de 450-500 RPS în orele de vârf. Din când în când, complet aleatoriu, îmi crapă tranzacțiile interactive cu celebra eroare Transaction already closed.
Ce e în spatele codului și cum se manifestă
Avem un flow de checkout unde fac câteva verificări de stoc, creez comanda și scad stocul. Nimic SF, vreo 4 query-uri scurte într-un $transaction. În staging, unde am rulat teste de sarcină cu K6 la 1000 RPS, totul e verde și funcționează brici. În producție însă, din 10.000 de tranzacții, vreo 15-20 crapă cu chestia asta.
Partea dubioasă e că timpul total de execuție al tranzacției – măsurat cu APM (Datadog) – este sub 180ms per total. Deci nu are cum să ajungă la timeout-ul default de 5000ms. În plus, conexiunea la bază nu pică, alte query-uri simple merg în paralel fără probleme.
Ce am încercat deja și n-a mers
Am zis inițial că e o problemă clasică de timeout din cauza vreunui query mai lent pe stoc. Am urcat maxWait la 10000ms și timeout la 15000ms. Nicio schimbare, rata de erori e fix aceeași.
Apoi m-am gândit la infrastructură. Folosim AWS RDS PostgreSQL cu PgBouncer în front în transaction mode. Știu că Prisma interactive transactions au nevoie de o conexiune dedicată pe durata blocului async, așa că am setat conexiunile direct prin portul de session mode sau direct la DB bypass-uind PgBouncer-ul pentru tranzacții. Tot degeaba, erorile încă apar.
M-am uitat atent și după un potențial async leak. Nu avem await-uri uitate, nu facem call-uri HTTP către API-uri externe în interiorul tranzacției și nu există bucle lungi care să blocheze Event Loop-ul.
Unde mai bănuiesc că ar fi buba
Am rămas cu doar doi suspecți pe care încă nu am reușit să-i izolez complet:
- Event Loop Lag – e posibil ca sub load mare, Garbage Collector-ul sau alt request paralelizat să blocheze thread-ul de Node.js pentru câteva sute de milisecunde. Dacă pachetul de
COMMITîntârzie să fie trimis de la Node către Query Engine-ul nativ de Rust din Prisma, engine-ul s-ar putea să considere tranzacția abandonată și să îi dea rollback automat. - Comunicarea IPC / Socket între Prisma Client (JS) și Prisma Engine (Rust binary). Avem limite destul de strânse pe pod-urile de Kubernetes (512MB RAM per replica) și am observat un mic spike de memorie exact când apare eroarea.
A mai lovit cineva zidul ăsta?
Nu aș vrea să renunț la DX-ul oferit de Prisma doar pentru un endpoint, dar dacă nu îi dau de capăt săptămâna asta, probabil va trebui să rescriu bucata asta cu pg-pool și SQL nativ. Merge excelent ORM-ul pentru 95% din aplicație, dar pe scrieri concurente critice pare că își prinde urechile fără un motiv clar.
Dacă ați mai întâlnit un behavior similar pe Prisma v5+, cum ați rezolvat-o? A fost nevoie de vreun flag ascuns, ajustări pe driver-ul npg sau pur și simplu ați trecut pe $transaction secvențial (matrice de query-uri)?