Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Event-Driven Mimaride Yinelenen Mesajlar – Idempotent Sorunsalı ve Idempotent Consumer Pattern

Merhaba,

Yazılım olgusu, tabiatı gereği birbiriyle etkileşim kurma mecburiyetinde olan farklı bileşenlerden meydana gelip, anatomik olarak bütünlük arz eden bir olgudur. Bu bütünlüğün içerisindeki bileşenler arasındaki etkileşimi modelleyebilmemizi sağlayan türlü yaklaşımlar mevcuttur ve bu yaklaşımların arasından Event-Driven Architecture(EDA) etkin olarak olay tabanlı bir model ortaya koymaktadır. Bu yaklaşımda, bir bileşen etkileşim kurmak istediği diğer bileşen(ler)e bir event/olay göndermekte ve bu olaya karşı diğer bileşenler tepki vermektedirler. Bizler bu içeriğimizde bir bileşen tarafından yayınlanmış olan bir olayın diğer bileşenler tarafından türlü sebeplerden dolayı birden fazla kez işlenmesi durumunu ifade eden idempotent kavramını ele alıyor olacak ve bu durumun yazılım sürecinde yaratabileceği handikaplarla birlikte, bu duruma nasıl önlem alınabileceği üzerine istişarede bulunuyor olacağız. O halde buyurun başlayalım…

Idempotent Nedir?

Idempotent nedir sorusuna karşın outbox pattern‘ı ele aldığımız makalemizdeki konuya dair açıklamayı buraya alıntılayarak cevap vermeye çalışalım.

…matematik ve bilgisayar bilimlerinde kullanılan ve ilk işlem haricinde sonraki tüm işlemler için etkisi söz konusu olmayan ve sonucu değiştirmeden uygulanabilen bir özelliği ifade eden terminolojik bir terimdir. Matematiksel olarak f(f(x)) = f(x) şeklinde formülüze edilir

Idempotent’e matematiksel örnek olarak mutlak değeri verebiliriz. Nihayetinde mutlak değerin amacı bir sayının sıfıra olan uzaklığını ölçmek olduğu için |n| ile |-n| işlemlerini ne kadar değerlendirirsek değerlendirelim, bu iki ölçümde sonuç açısından herhangi bir fark hiçbir zaman söz konusu olmayacaktır.

Idempotent kavramına biz yazılımcıların anlayacağı dilden daha farklı örnekler verebilmemiz açısından rest mimarisi üzerinden de bir metafor gerçekleştirebiliriz. Rest mimarisinde; get, put ve delete action’ları idempotent iken, post metodu idempotent değildir! Çünkü get, put ve delete her ne kadar tekrarlı tetiklenirse tetiklensinler yaptıkları ilk işlemin dışında ekstradan sonuç üretemezler. Yani örnek olarak, ‘3’ id değerine sahip bir kullanıcı silmek(delete) istiyorsanız eğer bunu ilk istekte gerçekleştirir, sonraki isteklerde ise ‘3’ id değerine karşılık bir kullanıcı olmayacağından dolayı ekstradan bir sonuç üretecek işlem gerçekleştiremezsiniz. Veya update products set name='abc' sorgusunu ilk çalıştırdığınızda tüm ürünlerin adı ‘abc’ olacak sonraki tetiklemelerde ise bu sorgu bir değişikliğe yol açmayacaktır. İşte bundan dolayı bu üçü idempotent özellik sergilemektedir. Amma velâkin, adı ‘Hilmi’ olan bir kullanıcıyı eklemek(post) isterseniz her istek gönderdiğinizde adı ‘Hilmi’ olan bir kullanıcı eklenecektir. Bu da sonucu etkileyecektir. Çünkü ilk istekte adı ‘Hilmi’ olan kullanıcı sayısı n iken, beşinci istekte n + 5 olacaktır. Haliyle post action’ı netice itibariyle veritabanı üzerinde işlemden önceki son hale nazaran değişiklik/fark meydana getireceği için idempotent özellik göstermeyen bir fonksiyondur.

Onca örneğin yanında bir de günlük hayattan bir misalde bulunalım isterim. Diyelim ki X bankasının bankamatiğine gittiniz ve bakiyenizi görmek istediniz. İşte bu işlem bir idempotent işlemdir. Nihayetinde bankamatik üzerinden hesabınızdaki bakiyeyi görme işlemini ne kadar yaparsanız yapın sonuç değişmeyecektir. Lakin aynı hesap üzerinden para çekme ya da para yatırma işlemi yapmanız idempotent olmayan bir işlemdir. Çünkü yapılan her bir işlem neticesinde bakiye değeri değişecektir…

Yukarıda yaptığımız alıntının idempotent kavramı üzerine yeterince teorik bir kültür sağladığını düşünüyorum.

