All checks were successful
Deploy / deploy (push) Successful in 10m52s
credit_ledger.ref_id benzersizliği yalnız AYNI denemenin tekrarını koruyordu (ağda kaybolan cevap → aynı requestId ile retry). requestId istemcide her mount'ta yeniden üretildiği için "aynı kullanıcı üretimi iki kez başlattı" durumu hiç yakalanmıyordu: üretim sürerken (20-60 sn) ikinci sekmede açılan /listem?uret=1 ya da geri→ileri ikinci bir ListeUretici monte edip 3 krediyi ikinci kez yakıyordu. Karşılığında tek rapor çıkıyordu, çünkü reports.user_id benzersiz ve ikinci üretim birincinin üstüne yazıyor. Bakiyesi yetmeyen deneme kullanıcısı ise akışın ortasında "kredi yetersiz" ekranı görüyordu. - report_locks (user_id birincil anahtar): ikinci INSERT benzersizlik ihlaliyle düşer, bu da "üretim sürüyor" demektir. Kilit kredi harcamasından ÖNCE alınır, finally'de bırakılır. - Bayat kilit (süreç OOM'la öldü, deploy) 180 sn sonra devralınabilir; devir koşullu UPDATE olduğu için iki eşzamanlı devralmadan yalnız biri kazanır. Salım requestId koşullu: geç uyanan eski sahip devralanın kilidini silemez. - Yeni SURUYOR kodu: istemci bunu "hata" sayıp taze requestId ile yeniden denerse çifte harcama kapısı yeniden açılırdı. Ekranda "Tekrar dene" bilerek yok; kullanıcı listeye yönlendiriliyor, kredi harcanmadı. - DUPLICATE dalının "İstek tekrarlandı" HATA'sı da SURUYOR'a çevrildi (BACKLOG #1'deki ikinci çifte harcama yolu). Revizyon dalı değişmedi. BACKLOG #1 · güvenlik denetimi G5 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import { and, eq, lt } from "drizzle-orm";
|
||
import { appDb, schema } from "./appdb";
|
||
import { uniqueIhlaliMi } from "./credits";
|
||
|
||
const { reportLocks } = schema;
|
||
|
||
/**
|
||
* Bayat kilit eşiği. Tek Yapay Zeka çağrısının sunucu sınırı 60 sn ve üretim
|
||
* iki denemeye kadar gidebiliyor (bkz. lib/ai/cagri.ts), üstüne havuz SQL'i ve
|
||
* DB yazımı biniyor. 180 sn bu üst sınırın rahatça üstünde: bu kadar eski bir
|
||
* kilit ancak süreç öldüyse (OOM, deploy) kalmıştır ve devralınabilir.
|
||
*/
|
||
const BAYAT_MS = 180_000;
|
||
|
||
export type KilitSonuc =
|
||
| { ok: true }
|
||
/** Aynı kullanıcı için taze bir üretim zaten koşuyor. */
|
||
| { ok: false; error: "SURUYOR" };
|
||
|
||
/**
|
||
* Kullanıcı başına üretim kilidini alır.
|
||
*
|
||
* Neden var: `credit_ledger.ref_id` benzersizliği yalnız AYNI denemenin
|
||
* tekrarını koruyor (ağda kaybolan cevap → aynı requestId ile retry). Oysa
|
||
* `requestId` istemcide her mount'ta yeniden üretiliyor, yani ikinci sekmede
|
||
* açılan `/listem?uret=1` ya da geri→ileri ikinci bir üretim başlatıp 3 krediyi
|
||
* ikinci kez yakıyordu — tek rapor karşılığında (reports.user_id benzersiz).
|
||
*
|
||
* Kilit krediden ÖNCE alınır: harcama yapılmadan reddedilir.
|
||
*/
|
||
export async function uretimKilidiAl(
|
||
userId: string,
|
||
requestId: string,
|
||
): Promise<KilitSonuc> {
|
||
const simdi = new Date();
|
||
try {
|
||
await appDb
|
||
.insert(reportLocks)
|
||
.values({ userId, requestId, startedAt: simdi });
|
||
return { ok: true };
|
||
} catch (err) {
|
||
if (!uniqueIhlaliMi(err)) throw err;
|
||
}
|
||
|
||
// Kilit var. Yalnız bayatsa devral — koşullu UPDATE olduğu için iki eşzamanlı
|
||
// devralma denemesinden yalnız biri satırı günceller (rowsAffected ile ayırt
|
||
// edilir), yani yarış çifte harcamaya dönüşemez.
|
||
const devir = await appDb
|
||
.update(reportLocks)
|
||
.set({ requestId, startedAt: simdi })
|
||
.where(
|
||
and(
|
||
eq(reportLocks.userId, userId),
|
||
lt(reportLocks.startedAt, new Date(simdi.getTime() - BAYAT_MS)),
|
||
),
|
||
);
|
||
if (devir.rowsAffected > 0) return { ok: true };
|
||
return { ok: false, error: "SURUYOR" };
|
||
}
|
||
|
||
/**
|
||
* Kilidi bırakır. `requestId` koşulu şart: bayat kilidi devralan başka bir
|
||
* deneme varken, geç uyanan eski sahip onun kilidini silmemeli.
|
||
*/
|
||
export async function uretimKilidiBirak(
|
||
userId: string,
|
||
requestId: string,
|
||
): Promise<void> {
|
||
await appDb
|
||
.delete(reportLocks)
|
||
.where(
|
||
and(eq(reportLocks.userId, userId), eq(reportLocks.requestId, requestId)),
|
||
)
|
||
// Salım hiçbir zaman asıl akışı kırmamalı: kilit en kötü ihtimalle
|
||
// BAYAT_MS sonra kendiliğinden devralınabilir hâle gelir.
|
||
.catch(() => {});
|
||
}
|