Serinin ikinci yazısında bir kural koymuştuk: sitede her zaman en az bir zamanlanmış yazı bulunacak. O yazıyı kuralın elle kontrolünü anlatarak bitirmiş ve bu kontrolü otomatikleştireceğimizi söylemiştik. 15 Ekim’de Claude bu iş için küçük bir “kuyruk bekçisi” yazdı. Bekçi o günden beri saatte bir çalışıyor. Bu yazıda bekçinin nasıl çalıştığını ve ilk haftasının günlüğünü anlatıyoruz. Bir de günlükte görünmeyen bir şeyi: bekçinin ilk gün bir kez sessizce çöktüğünü.
Kural ve Neden Gerekti
8 Ekim Perşembe sabahı, yayınlanması gereken yazının hiç hazırlanmadığını saat 09:58’de fark ettik. Yazı aceleyle tamamlandı ve 10:03’te yayına girdi. Bunun üzerine şu kuralı koyduk:
Sitede her zaman en az bir zamanlanmış yazı bulunur. Bir yazı yayına girdiğinde sıradaki yazı zaten zamanlanmış olmalıdır.
Kuralı kontrol etmek kolay. WordPress REST API’si zamanlanmış yazıları tek istekle listeliyor. Zor olan, bu kontrolü her gün hatırlamak. Kural ilk haftalarda Claude’un proje hafızasında duruyordu: her oturumun başında kuyruk sorgulanıyordu. Ama oturum açılmayan günlerde kimse bakmıyordu. İşi bir zamanlanmış göreve vermeye karar verdik.
Bekçi Ne Yapıyor?
Bekçi, tek dosyalık bir Python betiği (kuyruk_bekcisi.py). Yalnızca standart kütüphaneyi kullanıyor. Her çalıştığında zamanlanmış yazıları REST API’den okuyor ve kuyruğu dört durumdan birine yerleştiriyor:
| Durum | Ne zaman? | Bildirim |
|---|---|---|
| TAMAM | En az iki zamanlanmış yazı var ve sıradaki yayın günü (Pazartesi ya da Perşembe 10:00) dolu | Yok |
| UYARI | Tek yazı kaldı ya da sıradaki yayın günü boş | Normal |
| KRİTİK | Hiç zamanlanmış yazı yok | Acil |
| HATA | Kuyruk okunamadı (ağ, yetki, sunucu) | Acil |
Son satır bilinçli bir tercih. Bir bekçinin en tehlikeli hâli susmasıdır: kuyruğu okuyamayan bir betik hiçbir şey söylemezse, bu “her şey yolunda” gibi görünür. Bu yüzden okuma hatası da bir durum olarak sayılıyor ve bildiriliyor. Bu tercihin ilk gün sınandığını aşağıda göreceksiniz.
“Sıradaki yayın günü” hesabı şöyle:
def siradaki_yayin(simdi: datetime) -> datetime:
"""Şu andan sonraki ilk Pazartesi/Perşembe 10:00 (site saati = yerel saat)."""
aday = simdi.replace(hour=YAYIN_SAATI, minute=0, second=0, microsecond=0)
while aday <= simdi or aday.weekday() not in YAYIN_GUNLERI:
aday = (aday + timedelta(days=1)).replace(hour=YAYIN_SAATI)
return aday
Bekçi, bu tarihte zamanlanmış bir yazı var mı diye bakıyor. Böylece kuyrukta üç yazı olsa bile, aradaki bir Perşembe boş kaldıysa UYARI veriyor.
Neden Saatte Bir Kontrol, Ama Günde Bir Kayıt?
Bu bir masaüstü bilgisayar ve her sabah aynı saatte açık olmayabilir. “Her gün 08:00’de çalış” diye kurulan bir görev, bilgisayar o saatte kapalıysa o gün hiç çalışmaz. Bu yüzden cron bekçiyi her saatin 5. dakikasında çağırıyor. Bekçi de günlüğe bakıyor: o gün aynı durum zaten kaydedildiyse hiçbir şey yapmadan çıkıyor. Durum değişirse (ör. TAMAM’dan HATA’ya) yeni bir satır yazıyor ve bildirim gönderiyor.
Sonuç olarak bilgisayar hangi saatte açılırsa açılsın, ilk saat başında kontrol yapılıyor. Günlük de her saat için bir satırla dolmuyor.
Cron’da Çalıştırmanın İki Tuzağı
Betiği terminalde çalıştırmak ile cron’a vermek arasında iki fark çıktı.
1. Kimlik bilgileri. WordPress kullanıcı adı ve uygulama parolası ~/.bashrc dosyasında ortam değişkeni olarak tanımlı. Ubuntu’nun varsayılan .bashrc dosyası etkileşimli olmayan bir kabukta ilk satırlarda çıkıyor. Cron’un kabuğu da etkileşimli değil. Bu yüzden komutu bash -ic ile, etkileşimli bir kabukta çalıştırıyoruz. Bunun bir yan etkisi var: terminali olmayan etkileşimli kabuk her seferinde “no job control” uyarısı basıyor. Cron satırı bu uyarıyı süzüp gerçek hataları ayrı bir dosyaya yazıyor:
5 * * * * /bin/bash -ic 'python3 .../araclar/kuyruk_bekcisi.py' 2>&1 >/dev/null \
| grep -v -e 'no job control' -e 'terminal process group' >> .../araclar/kuyruk_bekcisi.err
2. Masaüstü bildirimi. notify-send, bildirimi masaüstü oturumunun D-Bus adresine gönderiyor. Cron bu adresi bilmiyor. Betik, adres tanımlı değilse oturumun standart adresini (/run/user/<uid>/bus) kendisi ekliyor.
İlk Haftanın Günlüğü
Bekçi her denetimi kuyruk_bekcisi.log dosyasına bir JSON satırı olarak yazıyor. 15–22 Ekim arasındaki günlüğün özeti şöyle:
| Tarih | İlk kontrol | Durum | Kuyruktaki yazı |
|---|---|---|---|
| 15 Ekim | 11:05 | TAMAM | 3 |
| 15 Ekim | 16:05 | HATA (TLS el sıkışması zaman aşımı) | – |
| 15 Ekim | 18:05 | TAMAM | 3 |
| 16 Ekim | 00:05 | TAMAM | 3 |
| 17 Ekim | 06:05 | TAMAM | 3 |
| 18 Ekim | 00:05 | TAMAM | 3 |
| 19 Ekim | 00:05 | TAMAM | 3 |
| 19 Ekim | 10:43 | TAMAM (düzeltmeden sonra elle) | 2 |
| 20 Ekim | 00:05 | TAMAM | 2 |
| 21 Ekim | 00:05 | TAMAM | 2 |
| 22 Ekim | 08:05 | TAMAM | 2 |
| 22 Ekim | 11:05 | UYARI (tek yazı kaldı) | 1 |
17 Ekim’in ilk kaydının 06:05’te olması, bilgisayarın o gece kapalı olduğunu gösteriyor. “Saatte bir dene” kararı burada işe yaradı.
15 Ekim 16:05’teki HATA da beklendiği gibi işledi: sunucu yanıt vermedi, bekçi bunu günlüğe yazdı ve masaüstüne acil bildirim gönderdi ("bildirim": true). İki saat sonra sunucu düzelince durum yeniden TAMAM’a döndü.
Günlüğün son satırı bekçinin ilk gerçek uyarısı. 22 Ekim 10:00’da serinin üçüncü yazısı yayına girdi ve kuyrukta yalnızca 26 Ekim yazısı kaldı. Bekçi 11:05’teki kontrolde bunu gördü, günlüğe UYARI yazdı ve masaüstüne bildirim gönderdi:
{"zaman": "2026-10-22T11:05:02", "durum": "UYARI", "sayi": 1, "tarihler": ["2026-10-26T10:00:00"], "mesaj": "Kuyrukta tek yazı kaldı (26.10). Sıradakini hazırlayın.", "bildirim": true}
Uyarı karşılıksız kalmadı: okuduğunuz bu yazı, o gün kuyruğa eklendi.
Ama günlükte 16:05 ile 18:05 arasında bir şey eksik.
Günlükte Görünmeyen Çökme
16:05’ten sonraki çalıştırmada durum hâlâ HATA olsaydı, bekçi “bugün zaten kaydedildi” deyip sessizce çıkacaktı. Bu beklenen davranış. Ama 17:05’te başka bir şey oldu. Bunu günlükte değil, cron’un hata dosyası kuyruk_bekcisi.err‘de bulduk:
File "/usr/lib/python3.12/http/client.py", line 305, in _read_status
raise RemoteDisconnected("Remote end closed connection without"
http.client.RemoteDisconnected: Remote end closed connection without response
Sunucu bu kez zaman aşımına düşmemişti. Bağlantıyı kabul etmiş, sonra hiç yanıt vermeden kapatmıştı. Bekçi bu hatayı yakalamadı ve çöktü. Sonuç: günlüğe kayıt yok, bildirim yok. “Okuma hatası da sessiz geçilmez” diye tasarlanan bekçi, tam da bu durumda sessiz kaldı. O saatte kuyruk gerçekten boş olsaydı da durum aynı olacaktı.
Nedeni, Claude’un yazdığı hata yakalama satırı:
except (urllib.error.URLError, TimeoutError, ValueError, KeyError) as exc:
yazilar, durum, mesaj = [], "HATA", f"Kuyruk okunamadı: {exc}"
Bu satır, urlopen‘in bütün ağ hatalarını URLError olarak bildirdiğini varsayıyor. Bu varsayım bağlantı kurulamadığında doğru: DNS hatası ya da TLS zaman aşımı URLError içine sarılarak geliyor. Ama bağlantı kurulduktan sonra, yanıt okunurken oluşan hatalar sarılmadan geliyor. RemoteDisconnected hem ConnectionResetError hem de http.client.HTTPException sınıfından türüyor. İkisi de URLError değil.
Bunu bir kez yaşadıktan sonra, bekçinin hangi hataları yakalayıp hangilerinde çöktüğünü sistematik olarak denedik. kuyrugu_oku() fonksiyonunu her seferinde farklı bir hata fırlatacak şekilde değiştirip main()‘i çalıştırdık:
| Hata | Ne zaman olur? | Özgün bekçi |
|---|---|---|
URLError |
DNS çözülemedi, bağlantı reddedildi | Yakaladı (HATA) |
TimeoutError |
Yanıt 30 saniyede gelmedi | Yakaladı (HATA) |
JSONDecodeError |
Sunucu JSON yerine HTML hata sayfası döndürdü | Yakaladı (HATA) |
RemoteDisconnected |
Sunucu bağlantıyı yanıtsız kapattı | Çöktü |
ConnectionResetError |
Bağlantı okuma sırasında koptu | Çöktü |
IncompleteRead |
Yanıt yarıda kesildi | Çöktü |
Üç hata türünde bekçi sessizce çöküyordu. Bu, 26 Ekim yazısında anlattığımız alışkanlığın bir başka yüzü. O yazıda incelediğimiz kodlar başarıyı kontrol etmeden bildiriyordu. Burada ise kod, hatanın yalnızca beklediği biçimde geleceğini varsaymıştı. İşin ironik yanı, bekçi, o yazıyı hazırladığımız gün, 15 Ekim’de yazıldı.
Senaryo Testleri ve İkinci Bulgu
Ağ hatalarını denerken değerlendirme mantığını da kuyruk senaryolarıyla sınadık. degerlendir() fonksiyonuna elle hazırlanmış kuyruklar ve saatler verdik:
| Senaryo | Beklenen | Özgün bekçi |
|---|---|---|
| Normal, üç yazı | TAMAM | TAMAM |
| Tek yazı kaldı | UYARI | UYARI |
| Kuyrukta yazı var ama sıradaki Perşembe boş | UYARI | UYARI |
| Kuyruk boş | KRİTİK | KRİTİK |
| Yazı yanlışlıkla 09:00’a zamanlanmış | UYARI | UYARI |
| Perşembe 10:00 yazısı 11:00’da hâlâ “zamanlanmış” | Alarm | TAMAM |
Son satır WordPress’in bilinen bir sorunu. WordPress zamanlanmış yazıları kendi iç zamanlayıcısıyla (WP-Cron) yayınlıyor. Bu zamanlayıcı gerçek bir cron değil, siteye bir istek geldiğinde çalışıyor. Bir şey ters giderse yazı saati geçtiği hâlde “zamanlanmış” durumunda kalıyor. Yönetim panelinde bu durum “Missed schedule” olarak görünüyor.
Özgün bekçi bu durumu fark etmiyordu, çünkü yalnızca ileriye bakıyordu. Saat 11:00’de “sıradaki yayın günü” artık bir sonraki Pazartesi oluyor. Kuyrukta yeterince yazı olduğu için de sonuç TAMAM çıkıyor. Oysa o sabahın yazısı hiç yayınlanmamış.
Düzeltme
İki değişiklik yaptık. Birincisi, hata yakalama satırı artık hata sınıflarının ortak atalarını yakalıyor:
# URLError, TimeoutError ve bağlantı kopmaları OSError'dan; IncompleteRead ve
# RemoteDisconnected HTTPException'dan türer. Yakalanmayan hata cron'da sessizce kaybolur.
except (OSError, http.client.HTTPException, ValueError, KeyError) as exc:
İkincisi, saati geçtiği hâlde yayınlanmamış yazılar artık KRİTİK durumuna düşüyor:
# WP-Cron zamanlanmış yazıyı ancak siteye bir istek gelince yayınlar; birkaç dakikalık gecikme normaldir
GECIKME_PAYI = timedelta(minutes=30)
...
kacan = [t for t in tarihler if t < simdi - GECIKME_PAYI]
if kacan:
return "KRİTİK", f"Saati geçtiği hâlde yayınlanmamış yazı var ({min(kacan):%d.%m %H:%M}). WP-Cron'u kontrol edin."
30 dakikalık pay, 10:05’teki kontrolün henüz yayınlanmamış yazı için yanlış alarm vermesini önlüyor. Yazı gerçekten takıldıysa 11:05’teki kontrol bunu yakalıyor. Durum TAMAM’dan KRİTİK’e değiştiği için de “bugün zaten kaydedildi” kuralına takılmıyor.
Düzeltmeden sonra aynı testleri yeniden çalıştırdık. Altı hata türünün altısı da HATA olarak kaydediliyor. Kaçırılmış yayın senaryosu da KRİTİK veriyor. Bekçiyi gerçek kuyrukta da çalıştırdık: TAMAM: 2 zamanlanmış yazı, çıkış kodu 0.
Çıkardığımız Dersler
- Hata dosyasını da okuyun. Bekçinin günlüğü temiz görünüyordu. Çökme, kimsenin düzenli bakmadığı
.errdosyasındaydı. Bir otomasyonun kendi günlüğü, ancak otomasyon çalışabildiği sürece doğruyu söyler. - “Hangi hatalar gelebilir?” sorusunu kodla yanıtlayın.
exceptsatırındaki liste, okuyana eksiksiz görünüyordu. Eksikliği ancak her hata türünü tek tek fırlatarak gördük. - Bekçinin tasarımı da bir iddiadır. “Okuma hatası sessiz geçilmez” cümlesi kodun açıklamasında yazıyordu. Bu cümle, yapay zekânın yazdığı başka her iddia gibi denenmesi gereken bir şeydi.
- Gerçek olay, sentetik testin yolunu açtı. 17:05’teki çökme olmasaydı ağ hatalarını sistematik olarak denemeyi düşünmeyecektik. Kaçırılmış yayın açığını da o testler sırasında bulduk.
Sırada Ne Var?
- WebP dönüşümü: Yeni yazıların görselleri artık WebP. Ekim yazılarının eski JPG görsellerini de 26 Ekim’de WebP ile değiştirdik. Ölçümleri ve geçişi serinin bir sonraki yazısında anlatacağız.
- Kod düzeltmeleri: #3’te README bakımında bulduğumuz kod hatalarının hangilerini nasıl düzelteceğimize henüz karar vermedik.
- Bekçinin günlüğü: Bekçi çalışmaya devam ediyor. İlk UYARI’yı bu yazıda paylaştık. Bir KRİTİK kaydı gelirse onu da paylaşacağız.
Ahmet Aksoy & Claude
Not: Bu yazıda anlatılan bekçinin kaynak kodunu https://github.com/ahmetax/practical-python-examples deposundaki v312/46_queue_watchdog klasöründe bulabilirsiniz. python3 main.py --demo komutu, yazıdaki senaryo ve hata tablolarını sunucuya bağlanmadan yeniden üretir.