Haliyle içeriğin giriş paragrafında değindiğimiz -yazılım sürecindeki idempotent- durumunu izah etmemiz gerekirse eğer; event-driven architecture gibi distributed mimarilerde, bir servis tarafından herhangi bir event’in birden fazla kez işlenmesi durumu olduğunu söyleyebiliriz.

Idempotent Sorunsalı Nedir?

Bir event’in mahiyeti gereği yapılacak işin birden fazla kez işlenmesi neticesinde uygulama verilerinde tutarsızlık oluşabilir ve bu durum uygulama açısından genel manada istatistiği bozabilir. Misal olarak; bir kullanıcının hesabına para yatırma işlemi gerçekleştirildiğinde, doğal olarak hesap bakiyesi artacaktır. Ancak bu işlemi gerçekleştirecek bileşen; bakiye arttırıldıktan sonra türlü hatalardan kaynaklı kendini tekrar eder ve aynı işlemi tekrar devreye sokarsa burada hesap bakiyesinde ikinci kez artış söz konusu olacak ve istemsiz bir sonuç ortaya çıkacaktır.

Idempotent sorunsalı ise esasında idempotent tasarımının uygulanmadığı bir başka deyişle non-idempotent(idempotent olmayan) olarak sistemin kurgulandığı durumları ifade eder. Yani yukarıdaki verilen misaldeki tasarım idempotent sorunsalına zemin oluşturmaktadır. Bir servis yahut bileşende, bir olay neticesinde yapılacak işlem tekrar edilebilir bir şekilde tasarlanmışsa(yani idempotent tasarım yoksa) burada idempotent sorunsalından bahsedebiliriz. Böyle bir tasarım problemli, olası hatalara ve türlü istatistiksel tutsarsızlıklara açık bir tasarımdır.

Idempotent Sorunsalına Çözüm

Bu sorunu çözmek için, Idempotent Consumer Pattern kullanılabilir. Bu tasarım, bir işlemin birden fazla kez gerçekleştirilmesi durumunda sonucun değişmemesini sağlayacak yahut bir başka deyişle aynı işlemi birden fazla kez yürürlüğe koymayacak bir ilke sağlamaktadır. Bu ilke sayesinde bir olaya karşın aynı işlemin tekrar tekrar olabilme ihtimali minimize edilecek ve doğal olarak tutarsızlık gibi yan etkiler de engellenmiş olacaktır.

Idempotent Kavramı Sadece Event-Driven Architecture’da mı Geçerlidir?

Tabi ki de hayır! Esasında bu kavram sadece event-driven architecture için değil, genel olarak bilgi işlem alanında yaygın bir kavramdır. Basit arayüze sahip olan yazılımlardan tutun, kompleks veritabanı yönetim süreçlerinde karşımıza çıkabilecek bir sahaya sahiptir. Tüm bunların yanında distributed sistemlerde ve service-oriented architecture’da sıklıkla kullanılır. Yani anlayacağınız; idempotent, yazılım süreçlerinde hangi mimaride olunursa olunsun farkında olunması gereken ve sorunsal açıdan kritik süreçlerde değerlendirilmesi şart olan bir kavramdır.

Idempotent Consumer Pattern’ın Tasarımı ve Diğer Pattern’larla İlişkisi

Idempotent Consumer Pattern’ın temel fikri, yukarıdaki satırlarda ifade edildiği gibi bir işlemin aynı giriş parametreleriyle birden çok kez çağrılması durumunda yan etki olmaksızın işlenmesini sağlamaya dayanmaktadır. Evet… bunu gerçekleştirmek zor olsa gerek, hele hele çalışılan mimari distributed bir yapıya sahipse bunu sağlayabilmek gerçekten zor bir durum olsa gerek. İşte bu zor duruma Idempotent Consumer Pattern sayesinde çözümsel bir yaklaşım sergileyebiliriz.

Event-Driven Mimaride Yinelenen Mesajlar - Idempotent Sorunsalı ve Idempotent Consumer PatternBunu sağlayabilmek için bir olay neticesinde işlenecek mesajın sürece tabi tutulmasından önce yandaki gibi ilgili mesajın işlenip işlenmediğine dair gerekli tetkikin yapılması gerekmekte ve bunun için de işlenmiş mesajların benzersiz bir kimlik ile(unique identifier) bir veritabanında saklanması gerekmektedir.

Mesajı gönderen publisher bu mesajı bir id ile ilişkilendirecek ve consumer ise bu id üzerinden veritabanından gerekli kontrolleri yapıp süreci yönetecektir. Tabi burada publisher’ın outbox pattern‘ı uygulaması ve mesajları outbox table’a göndererek bir outbox publisher application aracılığı ile kuyruğa yani consumer’a göndermesi beklenebilir. Aynı şekilde consumer’ında işlediği mesajları inbox pattern‘ı uygulayarak inbox table’da tutması ve buradan mesajların işlenip işlenmediğine dair gerekli kontrollerin yapılması beklenebilir. Burada akla aşağıdaki soru gelebilir!

