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 ILAN_BASE = 'https://www.ilan.gov.tr';
|
||||||
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 dk
|
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 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 = {
|
const ILAN_HEADERS = {
|
||||||
accept: 'application/json',
|
accept: 'application/json',
|
||||||
@@ -257,28 +263,85 @@ function htmlToText(htmlStr) {
|
|||||||
|
|
||||||
const cache = { tenders: null, fetchedAt: 0, totalCount: 0, todayCount: 0, detail: new Map() };
|
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) {
|
async function getTenders(force = false) {
|
||||||
const fresh = cache.tenders && Date.now() - cache.fetchedAt < CACHE_TTL_MS;
|
const fresh = cache.tenders && Date.now() - cache.fetchedAt < CACHE_TTL_MS;
|
||||||
if (fresh && !force) return cache;
|
if (fresh && !force) return cache;
|
||||||
|
|
||||||
const allAds = [];
|
// İlk parti: paralel, hızlı — kullanıcı bunu bekler
|
||||||
let numFound = 0;
|
const ilk = await ilanSayfalari(0, ILK_PARTI);
|
||||||
for (let page = 0; page < MAX_PAGES; page++) {
|
const numFound = ilk.find((r) => r.numFound)?.numFound || 0;
|
||||||
const result = await ilanSearch({ ats: [3] }, page * LIST_SIZE, LIST_SIZE);
|
const ads = ilk.flatMap((r) => r.ads || []);
|
||||||
const ads = result.ads || [];
|
|
||||||
numFound = result.numFound || numFound;
|
|
||||||
if (ads.length === 0) break;
|
|
||||||
allAds.push(...ads);
|
|
||||||
if (allAds.length >= numFound) break;
|
|
||||||
}
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
cache.tenders = allAds
|
cache.tenders = ads
|
||||||
.filter((ad) => (seen.has(ad.id) ? false : seen.add(ad.id)))
|
.filter((ad) => (seen.has(ad.id) ? false : seen.add(ad.id)))
|
||||||
.map(mapAd);
|
.map(mapAd);
|
||||||
cache.totalCount = numFound || allAds.length;
|
cache.totalCount = numFound || cache.tenders.length;
|
||||||
cache.todayCount = cache.tenders.filter((t) => t.isYeni).length;
|
cache.todayCount = cache.tenders.filter((t) => t.isYeni).length;
|
||||||
cache.fetchedAt = Date.now();
|
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;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,14 +580,14 @@ const server = http.createServer(async (req, res) => {
|
|||||||
// Doğrudan temin (EKAP worker) verisini varsa birleştir
|
// Doğrudan temin (EKAP worker) verisini varsa birleştir
|
||||||
let dt = { tenders: [], totalCount: 0, available: false };
|
let dt = { tenders: [], totalCount: 0, available: false };
|
||||||
try {
|
try {
|
||||||
dt = readDtTenders(300);
|
dt = readDtTenders(DT_LIMIT);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[ihaleist-api] DT okuma hatası:', e.message);
|
console.warn('[ihaleist-api] DT okuma hatası:', e.message);
|
||||||
}
|
}
|
||||||
// Geçmiş sonuç ilanları (kazanan firma + bedel + teklif sayısı)
|
// Geçmiş sonuç ilanları (kazanan firma + bedel + teklif sayısı)
|
||||||
let hist = { tenders: [], totalCount: 0, available: false };
|
let hist = { tenders: [], totalCount: 0, available: false };
|
||||||
try {
|
try {
|
||||||
hist = readHistory(600);
|
hist = readHistory(HISTORY_LIMIT);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[ihaleist-api] geçmiş okuma hatası:', e.message);
|
console.warn('[ihaleist-api] geçmiş okuma hatası:', e.message);
|
||||||
}
|
}
|
||||||
@@ -535,7 +598,9 @@ const server = http.createServer(async (req, res) => {
|
|||||||
success: true,
|
success: true,
|
||||||
source: sources.join(' + '),
|
source: sources.join(' + '),
|
||||||
fetchedAt: new Date(c.fetchedAt).toLocaleString('tr-TR'),
|
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,
|
todayCount: c.todayCount,
|
||||||
dtCount: dt.totalCount,
|
dtCount: dt.totalCount,
|
||||||
historyCount: hist.totalCount,
|
historyCount: hist.totalCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user