export async function processOrder(userId: string, items: CartItem[]) {
return await prisma.$transaction(async (tx) => {
for (const item of items) {
const product = await tx.product.findUnique({
where: { id: item.productId }
});
if (!product || product.stock < item.quantity) {
throw new Error(`Stoc insuficient pentru ${item.productId}`);
}
// Aici crapă random cu 'Transaction already closed'
await tx.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } }
});
}
return await tx.order.create({
data: { userId, items: { create: items } }
});
}, {
maxWait: 10000,
timeout: 15000
});
}Salutare tuturor. Am o problemă care îmi mănâncă zilele de marți încoace pe un serviciu de e-commerce care duce în jur de 12k-15k requesturi pe minut în orele de vârf.
Apare o eroare enervantă de la Prisma: Transaction already closed: A query failed because the transaction was already closed. Partea proastă e că se întâmplă complet aleatoriu, cam la 1 din 800-1000 de tranzacții. În staging sau pe mediul local n-am reușit sub nicio formă să o reproduc, nici măcar cu k6 când am tras cu 500 VUs în el.
Contextul și codul afectat
Avem un flux de checkout unde trebuie să scădem stocul, să creăm comanda și să salvăm un ledger de tranzacții. Folosim o tranzacție interactivă (prisma.$transaction(async (tx) => ...)), pentru că avem nevoie de logica de business direct în interiorul blocurilor, verificând stocul atomic înainte de insert.
Instanța de PostgreSQL (Managed pe RDS, db.r6g.xlarge) rulează lejer. CPU-ul stă în 20%, conexiunile sunt stabile, iar PgBouncer-ul e configurat pe transaction mode.
Ce am încercat deja (și n-a funcționat)
1. Am mărit timeout-urile explicite.
Am zis că poate durează apelurile externe mai mult decât e cazul (deși n-avem fetch în interiorul tranzacției). Am setat maxWait: 10000 și timeout: 15000. Nimic schimbat. Eroarea apare uneori după doar 200ms de la inițierea tranzacției, deci clar nu atinge pragul de 15 secunde.
2. Am verificat promise-urile orfane.
M-am gândit că poate cineva a scăpat vreun forEach cu async sau vreun Promise.all parțial așteptat în interiorul callback-ului de tranzacție, iar Prisma închide tranzacția în fundal înainte să se termine execuția. Am luat tot fișierul la mână, am rescris secvențial cu for...of absolut tot. Tot pică.
3. Upgrade de Prisma și reconfigurat pool-ul.
Eram pe Prisma 5.2.0. Am făcut upgrade la 5.12.0, sperând că e un bug intern de engine (folosim query engine-ul în rust pe Node 20). Am jucat și cu connection_limit în URL-ul de Postgres, coborând de la 50 la 20 pentru a lăsa PgBouncer să gestioneze mai bine coada. Rezultatul? Aceeași eroare, exact în aceleași momente de spike.
Măsurători și detalii ciudate
Ce mă bagă complet în ceață e că în APM (Datadog) văd uneori că query-ul de dinaintea erorii a durat 3ms. Imediat după, următorul apel pe obiectul tx aruncă direct Transaction already closed.
Am salvat un log detaliat chiar înainte de crash:
- Tranzacția pornește.
- Se execută 2 query-uri de
SELECT. - Trece un interval scurt de 15-30ms.
- Următorul
UPDATEcrapă instant cu eroare.
E posibil ca PgBouncer în transaction mode să taie conexiunea dacă detectează ceva dubios la nivel de TCP packet? Sau e o problemă cu modul în care Prisma gestionează conexiunile când un query din tranzacție e anulat la nivel de client (ex: userul dă cancel/disconnect în browser)?
A mai lovit cineva zidul ăsta în prod pe Prisma cu PgBouncer/RDS? Unde să mai sap, că deja rămân fără idei?