Idempotent Consumer Pattern İle Outbox ve Inbox Pattern’lar aynı amaca mı hizmet ediyorlar?

Dikkat ederseniz konumuz olan idempotent consumer pattern, outbox ve inbox pattern’larla kurgusal olarak bir benzerlik ilişkisine sahiptir. Ama asıl dikkat edilmesi gereken nokta şudur ki, bu pattern’lar her ne kadar birbirlerine benzerlik gösterseler de aralarında odaklandıkları sorunlar açısından farklılıklar vardır.

İsterseniz bu üç pattern arasındaki benzerlik ilişkisini ele alarak devam edelim.

Benzerlikler

  1. Güvenilir Mesaj İletimi
    Outbox Pattern ile Idempotent Consumer Pattern; gaye olarak işlenen olayların veya mesajların tekrar tekrar işlenmesini engelleyerek sistemdeki tutarlılığı korumak amacıyla güvenilir bir mesaj iletimini amaçlamaktadırlar.
  2. Durum Takibi
    Inbox Pattern ile benzer şekilde Idempotent Consumer Pattern da işlenen olayları veya mesajları takip eder. Her ikisi de işlenen olayların kimliklerini kaydederek, aynı olayın veya mesajın tekrar olacak şekilde işlenmesini engeller.
  3. Veri Tutarlılığı
    Her üç pattern’da, veri tutarlılığını sağlamak için kullanılır. Outbox ile Idempotent Consumer Pattern, tekrarlanabilirlikle ilgili sorunları ele alarak veri tutarlılığını korurken, Inbox Pattern ise gelen mesajların işlenme durumunu takip ederek veri tutarlılığını sağlar.

Şimdi de odaklandıkları sorunları ele alarak aralarındaki farklılıkları masaya yatıralım.

Odaklandıkları Sorunlar

  • Idempotent Consumer Pattern
    İşlenen olayların veya mesajların tekrar tekrar işlenmesini engelleyerek veri bütünlüğünü ve işlem tekrarlanabilirliğini sağlamaya odaklanır.
  • Outbox Pattern
    Bir servis yahut bileşen tarafından dış sistemlere güvenilir bir şekilde mesaj iletimini hedefler.
  • Inbox Pattern
    Gelen mesajların güvenilir bir şekilde işlenmesini ve işlenme durumunun takip edilmesini sağlayarak distributed sistemlerde veri bütünlüğünü korumaya odaklanır.

Haliyle buradan anlıyoruz ki; idempotent consumer pattern, outbox ve inbox pattern’lar ile benzerlik göstermekte ve hatta genellikle distributed sistemlerde birlikte kullanılmaktadır.

Idempotent Consumer Pattern’ın Uygulanması

Şimdi burada pratiksel olarak idempotent consumer pattern’ı basit düzeyde örneklendiriyor olacağız. Tabi bu örneklendirmede, outbox ve inbox pattern’larına dair bir detay barındırmayacak ve direkt olarak sade bir şekilde konuyu hedef alacağız. (ki bilmelisiniz ki; yukarıdaki satırlarda da anlatılmaya çalışıldığı gibi, bir tasarımda idempotent ilkesi en doğru şekilde outbox ve inbox pattern’ları eşliğinde uygulanabilir, bundan dolayı sizler gerçek çalışmalarınıza bu hassasiyetle yaklaşım sergilemelisiniz)

Idempotent olarak tasarlanacak mimaride mesajların işlenip işlenmediğini takip edebilmek için bir veritabanına ihtiyacımız olacağından bahsetmiştik. Haliyle bu işlemi aşağıdaki gibi bir tablo üzerinden yürütebiliriz.

CREATE TABLE [dbo].[Idempotent_Event]
(
  [OccuredOn] [datetime2](7) NOT NULL,
  [ProcessedDate] [datetime2](7) NULL,
  [Type] [nvarchar](max) NOT NULL,
  [IdempotentToken] [uniqueidentifier] NOT NULL
)

Yukarıdaki tablo yapısına dikkat ederseniz; ilgili mesajın hangi tarihte oluşturulduğu(OccuredOn) ve işlendiği(ProcessedDate), hangi event’e karşılık geldiği(Type) ve benzersiz olarak hangi idempotent id’sine sahip olduğu(IdempotentToken) bilgileri tutulmaktadır. Tabi bu bilgilerin yanında isterseniz outbox pattern’da olduğu gibi ‘Payload’ kolonunda ilgili mesajın/event’in içeriğini de depolayabilirsiniz.

