Outbox Pattern'i Neden Erken Ekledim
İki yazma, bir başarı tanımı

İlk sürüm düzdü: {SaveChanges}, sonra kuyruğa {Publish}. Mutlu yolda ikisi de oluyordu. Mutlu yol, demo. Gerçek yol, publish sırasında broker'ın 503 dönmesiydi. Sipariş vardı, dinleyen yoktu. Destek 'sistemde görünüyor' dedi, depo görmedi.
Tersini de gördüm: önce publish, sonra kayıt. Kayıt unique'e çarpınca mesaj uçmuştu. Dinleyen işi yaptı, kaynak satır yoktu. İki yazma, iki dünya. Aradaki ağ, işlem değil.
Tek işlem, iki yer
Outbox'ın vaadi basit. İş mesajı, iş kaydıyla aynı transaction'da durur. Röle, işlenmemiş satırları okur, broker'a verir, işaretler. Broker bir süre kör olsa bile kaynak gerçekliği durur. Gecikme vardır, kayıp yoktur — kayıp, işaretlemeyi yanlış yapınca gelir.
CREATE TABLE outbox_messages (
id uuid PRIMARY KEY,
type text NOT NULL,
payload jsonb NOT NULL,
occurred_at timestamptz NOT NULL,
processed_at timestamptz,
attempts int NOT NULL DEFAULT 0
);
CREATE INDEX outbox_messages_pending_idx
ON outbox_messages (occurred_at)
WHERE processed_at IS NULL;
Partial index, rölenin her seferinde tabloyu taramasını keser. {processed_at} doldukça satır soğur. Silmek ayrı bir temizlik; hemen silmek, teşhisi yok eder. Birkaç gün tutuyorum, sonra arşiv.
Yazma yolu
Domain olayı, SaveChanges'e gitmeden önce tabloya satır olur. Bunu interceptor'da veya açık bir {AddOutbox} çağrısında yapıyorum. Gizli sihir az olsun diye çoğu yerde açık çağrı tercih ediyorum: okuyan, mesajın nereden çıktığını görür.
public async Task PlaceAsync(PlaceOrder cmd, CancellationToken ct)
{
var order = Order.Place(cmd);
db.Orders.Add(order);
db.Outbox.Add(new OutboxMessage(
Id: Guid.CreateVersion7(),
Type: "OrderPlaced",
Payload: OrderPlaced.From(order),
OccurredAt: time.UtcNow));
await db.SaveChangesAsync(ct);
}
İki {Add}, bir {SaveChanges}. Commit olmazsa ne sipariş kalır ne mesaj. Commit olursa röle bakar. Röle bir kez değil, {attempts} ile bakar. Broker 200 deyince {processed_at} dolar. 200 demeden işaretlemek, kaybı geri getirir.
Neden erken
Erken eklemenin gerekçesi trafik değil, başarı tanımı. İlk günden iki yan etki varsa — kayıt ve haber — kopma ihtimali vardır. Trafik azken kopma da az görünür; görünmeyince 'gerek yok' denir. İlk yoğun günde aynı kod, sessiz kayıp üretir.
Outbox'ı her {INSERT} 'e koymuyorum. Mail şablonu denemesi, admin'in elle attığı iş, tek seferlik iç araç. Oralarda kayıp can sıkar, muhasebe bozmaz. Para, stok, sipariş durumu: orada erken.
Inbox karşı taraf. Aynı mesajı iki kez işlemek istemiyorsanız, tüketicide de bir tablo durur. Outbox tek başına 'en az bir kez'i 'tam bir kez' yapmaz; yayın tarafını toparlar. İkisini karıştırmak, birini unutturur.
Rölenin sıkıcı kuralları
Röleyi request thread'inde çalıştırmıyorum. Kullanıcı isteği commit ile bitsin. Yayın, arka plan. Aynı process'te hosted service yeter; ayrı servis şart değil. Şart olan, işaretlemenin yayın başarısından sonra gelmesi ve çökünce kaldığı yerden devam.
Sıra: iş alanına göre. Aynı {orderId} için {OrderPlaced} 'ten önce {OrderPaid} gitmesin istiyorsanız, ya tek akış numarası ya da tüketici dayanıklılığı. Outbox sihirli bir sıralayıcı değil; satırları basar.
Erken ekledim çünkü ikinci yazmayı 'bir satır daha' sanıyordum. O satır, işlem sınırının dışındaydı. Sınırın dışındaki her yayın, bir gün kayıp hikâyesi olur. Hikâyeyi üretimde dinlemektense, tabloyu baştan koydum.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap