await prisma.$transaction(async (tx) => {
const product = await tx.product.findUnique({ where: { id: productId } });
if (product.stock < quantity) throw new Error('Out of stock');
await tx.product.update({
where: { id: productId },
data: { stock: product.stock - quantity }
});
// Apelul extern suspect care dureaza mult
const invoice = await externalBillingService.create(userId, amount);
return tx.order.create({
data: { userId, invoiceId: invoice.id, total: amount }
});
}, {
maxWait: 10000,
timeout: 15000
});Salutare tuturor. Am dat de o belea pe care nu reușesc să o rezolv de vreo trei zile și m-am blocat complet. În producție, la un proiect cu vreo 12k utilizatori zilnici, ne lovim complet random de eroarea: Transaction API error: Transaction already closed.
Apare absolut imprevizibil. Poate să meargă totul brici timp de 12 sau chiar 24 de ore, după care bum: crapă la 3-4 request-uri consecutive pe același endpoint de checkout. În staging, evident, oricât am încercat să simulez încărcarea cu k6, totul trece fără nicio eroare.
Ce am încercat deja și n-a mers
Prima chestie la care m-am gândit a fost, evident, timeout-ul tranzacției. Implicit, Prisma închide tranzacțiile interactive după doar 5 secunde. Am zis ok, poate unele query-uri mai grele pe tabelele de comenzi durează mai mult când baza de date e mai solicitată. Am mărit timeout-ul la 15 secunde și max-wait-ul la 10 secunde în opțiunile tranzacției.
A doua zi dimineață, eroarea a apărut din nou, exact în același mod. Am monitorizat imediat CPU-ul și memoria pe instanța de RDS Postgres în momentele alea: utilizarea e undeva la 20-25%, deci nu e vorba de un blocaj de resurse la nivel de server de bază de date.
Apoi m-am gândit că poate e de la connection pool. Aveam connection_limit=10 definit în URL-ul de conexiune. Am urcat limita la 25, crezând că eliberez conexiunile mai repede și evit cozile de așteptare. Nicio schimbare, în afară de faptul că am consumat mai multe resurse pe DB degeaba.
Suspectul principal: I/O blocat în tranzacție?
Am izolat bucata de cod care pare să genereze cele mai multe probleme. Facem un $transaction interactiv pentru că avem nevoie de izolare solidă pe stocuri.
În interiorul acelei tranzacții, un coleg a lăsat un await către un serviciu extern de facturare. Chestia asta durează uneori și 3 sau 4 secunde dacă API-ul lor extern este leneș. Din câte m-am prins, dacă event loop-ul din Node este blocat sau dacă acel request extern durează prea mult, Prisma s-ar putea să creadă că tranzacția a expirat, chiar dacă timeout-ul configurat de noi este mai mare.
Aici e un trade-off destul de nasol de făcut. Dacă scot apelul extern în afara tranzacției, risc să am inconsistență de date. Adică dacă facturarea reușește, dar tranzacția din baza de date pică ulterior din alt motiv, rămân cu banii luați de la client și fără comandă înregistrată în sistem. Dacă le țin împreună, risc să blochez conexiunile și să primesc eroarea asta enervantă.
A mai pățit cineva mizeria asta cu tranzacțiile interactive din Prisma? Există vreo setare ascunsă de keep-alive pentru conexiuni sau chiar trebuie să rescriu tot fluxul cu tranzacții optimiste (Saga pattern) și să renunț complet la tranzacțiile ACID native pe fluxul ăsta?