perf: canlı ihale havuzunun tamamı yükleniyor
Sorun: /api/tenders yalnızca 1.060 kayıt döndürüyordu — üç sabit sınır yüzünden (ilan.gov.tr 8 sayfa=160, DT 300, arşiv 600). Havuz ise 5.000+. Çözüm: - ilan.gov.tr sayfaları artık PARALEL çekiliyor (6 eşzamanlı), sıralı değil - İlk yanıt için 6 sayfa yeter; kalan sayfalar ARKA PLANDA doldurulur, kullanıcı beklemez (ölçüm: ilk yanıt 0.05s, havuz arka planda 120 → 3.059 ilana çıktı) - Sayfa sınırı 8 → 300 (6.000 ilan), DT 300 → 5.000, arşiv 600 → 5.000 - Hepsi env ile ayarlanabilir: ILAN_MAX_PAGES, ILAN_ILK_PARTI, ILAN_ESZAMANLI, DT_LIMIT, HISTORY_LIMIT - totalCount artık arşivi de içeriyor; 'yüklü' sayısı 'havuz'u geçmiyor Not: yanıt nginx tarafından gzip'leniyor (8.5x), tüm havuz ~600 KB — boyut darboğaz değildi, sayfalama darboğazdı.
This commit is contained in:
+82
-17
@@ -36,7 +36,13 @@ const PORT = process.env.PORT || 3001;
|
||||
const ILAN_BASE = 'https://www.ilan.gov.tr';
|
||||
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 dk
|
||||
const LIST_SIZE = 20; // ilan.gov.tr sayfa başına 20 kayıtla sınırlıyor
|
||||
const MAX_PAGES = 8; // 8 sayfa × 20 = 160 ilan
|
||||
// ilan.gov.tr sayfa başına 20 kayıt veriyor; havuzun tamamı için çok sayfa gerekir.
|
||||
// Sayfalar paralel çekilir, ilk yanıt beklemesin diye kalanı arka planda dolar.
|
||||
const MAX_PAGES = Number(process.env.ILAN_MAX_PAGES || 300); // 300 × 20 = 6000 ilan
|
||||
const ILK_PARTI = Number(process.env.ILAN_ILK_PARTI || 6); // ilk yanıt için yeterli sayfa
|
||||
const ES_ZAMANLI = Number(process.env.ILAN_ESZAMANLI || 6); // paralel istek sayısı
|
||||
const DT_LIMIT = Number(process.env.DT_LIMIT || 5000);
|
||||
const HISTORY_LIMIT = Number(process.env.HISTORY_LIMIT || 5000);
|
||||
|
||||
const ILAN_HEADERS = {
|
||||
accept: 'application/json',
|
||||
@@ -257,28 +263,85 @@ function htmlToText(htmlStr) {
|
||||
|
||||
const cache = { tenders: null, fetchedAt: 0, totalCount: 0, todayCount: 0, detail: new Map() };
|
||||
|
||||
let arkaPlanDoluyor = false;
|
||||
|
||||
// Bir sayfa grubunu paralel çeker
|
||||
async function ilanSayfalari(baslangic, adet) {
|
||||
const istekler = [];
|
||||
for (let i = 0; i < adet; i++) {
|
||||
const page = baslangic + i;
|
||||
istekler.push(
|
||||
ilanSearch({ ats: [3] }, page * LIST_SIZE, LIST_SIZE).catch((e) => {
|
||||
console.warn(`[ihaleist-api] sayfa ${page} alınamadı: ${e.message}`);
|
||||
return { ads: [] };
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.all(istekler);
|
||||
}
|
||||
|
||||
function ilanlariBirlestir(mevcut, yeniAds) {
|
||||
const seen = new Set(mevcut.map((t) => t.id));
|
||||
const eklenen = [];
|
||||
for (const ad of yeniAds) {
|
||||
const t = mapAd(ad);
|
||||
if (seen.has(t.id)) continue;
|
||||
seen.add(t.id);
|
||||
eklenen.push(t);
|
||||
}
|
||||
return eklenen;
|
||||
}
|
||||
|
||||
// Kalan sayfaları arka planda çeker; kullanıcı beklemez, havuz zamanla dolar.
|
||||
async function arkaPlandaTamamla(numFound) {
|
||||
if (arkaPlanDoluyor) return;
|
||||
arkaPlanDoluyor = true;
|
||||
try {
|
||||
const gerekliSayfa = Math.min(MAX_PAGES, Math.ceil(numFound / LIST_SIZE));
|
||||
let page = ILK_PARTI;
|
||||
while (page < gerekliSayfa) {
|
||||
const grup = Math.min(ES_ZAMANLI, gerekliSayfa - page);
|
||||
const sonuclar = await ilanSayfalari(page, grup);
|
||||
const ads = sonuclar.flatMap((r) => r.ads || []);
|
||||
if (!ads.length) break;
|
||||
const eklenen = ilanlariBirlestir(cache.tenders, ads);
|
||||
if (eklenen.length) cache.tenders = [...cache.tenders, ...eklenen];
|
||||
page += grup;
|
||||
if (page % 60 === 0) {
|
||||
console.log(`[ihaleist-api] arka plan: ${cache.tenders.length}/${numFound} ilan yüklendi`);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 120)); // kaynağı yormamak için
|
||||
}
|
||||
console.log(`[ihaleist-api] havuz dolduruldu: ${cache.tenders.length}/${numFound} ilan`);
|
||||
} catch (e) {
|
||||
console.warn('[ihaleist-api] arka plan doldurma hatası:', e.message);
|
||||
} finally {
|
||||
arkaPlanDoluyor = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTenders(force = false) {
|
||||
const fresh = cache.tenders && Date.now() - cache.fetchedAt < CACHE_TTL_MS;
|
||||
if (fresh && !force) return cache;
|
||||
|
||||
const allAds = [];
|
||||
let numFound = 0;
|
||||
for (let page = 0; page < MAX_PAGES; page++) {
|
||||
const result = await ilanSearch({ ats: [3] }, page * LIST_SIZE, LIST_SIZE);
|
||||
const ads = result.ads || [];
|
||||
numFound = result.numFound || numFound;
|
||||
if (ads.length === 0) break;
|
||||
allAds.push(...ads);
|
||||
if (allAds.length >= numFound) break;
|
||||
}
|
||||
// İlk parti: paralel, hızlı — kullanıcı bunu bekler
|
||||
const ilk = await ilanSayfalari(0, ILK_PARTI);
|
||||
const numFound = ilk.find((r) => r.numFound)?.numFound || 0;
|
||||
const ads = ilk.flatMap((r) => r.ads || []);
|
||||
|
||||
const seen = new Set();
|
||||
cache.tenders = allAds
|
||||
cache.tenders = ads
|
||||
.filter((ad) => (seen.has(ad.id) ? false : seen.add(ad.id)))
|
||||
.map(mapAd);
|
||||
cache.totalCount = numFound || allAds.length;
|
||||
cache.totalCount = numFound || cache.tenders.length;
|
||||
cache.todayCount = cache.tenders.filter((t) => t.isYeni).length;
|
||||
cache.fetchedAt = Date.now();
|
||||
console.log(`[ihaleist-api] ${cache.tenders.length} ihale yüklendi (toplam havuz: ${cache.totalCount})`);
|
||||
console.log(`[ihaleist-api] ${cache.tenders.length} ihale yüklendi (havuz: ${cache.totalCount}) — kalanı arka planda`);
|
||||
|
||||
// Kalanı arka planda doldur (yanıtı bloklamaz)
|
||||
if (cache.totalCount > cache.tenders.length) {
|
||||
arkaPlandaTamamla(cache.totalCount);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
@@ -517,14 +580,14 @@ const server = http.createServer(async (req, res) => {
|
||||
// Doğrudan temin (EKAP worker) verisini varsa birleştir
|
||||
let dt = { tenders: [], totalCount: 0, available: false };
|
||||
try {
|
||||
dt = readDtTenders(300);
|
||||
dt = readDtTenders(DT_LIMIT);
|
||||
} catch (e) {
|
||||
console.warn('[ihaleist-api] DT okuma hatası:', e.message);
|
||||
}
|
||||
// Geçmiş sonuç ilanları (kazanan firma + bedel + teklif sayısı)
|
||||
let hist = { tenders: [], totalCount: 0, available: false };
|
||||
try {
|
||||
hist = readHistory(600);
|
||||
hist = readHistory(HISTORY_LIMIT);
|
||||
} catch (e) {
|
||||
console.warn('[ihaleist-api] geçmiş okuma hatası:', e.message);
|
||||
}
|
||||
@@ -535,7 +598,9 @@ const server = http.createServer(async (req, res) => {
|
||||
success: true,
|
||||
source: sources.join(' + '),
|
||||
fetchedAt: new Date(c.fetchedAt).toLocaleString('tr-TR'),
|
||||
totalCount: c.totalCount + dt.totalCount,
|
||||
// Havuz = canlı ilan + doğrudan temin + sonuç arşivi (yüklü sayısıyla tutarlı olsun)
|
||||
totalCount: c.totalCount + dt.totalCount + hist.totalCount,
|
||||
ilanCount: c.totalCount,
|
||||
todayCount: c.todayCount,
|
||||
dtCount: dt.totalCount,
|
||||
historyCount: hist.totalCount,
|
||||
|
||||
Reference in New Issue
Block a user