← notlar

2026-04-06

Frontend fiyatları görmeden COMPLETED diyen pipeline

Bir bug vardı: frontend search job'ın durumunu polling ile kontrol ediyordu. COMPLETED görünce fiyatları çekiyordu — ama fiyatlar boş geliyordu. Sayfa yenilersen fiyatlar görünüyordu. Klasik race condition.

Sorun

Eski akış şuydu:

storeResult() → raw HTML kaydet → markCompleted() → job COMPLETED
  → [ASYNC] parse event → fiyatları DB'ye yaz

markCompleted() senkron çalışıyordu, storeResult() içinde. Fiyat parsing ise async event listener'da. Yani:

  1. Raw HTML kaydedildi
  2. Job COMPLETED oldu (commit)
  3. Frontend COMPLETED gördü, fiyatları sorguladı
  4. Fiyat parsing henüz bitmedi → boş sonuç

Adım 2 ve 4 arasında bir yarış var. Frontend hızlıysa (ki genelde öyle), fiyatlar henüz yazılmamış oluyor.

Çözüm

markCompleted()storeResult()'tan çıkardım. Fiyat parsing'in içine taşıdım:

storeResult() → raw HTML kaydet → RawResultReceivedEvent
  → [ASYNC, REQUIRES_NEW] HotelPriceParsingService
    → parseAndStore() → fiyatlar DB'ye yazıldı
    → markCompleted() → job COMPLETED
    → [COMMIT] fiyatlar + status aynı transaction'da

Artık markCompleted() ancak fiyatlar parse edilip DB'ye yazıldıktan sonra çalışıyor. Ve aynı transaction içinde — ya ikisi birden commit olur, ya hiçbiri.

REQUIRES_NEW detayı

Parsing listener'ı @Transactional(propagation = REQUIRES_NEW) ile çalışıyor. Neden? Çünkü:

  • storeResult() kendi transaction'ını commit etmeli (raw HTML kaydedilsin)
  • @TransactionalEventListener(phase = AFTER_COMMIT) kullanıyorum — event ancak commit'ten sonra tetikleniyor
  • Parsing kendi yeni transaction'ında çalışıp hem fiyatları hem COMPLETED status'ü birlikte commit ediyor

Bu sayede raw HTML kaydı ve fiyat kaydı birbirinden bağımsız transaction'larda ama fiyatlar + status atomik.

Terminal status guard

Bir de şunu ekledim: markCompleted() çağrılmadan önce job'ın zaten terminal durumda olup olmadığını kontrol ediyorum.

if (job.getStatus().isTerminal()) {
    log.warn("Job {} already in terminal state: {}", job.getId(), job.getStatus());
    return;
}

Neden? Timeout service arada job'ı FAILED yapabiliyor. Parsing yavaş kaldıysa, timeout gelip FAILED yaptı, sonra parsing bitti ve COMPLETED yapmaya çalışıyor — FAILED'dan COMPLETED'a geçiş mantıksız. Guard bunu engelliyor.

Ne öğrendim

Event-driven pipeline'larda "status değişikliği nerede olmalı" sorusu kritik. Status, yan etkiler tamamlanmadan değişirse consumer (frontend, başka servis) tutarsız veri görür.

Kural basit: status değişikliği, o status'ün vadettiği veri ile aynı transaction'da olmalı. COMPLETED diyorsan, fiyatların da orada olması lazım.