Event-Driven (Olay Güdümlü) Yapay Zekâ Mimarisi: Kafka/RabbitMQ ile Asenkron LLM Worker’ları Nasıl Ölçeklenir?
Fatura çıkarımı üzerinden durable inbox/outbox, Kafka offset’leri, RabbitMQ ack, lease ve fencing, token bütçesi, DLQ ve idempotent ERP webhook teslimatının uçtan uca tasarımı.
· TankDev Mühendislik
Bir tedarikçi faturası yükleniyor. OCR metni bir modele gidiyor; model satırları, vergi tutarlarını ve para birimini çıkarıyor. Sonuç ERP’ye yazılacak. İlk sürümde endpoint, modelden cevap gelene kadar bekliyor. Demo çalışıyor. Ardından aynı müşteri on bin belge yüklüyor ve sistemin asıl protokolü ortaya çıkıyor: süre aşımı, yeniden gönderim, mükerrer model çağrısı ve hangi belgenin gerçekten işlendiği belli olmayan kayıtlar.
Burada tasarlayacağımız sistem, fatura çıkarımını asenkron bir iş olarak kabul eder. PostgreSQL işin otoritesidir; broker yürütülecek işi taşır; worker sınırlı kapasiteyi kullanır; ERP teslimatı ayrı bir state machine üzerinden ilerler. Amaç HTTP isteğini erken bitirmekten daha geniştir: kabul edilen işin sahibini, tekrarını, son tarihini ve parasal yan etkisini tarif etmek.
Aşağıdaki sayılar kapasite hesabını tartışmak için seçilmiş varsayımlardır; TankDev müşterisine ait benchmark değildir. SQL PostgreSQL içindir. Kod parçaları protokolün kritik sınırlarını gösterir; tam bir broker istemcisi veya çalıştırılabilir servis deposu değildir. Model sağlayıcısı, broker sürümü ve deployment ayarları doğrulanmadan örnek değerler production konfigürasyonu sayılmaz.
1. LLM darboğazı: 15 saniyelik çağrının concurrency ve timeout hesabı
Senkron HTTP sözleşmesi ile bloklayan I/O aynı şey değildir. Async bir sunucu, model yanıtını beklerken event loop’u başka isteklere bırakabilir. Yine de socket, proxy akışı, request context, bağlantı havuzu ve iptal takibi yaşamaya devam eder. Thread-per-request bir uygulamada buna bekleyen thread’ler de eklenir. LLM boyunca veritabanı transaction’ı açık tutuluyorsa, darboğaz çoğu zaman GPU’dan önce connection pool’da görünür.
Kararlı bir sistemde Little yasası ortalama eşzamanlılığı verir: L = λ × E[S]. Saniyede 20 istek ve ortalama 15 saniye servis süresi için L = 300 devam eden çağrıdır. Bu hesabın içine p95 koyup ortalama concurrency elde edemezsiniz. Ayrıca 20 istek/s gelen, fakat 2 iş/s tamamlayabilen bir sistem kararlı değildir; backlog büyür ve Little yasasının bu basit kararlı durum yorumunu kullanamayız.
L = arrival_rate * mean_service_time
= 20 requests/s * 15 s = 300 in-flight requests
HTTP elapsed = admission + queue_wait + inference + persistence + response
timeout occurs when HTTP elapsed > the effective request deadline
Illustrative distribution, independently assumed:
P(HTTP elapsed > 30 s) = 0.02
30,000 requests * 0.02 = 600 expected deadline exceedancesOrtalamanın 15 saniye olması, 30 saniyelik timeout oranını söylemez. Bunun için dağılım gerekir. Yukarıdaki yüzde 2 ayrı bir varsayımdır. Gerçek sistemde gateway deadline’ı, uygulama deadline’ı ve sağlayıcı timeout’u farklıysa hangi katmanın önce vazgeçtiğini trace üzerinden buluruz. Kullanıcının bağlantıyı kapatması, sağlayıcının üretimi durdurduğunu veya ücret oluşmadığını kanıtlamaz.
Bu senaryoda POST /extraction-jobs yalnızca kimlik doğrulama, belge referansı kontrolü ve kalıcı iş kabulünü yapar; 202 Accepted, job_id ve Location döner. Kabul yanıtının süresi ile çıkarımın tamamlanma süresi ayrı SLI’lardır. Sohbet arayüzündeki token streaming hâlâ anlamlıdır. Binlerce belgeyi işleyen batch endpoint’inin doğruluk protokolünü açık bir HTTP bağlantısına bağlamak ise gereksiz bir hata alanı yaratır.
2. Queue tabanlı worker mimarisi: önce kalıcı kabul, sonra teslim
İstemcinin gönderdiği idempotency key tenant kapsamında benzersizdir. Aynı key ve aynı normalize edilmiş istek aynı job_id’yi döndürür; aynı key ile farklı içerik 409’dur. Request hash; belge sürümü, çıkarım şeması, model politikası ve sonucu etkileyen seçenekleri kapsar. Belgeyi yalnızca dosya adıyla kimliklendirmeyiz. İçerik digest’i ile değişmez object version birlikte saklanır; imzalı erişim URL’si ise kalıcı iş kimliği olmaz.
CREATE TABLE extraction_jobs (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL,
idempotency_key text NOT NULL,
request_hash text NOT NULL,
object_version text NOT NULL,
state text NOT NULL CHECK (state IN
('READY','RUNNING','RETRY_SCHEDULED','SUCCEEDED',
'REVIEW_REQUIRED','FAILED_TERMINAL')),
attempt integer NOT NULL DEFAULT 0,
fence bigint NOT NULL DEFAULT 0,
lease_until timestamptz,
available_at timestamptz NOT NULL DEFAULT now(),
deadline_at timestamptz NOT NULL,
result_ref text,
UNIQUE (tenant_id, idempotency_key)
);
CREATE TABLE outbox_events (
event_id uuid PRIMARY KEY,
job_id uuid NOT NULL REFERENCES extraction_jobs(id),
event_type text NOT NULL,
payload jsonb NOT NULL,
published_at timestamptz
);
BEGIN;
INSERT INTO extraction_jobs
(id, tenant_id, idempotency_key, request_hash,
object_version, state, deadline_at)
VALUES (:job_id, :tenant, :key, :hash, :version, 'READY', :deadline)
ON CONFLICT (tenant_id, idempotency_key) DO NOTHING
RETURNING id;
-- If no row was inserted: SELECT the existing row, compare request_hash,
-- return its id on equality, or roll back and respond 409 on mismatch.
-- Only the inserting branch executes the outbox INSERT below.
INSERT INTO outbox_events (event_id, job_id, event_type, payload)
VALUES (:event_id, :job_id, 'extraction.requested.v1',
jsonb_build_object('job_id', :job_id, 'schema_version', 1));
COMMIT;Job ve outbox aynı transaction’da commit edilir. Broker’a publish edip sonra job yazarsak consumer olmayan bir işi görebilir. Önce job yazıp ardından publish edersek aradaki process crash işi sessizce bekletebilir. Outbox relay commit edilmiş kayıtları yayınlar. Broker onayı geldikten sonra published_at yazar; onay alınıp bu alan yazılamadan süreç ölürse aynı event tekrar gider. Dolayısıyla consumer tasarımı baştan at-least-once kabul eder.
Relay tarafında da sahiplik gerekir: sınırlı batch, SKIP LOCKED veya süreli claim, crash sonrası claim geri kazanımı ve yayın gecikmesi alarmı. Broker çağrısı boyunca sınırsız transaction tutmayız. Mesaj gövdesine fatura metni koymak yerine job_id ve event sürümü taşırız. Worker gerçek tenant, belge ve izin bilgisini kendi yetkili veri kaynağından alır; mesaj içindeki tenant_id bir güvenlik sınırı değildir.
RabbitMQ: publisher confirm, consumer ack ve durable inbox
Bu fatura hattında ilk tercihimiz RabbitMQ olur: iş dağıtımı, sınırlı in-flight teslim ve operasyon ekibinin bildiği bir broker yeterlidir. Durable queue ile persistent mesaj, publisher confirm ve doğru replication politikası birlikte değerlendirilir. Publisher confirm consumer’ın LLM’i çalıştırdığı anlamına gelmez. Consumer ack de sağlayıcı çağrısının tek kez yapıldığını söylemez. Bu iki onay farklı bağlantıların sınırlarını kapatır.
Uzun inference süresini broker consumer’ından ayırmak için kısa bir ingress consumer kullanabiliriz. Mesajı event_id üzerinde unique constraint bulunan durable inbox’a, job’ın runnable işaretlemesiyle aynı transaction’da yazar; ancak commit’ten sonra ack veririz. LLM worker’ı bu kalıcı iş deposundan lease alır. Buradaki ack “LLM tamamlandı” değil, “yürütme sorumluluğu kalıcı depoya geçti” demektir. Inbox’ı yalnızca dedup tablosu yapıp hiçbir scheduler kurmazsak ack edilen iş kaybolmuş kadar erişilemez olur.
Bu seçimde prefetch LLM concurrency’sini doğrudan belirlemez; ingress’in DB’ye kaç teslim taşıdığını sınırlar. Worker concurrency’si ayrı semaphore ve dağıtık admission bütçesidir. Doğrudan broker’dan inference yapan alternatifte ise prefetch, worker’ın gerçek paralelliğine yakın tutulur; sonuç veya kalıcı retry durumu commit edilmeden ack verilmez. İki tasarımın ack zamanlamasını birbirine karıştırmamak gerekir.
-- The earlier job schema is a subset; production also stores
-- state_version, policy versions, audit timestamps, and attempt outcomes.
CREATE TABLE execution_inbox (
event_id uuid PRIMARY KEY,
job_id uuid NOT NULL REFERENCES extraction_jobs(id),
payload_digest text NOT NULL,
received_at timestamptz NOT NULL DEFAULT now()
);
BEGIN;
INSERT INTO execution_inbox(event_id, job_id, payload_digest)
VALUES (:event_id, :job_id, :digest)
ON CONFLICT (event_id) DO NOTHING;
-- Check existing digest on duplicates; mismatch => quarantine + alert.
-- The job was already durably READY in the acceptance transaction.
-- Do not reset a RUNNING or terminal job on redelivery.
COMMIT;
-- RabbitMQ: basic_ack on the delivering channel only AFTER this commit.
-- Kafka: advance contiguous committed ingestion offsets only AFTER commit.
-- A durable scheduler scans eligible jobs even without another notification.Kafka: replay ihtiyacı varsa, offset protokolünü de tasarlayın
Aynı olay akışını denetim, analitik ve başka tüketiciler bağımsız okuyacaksa; uzun retention, tekrar oynatma ve partition bazlı sıralama gerekiyorsa Kafka’yı değerlendiririz. Sadece LLM çağrısı uzun sürüyor diye Kafka gerekmez. İki broker’ı aynı iş için üst üste koymak da bu tasarımın varsayılanı değildir. Seçim throughput etiketinden önce tüketici modeli, retention ve mevcut işletim yetkinliğine dayanır.
Kafka ingress consumer’ında auto-commit kapalıdır. Partition ve offset ile kimliklenen kayıt inbox’a kalıcı yazılır; sonra offset commit edilir. Crash aralığında tekrar okunması unique inbox kaydıyla güvenlidir. Kafka’da committed offset bir sonraki okunacak kaydı gösterir: offset 41 kalıcı alındıysa güvenli ilerleme 42’dir. Paralel ingestion varsa aynı partition’da tamamlanmamış 40’ın üstünden atlayıp 42 commit edemeyiz; yalnızca kesintisiz kalıcı prefix ilerler.
Poll döngüsünü 15 saniyelik LLM çağrısına bağlamayız. Dispatcher dolduğunda partition’ları pause eder, consumer API’sinin poll/heartbeat sözleşmesini koruruz; max.poll.interval.ms ve rebalance senaryoları client sürümüyle test edilir. Kafka transaction’ı topic’ler arasında güçlü garantiler sunabilir. Haricî model sağlayıcısı ve PostgreSQL transaction’ı bu kapsama kendiliğinden girmez. Broker’ın exactly-once ifadesinden “tek ücretli LLM çağrısı” sonucu çıkmaz.
Worker sahipliği: lease, fencing token ve kısa transaction
-- One short claim transaction; inference runs AFTER COMMIT.
BEGIN;
WITH candidate AS (
SELECT id FROM extraction_jobs
WHERE state IN ('READY', 'RETRY_SCHEDULED')
AND available_at <= now() AND deadline_at > now()
ORDER BY available_at, id
FOR UPDATE SKIP LOCKED LIMIT 1
)
UPDATE extraction_jobs j
SET state='RUNNING', attempt=j.attempt+1,
fence=j.fence+1, lease_until=now()+interval '90 seconds'
FROM candidate c WHERE j.id=c.id
RETURNING j.id, j.tenant_id, j.attempt, j.fence, j.lease_until;
COMMIT;
-- Heartbeat from the current owner; zero updated rows means ownership lost.
UPDATE extraction_jobs SET lease_until=now()+interval '90 seconds'
WHERE id=:job AND state='RUNNING' AND fence=:fence
AND lease_until > now();
-- In ONE transaction: conditional state update + result/event INSERTs.
UPDATE extraction_jobs
SET state='SUCCEEDED', result_ref=:result_ref, lease_until=NULL
WHERE id=:job AND state='RUNNING' AND fence=:fence
AND lease_until > now()
RETURNING id;
-- Insert result metadata and extraction.completed outbox event only
-- when exactly one row was updated; otherwise discard stale completion.90 saniyelik lease burada örnektir. Sağlayıcı için toplam 45 saniyelik attempt deadline, heartbeat aralığı ve process durdurma payıyla birlikte seçilir. Süresi dolan RUNNING işlerini reaper koşullu UPDATE ile RETRY_SCHEDULED durumuna geçirir; fence artırılır ve next attempt zamanı kalıcı yazılır. Retry bütçesi veya job deadline’ı biten kayıtlar terminal duruma alınır. Pod öldü diye attempt sayacını sıfırlamayız.
Fencing, geç kalan worker’ın yeni sonucu ezmesini engeller; dışarıya yapılmış çağrıyı geri alamaz. Worker A’nın ağ bağlantısı takılır, lease biter, B işi alır, sonra A yanıt alırsa A’nın sonucu commit edilemez. Yine de iki sağlayıcı çağrısı ücretlenmiş olabilir. Belgelenmiş bir sağlayıcı idempotency sözleşmesi yoksa job_id’yi header’a koyarak bu problemi çözmüş saymayız. Aynı nedenle ERP yazımı extraction transaction’ına gömülmez.
3. Backpressure ve rate limiting: 10.000 belgeyi kabul etmek, aynı anda çağırmak değildir
Kuyruk kapasite üretmez; kapasite açığını zamana yayar. Sınırsız kuyruk yalnızca arızanın tarihini değiştirir. Bu nedenle admission katmanı tenant başına bekleyen iş sayısını, byte toplamını, token tahminini ve kalan günlük harcama bütçesini kontrol eder. İş zaten kabul edilmişse onu sessizce düşürmeyiz. Henüz kabul edilmemiş taleplere tenant kotası için 429, sistemin kabul kapasitesi kalmadığında uygun bir 503 ve yeniden deneme yönlendirmesi dönebiliriz.
| Değişken | Varsayım | Hesaptaki rolü |
|---|---|---|
| RPM bütçesi | 600 attempt/dakika | 10 attempt/s |
| Token admission bütçesi | 600.000 token/dakika | Uygulamanın örnek limiti |
| Attempt rezervasyonu | 6.000 token | 100 attempt/dakika |
| Ortalama inference | 15 saniye | Concurrency hesabı |
| Global in-flight tavanı | 30 attempt | 2 attempt/s servis kapasitesi |
| Ortalama attempt/job | 1,10 | Retry dahil iş kapasitesi |
attempt_capacity = min(RPM / 60,
TPM_budget / (60 * reserved_tokens),
concurrency / mean_inference_seconds)
= min(10, 1.6667, 2) = 1.6667 attempts/s
job_capacity = attempt_capacity / mean_attempts_per_job
= 1.6667 / 1.10 = 1.5152 jobs/s
10,000-job drain time, with no new arrivals:
10,000 / 1.5152 = 6,600 s = 110 minutes
With sustained new arrivals of 1 job/s:
net drain rate = 1.5152 - 1 = 0.5152 jobs/s
initial backlog drains in ~19,412 s = 5.39 hoursBu bir steady-state planlama yaklaşımıdır; token uzunlukları, burst pencereleri ve latency dağılımı gerçek bitiş zamanını değiştirir. 6.000 token uygulamanın ihtiyatlı rezervasyonudur. Sağlayıcının modele ve endpoint’e özgü kota muhasebesini veya faturalama hesabını evrensel olarak tarif etmez. RPM, TPM, günlük limitler ve paylaşılan model havuzları gerçek hesap limitlerinden okunur. Her job’ın maliyetini de aynı sabit sayıdan değil, raporlanan kullanım ve tamamlanan iş adedinden çıkarırız.
Üç pod’a ayrı ayrı 30 semaphore verirsek global tavan 90 olur. Limiter’ın kapsamı pod değil, kota paylaşan sağlayıcı projesi/model havuzudur. Atomik çok-boyutlu admission; request bütçesini, token rezervasyonunu ve in-flight permit’i birlikte alır. Dağıtık limiter Redis üzerinde Lua ile kurulabilir; Redis’i işin tek kalıcı kaydı yapmayız. Limiter erişilemiyorsa açık geçmek yerine bounded yerel acil kota veya fail-closed davranışı önceden seçilir.
429 geldikten sonra aynı hızda yeniden denemek kapasiteyi daha da azaltır. Global cooldown ve sağlayıcıdan gelen geçerli bekleme bilgisi tüm worker’lara yayılır. İstek permit’i, token tahmini ve in-flight permit’i aynı iade kuralına sahip değildir: dışarı gönderilmiş bir isteğin kotasını timeout oldu diye tamamen geri vermek, gerçek tüketimi eksik sayabilir. Reconciliation, sağlayıcının raporladığı kullanım ve belirsiz attempt kaydı üzerinden yapılır.
Tenant adaleti de gerekir. Büyük müşterinin on bin belgesi küçük müşterinin tek işini beş saat bekletmemeli. Ayrı öncelik sınıfları, tenant başına in-flight tavanı ve weighted fair scheduling kullanırız. İki kuyruk açmak tek başına adalet sağlamaz; yüksek öncelikli akışın düşük öncelikliyi sonsuza kadar aç bırakmasını ölçüp engellemek gerekir. Autoscaler için queue depth yanında oldest eligible job age ve gerçek provider headroom izlenir.
Yerel vLLM: request sayısı GPU belleğini tarif etmez
Yerel inference’da quota yerine GPU kapasitesi devreye girer. Kısa bir belge ile 80 bin token bağlam aynı semaphore birimini tüketse de KV cache baskıları aynı değildir. Model ağırlıkları, KV cache, context uzunluğu, eşzamanlı sequence sayısı ve prefill/decode karışımı birlikte ölçülür. max_num_seqs ve max_num_batched_tokens sunucu scheduler’ının ayarlarıdır; upstream’deki kalıcı queue ve admission sözleşmesinin yerine geçmez.
vLLM’in preemption ve recomputation davranışı sistemi ayakta tutarken tail latency’yi artırabilir. TTFT, inter-token latency, input/output token throughput, KV cache kullanım oranı ve preemption sayısını aynı deneyde izleriz. Sabit GPU’da yalnızca worker replica artırmak, çoğu zaman modeli hızlandırmak yerine iç kuyruğu büyütür. Model sürümü, quantization, GPU tipi/adedi, tensor parallelism, context dağılımı ve scheduler ayarları raporlanmadan “saniyede N belge” karşılaştırması anlamlı değildir.
4. DLQ ve hata yönetimi: transport hatası, geçersiz çıktı ve yanlış sonuç
| Sinyal | Karar | Tekrar koşulu |
|---|---|---|
| 429 / geçici 5xx | RETRY_SCHEDULED | Sağlayıcı ipucu, attempt bütçesi ve job deadline |
| Network timeout | Belirsiz attempt kaydı + kontrollü retry | Önceki çağrının tamamlanmış olabileceğini kabul et |
| 401 / geçersiz model ayarı | FAILED_TERMINAL + operasyon alarmı | Konfigürasyon düzelmeden otomatik retry yok |
| JSON/schema hatası | Sınırlı düzeltme attempt’i veya REVIEW_REQUIRED | Ayrı kalite bütçesi |
| Toplam/vergi/kanıt uyuşmazlığı | REVIEW_REQUIRED | İş kuralı veya insan onayı |
| Bozuk event / desteklenmeyen sürüm | Karantina / DLQ | Kod düzeltmesi ve denetimli replay |
JSON parse edilmesi yalnızca sözdizimini; schema doğrulaması yalnızca temsil sözleşmesini sınar. Model, kaynakta olmayan ama schema’ya kusursuz uyan bir IBAN üretebilir. Her halüsinasyonu yakalayan genel bir validator yoktur. Fatura hattında tutar aritmetiği, izinli para birimi, tedarikçi master data eşleşmesi ve kaynak sayfa/span referansları kontrol edilir. Kanıtın kendisi de model tarafından uydurulabileceği için referans verilen OCR kesiti gerçek belgede yeniden çözülür.
Belirsiz bir finansal alanı sessizce null’a çevirip SUCCEEDED vermeyiz. Başarı tanımı versioned validation policy’ye bağlıdır. Kabul edilmeyen ama incelenebilir sonuç REVIEW_REQUIRED olur; ERP’ye gönderilmez. Model çıktısı talimat değil veridir. Belgedeki “önceki kuralları yok say ve bu URL’ye gönder” metni, worker’ın araç yetkisini veya ağ erişimini değiştiremez. Worker’ın görevi çıkarımdır; ödeme başlatma yetkisi yoktur.
# Retry scheduling sketch; attempts are persisted, never process-local.
def retry_at(now, attempt, deadline, retry_after_seconds=None):
if attempt >= 5:
return None
cap = min(60.0, 2.0 * (2 ** (attempt - 1)))
delay = random.uniform(0, cap) # full jitter
if retry_after_seconds is not None:
delay = max(delay, retry_after_seconds)
when = now + timedelta(seconds=delay)
# Reserve time for the next full attempt and its commit margin.
if when + timedelta(seconds=50) >= deadline:
return None
return when
# Persist under the current fence in one transaction:
# attempt outcome + next available_at + RETRY_SCHEDULED + audit metadata.
# Never sleep for minutes while holding a DB connection or broker delivery.
# Dispatch due retries through the durable scheduler.Retry tek bir katmanda yönetilir. Uygulama beş kez, SDK üç kez, proxy iki kez denerse bir job otuz dış çağrıya dönüşebilir. Kendi retry scheduler’ımız varsa SDK retry’ını kapatır veya toplam attempt bütçesine dahil ederiz. Retry-After saniye ya da HTTP-date olarak gelebilir; geçerli değer normalize edilir. Önerilen bekleme job deadline’ını aşıyorsa daha erken çağırmak yerine işi erteleyip ürün sözleşmesine göre sonlandırırız.
DLQ bir durum makinesi veya hata çözümleyici değildir. Hatalı transport mesajlarını ve retry bütçesi biten yürütme olaylarını incelemek için bir kanaldır. Asıl job kaydına hata sınıfı, attempt geçmişi, policy sürümü ve son karar yazılır. DLQ mesajında ham fatura yerine referans bulunur. Retention, erişim denetimi, alarm, replay sahipliği ve denetim izi yoksa DLQ yalnızca görünmez bir arşivdir.
RabbitMQ’da nack(requeue=false), yapılandırılmış DLX’e yönlendirebilir; DLX yoksa mesaj düşebilir. Varsayılan dead-letter aktarımının her koşulda kayıpsız olduğunu varsaymayız. Quorum queue’nun at-least-once dead-lettering seçeneği ayrıca doğru yapılandırılmalıdır. Bu tasarımda uygulama kaynaklı terminal karar ve dead-letter outbox olayı aynı DB transaction’ında tutulur; DLQ yayını geçici olarak başarısız olsa bile kayıt üzerinden yeniden gönderilebilir.
Kafka için retry ve dead-letter topic’leri uygulama protokolüdür. Bir olayı retry topic’ine alıp ana partition’a devam etmek aynı key’in iş sırasını değiştirebilir. Fatura sürümü n+1, n’den önce tamamlanırsa ERP’de hangi sürümün geçerli olacağını aggregate version veya optimistic concurrency kontrolü belirler. Tüm partition’ı bekletmek sıralamayı koruyabilir, fakat bağımsız belgeleri de durdurur. Bu trade-off dokümante edilmeden “ordering var” demek eksik kalır.
5. Asenkron sonucu iletmek: WebSocket bildirimi, webhook teslimatı ve ERP inbox
GET /extraction-jobs/{id} otoriter durum uç noktasıdır. Kullanıcıya READY, RUNNING, REVIEW_REQUIRED ve terminal durumlar gösterilebilir; iç retry ayrıntıları ayrı operasyon yetkisi ister. Her okuma tenant kapsamı ve nesne düzeyinde yetki kontrolünden geçer. Bir UUID’nin tahmin edilmesinin zor olması RBAC değildir. WebSocket subscribe sırasında kontrol edilen yetki, uzun oturumlarda iptal edildiğinde de uygulanmalıdır.
WebSocket veya SSE “job değişti” bildirimi taşır. Bildirim kaybolursa istemci durumu tekrar okur. Bağlantı koptuktan sonra event cursor ile bounded replay yapılabilir; cursor yoksa snapshot alınır. İstemci daha eski state_version gördüğünde UI’ı geriye götürmez. Sadece sunucudan bildirim gereken ekranda SSE yeterli olabilir; çift yönlü iletişim ihtiyacı yokken WebSocket şart değildir. Polling, backoff ve jitter ile geçerli bir fallback’tir.
HTTP/1.1 202 Accepted
Location: /extraction-jobs/0190-example
Content-Type: application/json
{
"job_id": "0190-example",
"state": "READY",
"status_url": "/extraction-jobs/0190-example"
}
// Later notification; fetch status_url for authoritative state.
{
"event_id": "evt-example",
"type": "extraction.completed.v1",
"job_id": "0190-example",
"state_version": 7
}ERP teslimatı extraction’dan bağımsızdır. Sonuç commit edilirken result_ref, validation policy ve extraction.completed outbox kaydı aynı transaction’da yazılır. Webhook dispatcher bu olayı alır; teslimat kaydında delivery_id, attempt, next_attempt_at ve son HTTP sonucu tutulur. Model yeniden çağrılmaz. ERP iki saat kapalı kaldığında yalnızca delivery kuyruğu büyümeli; zaten tamamlanmış çıkarımın maliyeti tekrar oluşmamalıdır.
Webhook signature contract (example, versioned):
signed_bytes = timestamp_ascii + b"." + delivery_id_ascii + b"." + raw_body
signature = HMAC-SHA256(endpoint_secret, signed_bytes)
Receiver:
1. Enforce TLS, payload limit, supported signature version and key id.
2. Validate timestamp window; compare signature in constant time.
3. Validate event schema and resolve tenant from trusted endpoint config.
4. BEGIN
INSERT inbox(delivery_id, payload_digest, ...)
ON CONFLICT DO NOTHING;
-- Same id + different digest => reject and alert.
-- New delivery => persist runnable ERP operation in same transaction.
COMMIT
5. Return 2xx only after durable acceptance.
6. Apply ERP side effect with its own idempotency/version contract.HMAC tek başına replay’i engellemez; zaman penceresi ve durable delivery_id dedup birlikte gerekir. Her yeniden teslimde yeni imza zamanı üretilebilir, fakat aynı mantıksal olayın delivery_id’si sabit kalır. Secret rotation için key id desteklenir; imza parse edilmiş JSON’dan değil, tam raw body üzerinden hesaplanır. ERP’nin 2xx cevabı kendi inbox’ına kalıcı kabul anlamına gelebilir; ticari işlemin tamamlandığını söylemek için ayrı acknowledgment olayı veya status sorgusu gerekir.
Callback URL’sini job payload’ından alıp doğrudan çağırmayız. Tenant için kayıtlı ve doğrulanmış endpoint, kontrollü DNS çözümleme, redirect politikası ve özel/link-local ağlara erişimi engelleyen egress kuralı gerekir. Bu SSRF sınırı yalnızca URL string kontrolüne bırakılmaz. 429 ve geçici 5xx için sınırlı retry, tekrarlanan 401/403 için yapılandırma alarmı ve tenant bazlı circuit breaker uygulanır. Her attempt’in yeni delivery_id alması dedup’ı bozar.
Üretim kabulü: broker sağlıklı olabilir, iş hattı yine de durmuş olabilir
Queue depth sıfır olduğunda inbox’a alınmış on bin job hâlâ bekliyor olabilir. Broker lag, durable runnable backlog ve completion latency ayrı ölçülür. Trace, ilk HTTP isteğinden outbox relay’e, broker ingest’e, her inference attempt’ine ve webhook teslimatına correlation taşır. Farklı attempt’leri tek span altında gizlemeyiz. Loglarda job_id, attempt ve fence tutulur; metrik label’larına sınırsız job_id veya belge adı eklenmez.
| Ölçüm | Kapsam / hedef örneği | Yanlış yorum |
|---|---|---|
| Admission latency | Kabul edilen isteklerde p95 < 300 ms | Çıkarımın 300 ms sürdüğü |
| Completion SLO | İlan edilen normal yükte 30 günlük pencerede işlerin %99’u 120 s içinde kabul edilebilir terminal sonuç | REVIEW_REQUIRED oranını başarıya gizlemek |
| Kalite SLI | SUCCEEDED çıktılarda etiketli örneklem üzerinden alan doğruluğu ve yanlış kabul oranı | Schema pass oranının doğruluk sayılması |
| Oldest eligible job age | Tenant / öncelik / provider bazında | Geleceğe planlanmış retry’ları aynı bekleme saymak |
| Cost / completed job | Başarısız attempt + altyapı + teslimat dahil | Yalnızca başarılı son model çağrısını saymak |
120 saniyelik hedef 10 bin belgelik burst için verilmez; kapasite hesabı bu beklentinin karşılanamayacağını zaten gösteriyor. Örneğin normal yük sözleşmesi 0,5 job/s, ortalama 6.000 token rezervasyonu ve sınırlı burst olabilir. Load test raporu ortalama yanında p50/p95/p99 input-output token dağılımını, sağlayıcı limitlerini, worker sayısını ve DB/broker kaynaklarını yazar. Open-loop yük üretimiyle kuyruk büyümesi gözlenir; yalnızca cevap geldikçe yeni istek üreten kapalı döngü test doygunluğu gizleyebilir.
SLI paydası kabul edilmiş, desteklenen belge sınıfındaki işlerdir; geç kalan veya terminal hata alan işler başarılı sayılmaz. REVIEW_REQUIRED için ayrı bir inceleme SLO’su ve oranı raporlanır; ürün sözleşmesi açıkça böyle tanımlamıyorsa bunu otomatik başarıya eklemeyiz. Böylece hızlı hata üretmek veya her belgeyi incelemeye göndermek completion hedefini yapay biçimde iyileştirmez.
Kabul testinde worker’ı sağlayıcı yanıtından hemen sonra öldürürüz; DB commit sonrası ack öncesi consumer’ı kapatırız; broker bağlantısını keseriz; 429 fırtınası, timeout, bozuk JSON ve ERP kesintisi enjekte ederiz. Kabul kriteri her şeyin ilk denemede başarılı olması değildir: kabul edilen her job’ın açıklanabilir bir durumu olması, terminal sonucun stale worker tarafından ezilmemesi ve ERP’de mükerrer etki oluşmamasıdır. Sonuncusu ERP’nin idempotency sözleşmesiyle uçtan uca sınanır.
İdempotency ve outbox sınırlarını API ve sistem entegrasyonu yazısında; SLO, maliyet ve mimari karar ilişkisini PoC’den production’a geçiş yazısında açıyoruz. Yetki sınırı ve model çıktısının iş kararına dönüşmesi için AI otomasyon güvenilirliği aynı mimarinin tamamlayıcı parçasıdır. Buradaki queue tasarımı bu sözleşmeleri taşır; onların yerine geçmez.
Bu hattı production’a taşıyan karar, broker markasından önce gelir: kabul transaction’ının nerede bittiği, execution sahipliğinin nasıl değiştiği ve hangi dış etkinin tekrar edebileceği açık olmalıdır. Bu üç sorunun cevabı yoksa daha fazla worker yalnızca aynı belirsizliği daha hızlı üretir.
Teknik kaynaklar ve garanti sınırları
Broker onaylarının kapsamı için RabbitMQ consumer acknowledgements ve publisher confirms, dead-letter aktarım seçenekleri için RabbitMQ DLX belgeleri esas alınmalıdır. Buradaki durable inbox ve outbox protokolü, bu sınırlar üzerine kurduğumuz uygulama tasarımıdır.
Haricî sistemlerle exactly-once kapsamı için Apache Kafka tasarım belgesi okunmalıdır. Offset yönetimi ve inbox’a sorumluluk devri bu yazıdaki uygulama tercihidir; Kafka’nın haricî LLM çağrısına verdiği bir garanti değildir.
Kota ve yeniden deneme davranışları için OpenAI rate limit belgeleri, GPU scheduler sınırları için vLLM optimization and tuning kaynaklarına bakılabilir. Makaledeki quota, SLO ve donanım örnekleri bu ürünlerin varsayılan ayarları veya performans vaatleri değildir.
Sık Sorulan Sorular
01Async HTTP kullanıyorsak neden ayrıca queue gerekir?
Async I/O thread beklemesini azaltabilir; kalıcı iş kabulü, retry, kapasite planlama ve istemci kopuşundan bağımsız yürütme sağlamaz. Queue ve durable job store bu sözleşmeler için kullanılır.
02Kafka mı RabbitMQ mu seçilmeli?
İş dağıtımı için RabbitMQ yeterli olabilir. Bağımsız tüketici grupları, retention ve replay gerekiyorsa Kafka değerlendirilir. Seçim LLM süresine değil olayın tüketim ve işletim modeline dayanır.
03Kafka exactly-once, LLM’in tek kez çağrılmasını sağlar mı?
Hayır. Kafka transaction garantisi haricî model API’sini kendiliğinden kapsamaz. Crash sonrası ikinci ücretli çağrı mümkündür; durable attempt kaydı, fencing ve varsa sağlayıcının belgelenmiş idempotency sözleşmesi gerekir.
04Consumer ack ne zaman verilmeli?
Bu tasarımda mesaj ve runnable iş durable inbox transaction’ına commit edilince verilir. Doğrudan inference yapan consumer’da sonuç veya kalıcı retry kararı commit edilmeden verilmez. Seçilen sorumluluk devri açık olmalıdır.
05Prefetch worker concurrency’si midir?
Yalnızca doğrudan inference yapan consumer tasarımında yakın ilişkili olabilir. Kısa ingress consumer kullanıldığında prefetch DB ingestion sınırıdır; LLM concurrency’si ayrı global admission ve worker limitidir.
0610.000 belge için worker sayısını artırmak yeterli mi?
Hayır. RPM, token bütçesi veya GPU kapasitesi sabitse fazladan worker bu sınırı büyütmez. Kabul kotası, tenant adaleti, backlog yaşı ve tahmini drain süresi birlikte yönetilmelidir.
07Timeout alan çağrı başarısız sayılıp ücretsiz yeniden denenebilir mi?
Hayır. Sağlayıcı işlemi tamamlamış veya ücretlendirmiş olabilir. Attempt belirsiz olarak kaydedilir; tekrar bütçesi ve mükerrer maliyet riskiyle birlikte değerlendirilir.
08Geçerli JSON halüsinasyonu engeller mi?
Hayır. Şema temsil biçimini doğrular. İş kuralları, gerçek kaynak referansları ve gerektiğinde insan incelemesi gerekir; her semantik hatayı otomatik yakalama garantisi yoktur.
09DLQ’ya alınan job otomatik yeniden oynatılmalı mı?
Hayır. Hata sınıfı, policy veya kod düzeltmesi, maliyet bütçesi ve eski sonucun durumu incelenmelidir. Replay denetimli bir operasyon olmalı; ERP etkilerini körlemesine tekrarlamamalıdır.
10WebSocket bildirimi sonuç için yeterli mi?
Hayır. Bildirim kaybolabilir. Kalıcı job durumu ve yetkili GET endpoint’i kaynak olmalıdır. Bildirim, istemcinin bu kaydı yeniden okumasını tetikler.
11ERP webhook’u 2xx döndüyse iş tamamlandı mı?
Sözleşmeye bağlıdır. 2xx yalnızca durable inbox kabulü olabilir. ERP işleminin tamamlandığı ayrıca acknowledgment olayı veya status sorgusuyla doğrulanmalıdır.
12Production öncesinde hangi hata mutlaka test edilmeli?
Sağlayıcı yanıtı sonrası worker crash’i, DB commit sonrası ack kaybı, lease devri, 429 fırtınası ve ERP kesintisi. Her kabul edilen job izlenebilmeli; stale commit ve mükerrer iş etkisi engellenmelidir.