Gelen mesajların consumer tarafında handle edilmeden önce işlenip işlenmediğini ise aşağıdaki gibi check edebiliriz.

    public class OrderCreatedEventConsumer : IConsumer<OrderCreatedEvent>
    {
        public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
        {
            ApplicationDbContext dbContext = new();
            if (await dbContext.IdempotentEvents.AnyAsync(idempotent => idempotent.IdempotentToken == context.Message.IdempotentToken))
                return;

            //Handling...
            //Handling...
            //Handling...
            Console.WriteLine($"{JsonSerializer.Serialize(context.Message)}");

            await dbContext.IdempotentEvents.AddAsync(new()
            {
                IdempotentToken = context.Message.IdempotentToken,
                OccuredOn = DateTime.UtcNow,
                ProcessedDate = DateTime.UtcNow,
                Type = nameof(OrderCreatedEvent)
            });
            await dbContext.SaveChangesAsync();
        }
    }

Yukarıdaki kod bloğunu incelerseniz eğer; ilgili tabloda idempotent token’a karşılık bir kayıt varsa eğer bu mesajın önceden işlendiğini anlayıp return ediyoruz, kayıt yoksa da mesajın handle edilip ardından işlendiğine dair bilgi tutabilmek için tabloya eklendiğini görüyoruz. Böylece bir işin tekrarlı olarak işlenmesinin önüne geçmiş oluyoruz.

Burada ‘la hoca, mesaj handle edildikten sonra veritabanına eklenirken bir hata meydana gelirse napcaz?‘ şeklinde bir ihtimale nazaran aklınızda suallerin oluştuğunu sezer gibiyim… Evet, haklısınız. Eğer ki mesaj handle edildikten sonra tam da veritabanına bu mesajın handle edildiğine dair kayıt atarken türlü sebeplerden dolayı hata alınırsa yine tutarsızlığa mahal verebilecek bir durumla karşılaşılmış olunacaktır. Bu durumu engelleyebilmek için ise hata kontrolü yapmalı ve hata meydana geldiği taktirde handle sürecinde yapılan işlemlerin hepsi geri alınmalıdır.

Şöyle ki;

    public class OrderCreatedEventConsumer : IConsumer<OrderCreatedEvent>
    {
        public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
        {
            try
            {
                ApplicationDbContext dbContext = new();
                if (await dbContext.IdempotentEvents.AnyAsync(idempotent => idempotent.IdempotentToken == context.Message.IdempotentToken))
                    return;

                //Handling...
                //Handling...
                //Handling...
                Console.WriteLine($"{JsonSerializer.Serialize(context.Message)}");

                await dbContext.IdempotentEvents.AddAsync(new()
                {
                    IdempotentToken = context.Message.IdempotentToken,
                    OccuredOn = DateTime.UtcNow,
                    ProcessedDate = DateTime.UtcNow,
                    Type = nameof(OrderCreatedEvent)
                });
                await dbContext.SaveChangesAsync();
            }
            catch
            {
                //Rollback Handling
                //or
                //Compensable Transactions
            }
        }
    }

Yukarıdaki kod bloğuna göz atarsanız eğer veritabanında idempotent token’a karşılık bir mesaj yoksa eğer gerekli handling operasyonu gerçekleştirilmekte ve mesajın işlendiğine dair veritabanına idempotent token ile ilişkili kayıt atılmaktadır. Yok eğer bu süreçte bir hata meydana gelirse eğer yapılan tüm işlemler geri alınmaktadır.

Idempotent Consumer Pattern’ı salt bir şekilde uygulanıyorsa eğer buna Lazy Idempotent Consumer yok eğer rollback handling yahut compensable transaction tasarımı göz önüne alınıyorsa eğer buna da Eager Idempotent Consumer adı verilmektedir.

Nihai olarak;
Idempotent sorunsalına karşı göstermiş olduğumuz davranış her ne kadar kritik bir tutum olsa da bazı işlemlerin doğal olarak idempotent olduğunu ve bu işlemlerde bu pattern’ın uygulanmasının gereksiz bir maliyete sebebiyet vereceğini aklınızdan çıkarmamanız gerekmektedir. Ayrıca yazı sürecinde sürekli ifade ettiğimiz gibi her ne kadar bu içeriğimizde tek başına idempotent consumer pattern’a odaklanmış olsak da distributed sistemlerde, outbox ve inbox pattern’lar eşliğinde idempotent mantığını uygulamak bütünsel açıdan daha anlamlı olacak ve hatta onlar olmaksızın idempotent consumer pattern’ın da bir anlamı kalmayacaktır diyebiliriz.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek projeye aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/Idempotent.Consumer.Pattern.Example

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir