Giriş: Kendi Projelerimin Gizli Öğretmenleri
Kendi başıma giriştiğim yan projeler, kariyerimin en büyük öğrenme alanları oldu. Genellikle büyük kurumsal projelerin gerektirdiği süreçlerden sıyrılıp, doğrudan teknolojiyle haşır neşir olabildiğim anlar bunlar. Ancak bu özgürlük, beraberinde belirli zorlukları da getiriyor. En belirginlerinden biri ise, özellikle hata durumlarında, tekrar denemeleri sistematik bir şekilde yönetebilmek. Geçtiğimiz on yılda, kendi finansal hesaplayıcımdan mobil spam engelleyicime kadar pek çok yan proje geliştirdim. Bu süreçte edindiğim tecrübelerle, hatalardan ders çıkarıp ilerlemeyi sağlayan dört temel stratejiyi oturtmaya çalıştım. Bu yazıda, bu stratejileri ve gerçek hayattan örneklerle nasıl uyguladığımı anlatacağım.
Bu yan projeler, benim için sadece birer kod parçası değil, aynı zamanda sürekli bir gelişim platformu. Bir üretim ERP’si geliştirirken veya bir bankanın iç platformunda çalışırken edindiğim kurumsal tecrübelerin yanı sıra, bu kişisel projeler, farklı bakış açıları ve deneysel yaklaşımlar kazanmamı sağlıyor. Örneğin, kendi VPS’imde çalışan özel finansal hesaplayıcılarımda yaptığım optimizasyonlar, bazen ana projelerdeki veritabanı performans sorunlarına bile ışık tutabiliyor. Bu makalede, bu “gizli öğretmenlerimin” bana öğrettiği, tekrar denemeleri nasıl daha akıllıca yönetebileceğime dair düşüncelerimi paylaşacağım.
1. Hata Kaydını Otomatikleştir: Journald ve Loglama Stratejileri
Yan projelerde en çok karşılaştığım sorunlardan biri, hataların neden meydana geldiğini anlamak için yeterli veriye sahip olmamamdı. Özellikle bir kerelik çalışan script’lerde veya anlık çalışan servislerde, hatanın oluştuğu anda sistemde neler olup bittiğini anlamak kritik önem taşıyor. Bu noktada, uzun süredir SystemD kullanan Linux sistemlerimde journald’in gücünden faydalanmaya başladım.
Journald, sadece logları toplamakla kalmıyor, aynı zamanda loglama seviyelerini ve oran sınırlamalarını da yönetebiliyor. Kendi yan projelerim için genellikle şu stratejiyi izliyorum: Kritik hatalar için syslog’a veya özel bir dosyaya yüksek detaylı loglama yaparken, rutin operasyonel logları daha özet tutuyorum. Bu, disk alanından tasarruf etmemi sağlarken, hata anında gerekli tüm bilgiyi de elde etmeme olanak tanıyor. Örneğin, bir seferlik çalışan bir veri çekme script’im vardı. Bu script, belirli bir API’den veri çekerken ara sıra HTTP 500 Internal Server Error alıyordu. İlk başta hatayı anlamak için script’in içine print() komutları ekleyerek debugging yapmaya çalıştım. Ancak bu hem yavaştı hem de hatanın oluştuğu anı tam olarak yakalamak zordu.
Journald’in sunduğu rate limiting özelliği de burada devreye giriyor. Özellikle yoğun çalışan servislerde, aynı hatanın defalarca loglanması hem disk alanını doldurabilir hem de logları okunaksız hale getirebilir. SystemD unit dosyalarına eklediğim RateLimitIntervalSec ve RateLimitBurst gibi parametrelerle, belirli bir zaman diliminde aynı kaynaktan gelen log sayısını sınırlayabiliyorum. Bu, özellikle aynı 429 Too Many Requests türünden hataların sürekli tekrarlandığı durumları analiz ederken işe yarıyor. Hataların nedenini anlamak için ilk adım, hatayı ne kadar detaylı ve ne kadar verimli bir şekilde kaydedebildiğimizi otomatikleştirmek. Bu, sadece hatayı tespit etmekle kalmıyor, aynı zamanda hatanın kök nedenini bulma sürecini de hızlandırıyor.
2. Tekrar Deneme Mekanizmaları: İdeal Bekleme Süresi ve Üstel Geri Çekilme (Exponential Backoff)
Bir hata oluştuğunda, ilk aklıma gelen şeylerden biri “biraz bekleyip tekrar denemek” oluyor. Ancak bu “biraz”ın ne kadar olması gerektiği, sistemin kararlılığı ve kullanıcı deneyimi açısından kritik. Özellikle ağ hataları veya geçici servis kesintileri gibi durumlarda, hemen tekrar denemek yerine akıllıca bir bekleme stratejisi izlemek gerekiyor. Burada devreye giren temel konseptlerden biri “üstel geri çekilme” yani exponential backoff.
Yan projelerimde, özellikle harici API’lere veya servislere erişim sağlayan kodlarımda bu mekanizmayı sıkça kullanırım. Örneğin, kendi finansal hesaplayıcı projemde, bir veri sağlayıcının API’sine bağlanırken ara sıra ağ bağlantı sorunları yaşıyordum. İlk denemede hata aldığımda, hemen bir sonraki saniyede tekrar denemek yerine, 1 saniye bekleyip tekrar deniyordum. Eğer yine hata alırsam, bekleme süresini ikiye katlayarak 2 saniye, sonra 4 saniye, 8 saniye gibi artırarak devam ediyordum. Bu stratejinin en büyük avantajı, hem hedef servise aşırı yük bindirmemek hem de geçici bir sorunun düzelmesi için makul bir zaman tanımak.
Bu stratejiyi uygularken dikkat ettiğim bir diğer nokta ise maksimum tekrar deneme sayısı ve toplam bekleme süresi. Sonsuz bir döngüye girmek istemem. Genellikle 3 ila 5 tekrar deneme yeterli oluyor. Eğer bu denemeler sonunda hala hata alıyorsam, bu artık geçici bir sorun olmaktan çıkmış demektir ve sorunu manuel olarak incelemem gerektiğini anlarım. Bu konudaki bir başka önemli parametre ise “jitter” eklemek. Yani, bekleme süresini tam olarak hesaplamak yerine, hesaplanan süreye küçük bir rastgelelik eklemek. Bu, birden çok istemcinin aynı anda hata alıp aynı anda tekrar denemesini engelleyerek “thundering herd” problemini önlemeye yardımcı olur.
Astro’nun fetch API’sini kullanarak yaptığım bir örnekte, bu stratejiyi nasıl uyguladığımı görebilirsiniz:
async function fetchDataWithRetry(url, maxRetries = 3, initialDelay = 1000) {
let delay = initialDelay;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
// Hata durumunda tekrar deneme mantığı
if (response.status >= 500 && response.status < 600) {
console.warn(`Request failed with status ${response.status}. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
// Jitter ekle
delay += Math.random() * initialDelay;
} else {
// Client errors or other non-retryable errors
throw new Error(`HTTP error! status: ${response.status}`);
}
} else {
// Başarılı olursa veriyi döndür
return await response.json();
}
} catch (error) {
console.error(`Attempt ${i + 1} failed: ${error.message}`);
if (i === maxRetries - 1) {
throw error; // Son deneme ise hatayı fırlat
}
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
delay += Math.random() * initialDelay;
}
}
}
// Kullanım örneği:
// fetchDataWithRetry('https://api.example.com/data')
// .then(data => console.log('Data received:', data))
// .catch(error => console.error('Failed to fetch data after multiple retries:', error));
Bu kod parçası, harici bir servisten veri çekerken olası ağ sorunlarını ele almak için üstel geri çekilme ve jitter mekanizmalarını içeriyor. Bu tür basit ama etkili stratejiler, yan projelerimin daha kararlı çalışmasını sağlıyor.
3. Durum Yönetimi ve İdempotency: Tekrar Denemelerin Yan Etkileri
Tekrar deneme mekanizmalarını uygularken göz ardı edilmemesi gereken en önemli konulardan biri, yapılan işlemin “idempotent” olup olmadığıdır. İdempotency, bir işlemin birden çok kez uygulanmasının, tek bir kez uygulanmasıyla aynı sonucu doğurması anlamına gelir. Eğer yaptığınız işlem idempotent değilse, tekrar denemeler beklenmedik ve istenmeyen yan etkilere yol açabilir.
Örneğin, bir kullanıcıya e-posta gönderen bir fonksiyon düşünelim. Eğer bu fonksiyon idempotent değilse ve ağ hatası nedeniyle tekrar denenirse, kullanıcıya aynı e-postayı iki kez gönderebilir. Bu, kullanıcı deneyimi açısından oldukça rahatsız edici bir durumdur. Kendi projelerimde, özellikle finansal işlemler veya veri güncellemeleri gibi hassas operasyonlarda idempotency’yi sağlamak için çeşitli yöntemler kullanıyorum.
Bir yöntem, her işlem için benzersiz bir işlem kimliği (transaction ID) kullanmaktır. Bir istek gönderdiğimde, bu kimliği de istekle birlikte gönderirim. Sunucu tarafında, bu kimliği kontrol ederek işlemin daha önce gerçekleştirilip gerçekleştirilmediğini anlar ve eğer gerçekleştirilmişse, aynı işlemi tekrar yapmaz. Örneğin, kendi Android spam engelleyici uygulamamda, bir numarayı engelleme isteği gönderirken, her engelleme isteği için benzersiz bir UUID oluşturur ve bunu sunucuya gönderirdim. Sunucu, bu UUID’yi daha önce görmüşse, aynı numarayı tekrar engelleme komutunu işleme almazdı. Bu, uygulamanın kararlılığını artırdı ve çift engelleme gibi durumların önüne geçti.
Bir diğer yaklaşım ise, işlemin kendisini idempotent hale getirmektir. Örneğin, bir değeri arttırmak yerine, değeri doğrudan belirli bir değere ayarlamak (SET operasyonu gibi) daha idempotent bir yaklaşımdır. Veya, bir öğeyi silmek yerine, öğenin “silindi” olarak işaretlenmesini sağlamak. Bu tür yaklaşımlar, tekrar denemelerin güvenli bir şekilde yapılabilmesine olanak tanır. PostgreSQL gibi veritabanlarında INSERT ... ON CONFLICT DO NOTHING veya UPSERT gibi özellikler de bu konuda bize yardımcı olabilir.
Bu konuda, “transaction outbox” deseni de oldukça kullanışlıdır. Bir işlemi veritabanına kaydederken, aynı zamanda bu işlemin gerçekleştirileceği mesajı da bir “outbox” tablosuna yazarız. Daha sonra ayrı bir servis bu outbox’tan mesajları okuyarak dış servislere gönderir. Bu sayede, veritabanı işlemi başarılı olur ancak dış servis mesajı alınamazsa, mesaj tekrar gönderilebilir. Bu, iki aşamalı commit’in (two-phase commit) daha basit ve daha güvenilir bir alternatifidir.
4. Öğrenme Döngüsü Oluşturma: Hatalardan Ders Çıkarma ve Dokümantasyon
Yan projelerimin en değerli yanlarından biri, hatalardan ders çıkarma ve bu dersleri kalıcı hale getirme becerisidir. Sadece hatayı çözmek yeterli değil; neden olduğunu anlamak, gelecekte benzer hataların önüne geçmek için gerekli adımları atmak ve bu bilgiyi organize etmek uzun vadede büyük fark yaratır. Bu, bir nevi kendi kişisel “post-mortem” analizimi yapmaktır.
Benim için bu öğrenme döngüsünün ilk adımı, yukarıda bahsettiğim gibi detaylı loglama ve tekrar deneme mekanizmalarını kurmak. Ancak bundan sonra gelen adım, bu logları ve hata durumlarını analiz etmek. Kendi yan projelerimde genellikle basit bir Markdown dosyası veya bir Notion sayfası kullanarak “Hata Günlüğü” tutarım. Bu günlüğe, karşılaştığım sorunu, hatanın ne zaman ve nerede oluştuğunu, sorunu çözmek için attığım adımları ve en önemlisi, bu hatadan çıkardığım dersleri yazarım.
Örneğin, kendi finansal hesaplayıcı projelerimde PostgreSQL’in WAL (Write-Ahead Logging) dosyalarının şişmesi gibi bir sorunla karşılaştığımda, bu durumu detaylıca kaydettim. pg_wal dizininin boyutunun beklenmedik biçimde büyüyüp disk alanını doldurduğunu belirttim. Sorunu çözmek için pg_wal_keep_size parametresini ayarladığımı ve autovacuum ayarlarını optimize ettiğimi not aldım. Bu notlar, ileride benzer bir sorunla karşılaştığımda bana yol gösterecektir. Bu tür dokümantasyon, sadece bana değil, eğer bir gün projeyi başkasıyla paylaşacak olursam onlara da büyük fayda sağlar.
Bu öğrenme döngüsünü tamamlamak için bazen kodumda da iyileştirmeler yaparım. Örneğin, bir hata nedeniyle sürekli manuel müdahale gerekiyorsa, bu durumu otomatikleştirecek küçük bir script veya script değişikliği yapmaya çalışırım. Bu, sadece mevcut sorunu çözmekle kalmaz, aynı zamanda projenin genel güvenilirliğini de artırır. Kendi yan projelerimde, bu yaklaşım sayesinde zamanla daha sağlam ve daha az bakım gerektiren sistemler inşa edebildim.
Sonuç: Hatalar, Gelişimin Yakıtıdır
Yan projeler, kendi başımıza bir şeyler inşa etme ve deneme özgürlüğü sunar. Ancak bu denemeler sırasında hatalarla karşılaşmak kaçınılmazdır. Önemli olan bu hatalarla nasıl başa çıktığımızdır. Sistematik hata kaydı, akıllı tekrar deneme mekanizmaları, idempotency’nin sağlanması ve hatalardan ders çıkarma döngüsünün oluşturulması, bu süreci daha verimli ve daha öğretici hale getirir. Kendi projelerimde edindiğim bu tecrübeler, sadece kişisel projelerimde değil, aynı zamanda kurumsal projelerimde de karşılaştığım zorluklara daha hazırlıklı olmamı sağladı. Hatalar, gelişimin yakıtıdır; yeter ki onları doğru yönetebilelim.
Bu makalede anlattığım stratejiler, benim kişisel deneyimlerimden yola çıkarak oluşturduğum yaklaşımlardır. Her projenin kendine özgü dinamikleri olabilir, ancak temel prensipler genellikle aynı kalır. Umarım bu bilgiler, sizlerin de yan projelerinizde daha sağlam adımlarla ilerlemenize yardımcı olur. Unutmayın, her hata, daha iyi bir sonraki adımın habercisidir.