Merhaba arkadaşlar, bugün sizlere bir iş kuyruğu (queue) mekanizması tasarlarken yaşadığım bir optimizasyon hikayesini anlatacağım. Özellikle belirli aralıklarla çalışan, sırayla işlem yapan sistemler kurarken, hepimizin ilk aklına gelen setInterval oluyor değil mi? Ama işler bazen göründüğü gibi olmuyor, ben de bu yüzden kafayı yemiştim!
Neden setInterval Sorun Çıkarıyor?
Şöyle düşünün: `setInterval` tam bir saat gibi, her X milisaniyede bir tetiklenmeye programlanmış. Peki ya o tetiklenme anında yapması gereken iş (callback fonksiyonu) beklenenden uzun sürerse? İşte tam da burada kuyrukta birikmeler başlıyor. Çünkü interval, işin bitip bitmediğine bakmadan, "zamanı geldi" diye yeni bir iş parçacığı daha ateşliyor. Bu da özellikle ağ istekleri, dosya okuma/yazma gibi değişken süreli işlemlerde callback hell ve bellek tüketimine yol açabiliyor.
Benim senaryomda, bir API'den veri çekip, bu verileri işleyip, sonra bir sonraki API isteğini yapacak bir queue lazımdı. İki istek arasında da sabit bir bekleme süresi olmalıydı. İlk denememde `setInterval` kullandım ve bazen API yavaş cevap verdiğinde, kuyrukta korkunç bir birikme olduğunu gördüm.
Recursive setTimeout Mantığı
Çözüm olarak, recursive (kendini çağıran) bir `setTimeout` yapısına geçtim. Mantık basit: Bir işi yap, iş BİTTİKTEN SONRA, bir sonraki işin ne zaman başlayacağını hesapla ve timer'ı öyle kur. Bu sayede, önceki işlem ne kadar uzun sürerse sürsün, bir sonraki işlem için geri sayım, ancak o işlem bittikten sonra başlıyor. Kuyrukta hiçbir şekilde birikme olmuyor.
İşte benim basit ve temiz queue sınıfım:
Kullanım Örneği ve Avantajları
Bu queue'yu şöyle kullanabilirsiniz:
Bu yapının en büyük avantajı, işlemlerin asla üst üste binmemesi. Konsol çıktısına bakarsanız, bir işlem 2.5 saniye de sürse, bir sonraki işlem onun bitiminden tam 1 saniye sonra başlayacak. `setInterval` kullansaydık, aynı 2.5 saniyelik işlem sırasında 2 yeni işlem daha tetiklenmeye çalışılacak ve kuyruk şişecekti.
Sonuç olarak, özellikle async/await veya Promise kullandığınız, süresi öngörülemeyen işlemleriniz varsa, recursive `setTimeout` bence çok daha sağlam ve temiz bir çözüm. Siz daha önce böyle bir queue mekanizması tasarladınız mı? `setInterval` ile başınız dertte oldu mu? Ya da bu pattern için ekleyeceğiniz farklı bir öneriniz var mı? Yorumlarda paylaşalım!
Şöyle düşünün: `setInterval` tam bir saat gibi, her X milisaniyede bir tetiklenmeye programlanmış. Peki ya o tetiklenme anında yapması gereken iş (callback fonksiyonu) beklenenden uzun sürerse? İşte tam da burada kuyrukta birikmeler başlıyor. Çünkü interval, işin bitip bitmediğine bakmadan, "zamanı geldi" diye yeni bir iş parçacığı daha ateşliyor. Bu da özellikle ağ istekleri, dosya okuma/yazma gibi değişken süreli işlemlerde callback hell ve bellek tüketimine yol açabiliyor.
Benim senaryomda, bir API'den veri çekip, bu verileri işleyip, sonra bir sonraki API isteğini yapacak bir queue lazımdı. İki istek arasında da sabit bir bekleme süresi olmalıydı. İlk denememde `setInterval` kullandım ve bazen API yavaş cevap verdiğinde, kuyrukta korkunç bir birikme olduğunu gördüm.
Çözüm olarak, recursive (kendini çağıran) bir `setTimeout` yapısına geçtim. Mantık basit: Bir işi yap, iş BİTTİKTEN SONRA, bir sonraki işin ne zaman başlayacağını hesapla ve timer'ı öyle kur. Bu sayede, önceki işlem ne kadar uzun sürerse sürsün, bir sonraki işlem için geri sayım, ancak o işlem bittikten sonra başlıyor. Kuyrukta hiçbir şekilde birikme olmuyor.
İşte benim basit ve temiz queue sınıfım:
JavaScript:
class SabitAralikliQueue {
constructor(islemYapFonksiyonu, aralikMs = 1000) {
this.islemYap = islemYapFonksiyonu;
this.aralik = aralikMs;
this.timeoutId = null;
this.calissin = false;
}
// Kuyruğu çalıştırmaya başla
baslat() {
if (this.calissin) return;
this.calissin = true;
console.log('Queue başlatıldı.');
this.sonrakiAdimiPlanla();
}
// Recursive setTimeout'un kalbi burada
sonrakiAdimiPlanla() {
if (!this.calissin) return;
// İŞİ YAP: API çağrısı, dosya işlemi vs.
this.islemYap().finally(() => {
// İş BİTTİ, şimdi bir sonraki iş için timer kur
if (this.calissin) {
this.timeoutId = setTimeout(() => {
this.sonrakiAdimiPlanla(); // Kendini tekrar çağır (Recursive)
}, this.aralik);
}
});
}
// Kuyruğu durdur
durdur() {
this.calissin = false;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
console.log('Queue durduruldu.');
}
}
Bu queue'yu şöyle kullanabilirsiniz:
JavaScript:
// Örnek bir işlem fonksiyonu (Örn: Bir API'ye istek at)
async function ornekIslem() {
console.log(`[${new Date().toISOString()}] İşlem başlıyor...`);
// Simüle edilmiş değişken süreli iş (0-3 saniye arası)
const bekleme = Math.random() 3000;
await new Promise(resolve => setTimeout(resolve, bekleme));
console.log(`İşlem tamamlandı (${bekleme.toFixed(0)}ms sürdü).`);
}
// Saniyede 1 işlem yapması için queue oluştur
const benimKuyrugum = new SabitAralikliQueue(ornekIslem, 1000);
// Kuyruğu başlat
benimKuyrugum.baslat();
// 10 saniye sonra durdur (örnek amaçlı)
setTimeout(() => benimKuyrugum.durdur(), 10000);
Bu yapının en büyük avantajı, işlemlerin asla üst üste binmemesi. Konsol çıktısına bakarsanız, bir işlem 2.5 saniye de sürse, bir sonraki işlem onun bitiminden tam 1 saniye sonra başlayacak. `setInterval` kullansaydık, aynı 2.5 saniyelik işlem sırasında 2 yeni işlem daha tetiklenmeye çalışılacak ve kuyruk şişecekti.
Sonuç olarak, özellikle async/await veya Promise kullandığınız, süresi öngörülemeyen işlemleriniz varsa, recursive `setTimeout` bence çok daha sağlam ve temiz bir çözüm. Siz daha önce böyle bir queue mekanizması tasarladınız mı? `setInterval` ile başınız dertte oldu mu? Ya da bu pattern için ekleyeceğiniz farklı bir öneriniz var mı? Yorumlarda paylaşalım!