MassTransit ZeroToHero — 1
MassTransit hakkında örnek uygulamalar ve anlatımlarda bulunacağım bu yazı serisinde en temel kavramlardan ileri seviye konulara kadar örneklere yer vereceğim. Tüm örnekleri github’da bulabilirsiniz.
Önceki yazımda RabbitMQ ile Queue yapılarına giriş yapmıştık bu yazımda ise konuyu bir aşama daha ilerletip, mesaj brokerlar’da ihtiyaç duyduğumuz Exception Handling, Test, Activity, Saga gibi yapıları kendi bünyesinde barındıran MassTransite giriş yapacağız.
MassTransit
MassTransit Nedir?
MassTransit, .NET için mesaj tabanlı, loosely-coupled (gevşek bağlı) asenkron iletişimden yararlanan uygulamalar ve hizmetler oluşturmayı kolaylaştıran ücretsiz, açık kaynaklı bir dağıtılmış uygulama framework’üdür.
Ne zaman kullanılır?
Son zamanlarda geliştirilen hemen her uygulamada mikro servis mimarisi, bunların haberleşmesi, queue mekanizmaları ile çalışan mesajlaşma temelli yapılar vb. ihtiyaçlar giderek artmaktadır. Her uygulamada belirli teknolojilere zaman harcayıp bunlar ile uzun araştırmalar ve altyapılar oluşturulmak gerekmektedir. Proje ihtiyaçlarına göre bu teknolojilerde de sürekli değişiklik yapmak zorunda kaldığımız durumlar ortaya çıkmaktadır. Bugün RabbitMQ ile başladığınız bir uygulamayı daha sonra ActiveMQ’ya geçirmeniz gerekmekte, bu teknoloji geçişleri de bize zaman kaybı ve ön görülemez planlama problemleri çıkartmaktadır.
İşte bu noktada en temel olarak MassTransit bize, ORM sistemlerinin veritabanı teknolojileri üzerinde sağlamış olduğu kolaylığı, mesaj tabanlı iletişim araçları için sağlamaktadır. EntityFramework ile çalışırken, kullandığınız veritabanı teknolojisinin PostgreSql veya MSSQL olduğunu umursamadan geliştirme yapabilirsiniz.
Benzer şekilde MassTransit RabbitMQ, ActiveMQ, Azure Service Bus, Amazon SQS ve InMemory gibi mesajlaşma teknolojilerini destekler. Bu teknolojiler arası geçiş yalnızca basit bir konfigürasyon ayarı ile gerçekleştirilebilir. Bunun yanında MassTransit mesajlaşma teknolojilerindeki Saga, Activity gibi yapıları da destekler ve Test için bize kolaylık sağlayacak kütüphanelere sahiptir.
Mesaj Transfer Yapıları
MassTransit’ de ilk olarak InMemory transfer ve RabbitMQ transfer örneklerinden bahsedeceğim.
InMemory Transfer
Herhangi bir mesaj broker kurulmasını veya çalıştırılmasını gerektirmediğinden test için harika bir araçtır. Aynı zamanda çok hızlıdır. Ancak kalıcı değildir ve bus durdurulursa veya işlem sona ererse mesajlar kaybolur. Bu nedenle, genellikle production ortamı için akıllıca bir seçenek değildir.
InMemory transfer, yalnızca tek bir process içinde kullanılmak üzere tasarlanmıştır. Birden fazla process arasında iletişim kurmak için kullanılamaz (aynı makinede olsalar bile).
InMemory Message Transfer Example
InMemory Message Transfer örneğinde, Console’da çalışan bir uygulama kullanıcının girdiği mesajı bus üzerinden publish eder, receive endpointi ile alınan mesaj yine console’a yazar.
InMemory Message Transfer Example Code
RabbitMQ
RabbitMQ yaygın olarak kullanılan open source bir mesaj broker uygulamasıdır. Queue(Kuyruk) yapısı ile çalışır. Küçük ölçekli uygulamalardan kurumsal uygulamalara kadar birçok kullanım alanı mevcuttur. Cloud ve on-premise ortamlarda deploy edilebilir ve çoklu mesajlaşma protokollerini destekler. Distributed çalışma imkanı sağlar ve işletim sistemi bağımsız çalışır. Daha fazlası için RabbitMQ ile ilgili yazımdan faydalanabilirsiniz.
MassTransit, birçok gelişmiş özellik ve yetenek dahil olmak üzere RabbitMQ’yu tam olarak destekler.
RabbitMQ Message Transfer Example
RabbitMQ Message Transfer örneğinde, çalışma mantığı bir önceki örneğimizle aynı olan yalnızca transfer yapısı olarak RabbitMQ kullanan bir uygulama oluşturduk.
Bir mesaj broker kullanmamız bize, mesaj hattının uygulamamızın dışında yönetilmesi imkanı sağlar. Diğer bir deyişle gönderdiğimiz bir mesaj işleme alınmadan uygulama sonlansa dahi RabbitMQ kendi sisteminde bu mesajı saklayacak ve uygulama tekrar ayağa kalktığında mesaj işletilecektir.
RabbitMQ Message Transfer Example Code
Yukarıda, çalışma mantığı aynı olan yalnızca mesaj transfer yöntemleri farklı olan iki örnek yaptık. MassTransit’in bize sağlamış olduğu en önemli özelliklerden birisi de çok kolay bir şekilde transfer yapılarımızı değiştirebiliyor olmamızdır.
Gördüğünüz üzere yalnızca “Bus Factory” kısmı ve “RabbitMQ” ya özel konfigürasyon kısmında farklılık var. Bu yapı bize mesaj brokerlarından soyutlanmış bir sistem oluşturmamız için kolaylık sağlamaktadır.
Mediator
İlk iki başlıkta MassTransit üzerinden mesajlaşma yapılarını kurguladık ve basit anlamda örnekler gerçekleştirdik. Yalnız gerçek uygulamalarda yapılar daha karmaşık ve dağıtık bir haldedir. Tüm işlemleri tek bir sınıf içerisinde toplamaktansa her sınıfın kendi işini yaptığı single-responsiblity’e uygun yapılar kurulmalıdır. Ayrıca mesaj yapılarında bazen gönderdiğimiz mesajların sonuçları üzerinde işlem yapmak isteriz. İşte bu noktada MassTransit bize Mediator yaklaşımı ile ihtiyaçlarımızı karşılayacak yapılar sunar.
Mediator Pattern
Nesne yönelimli programlamada, programlar genellikle birçok sınıftan oluşur. İş mantığı ve hesaplama bu sınıflar arasında dağıtılır. Bununla birlikte, özellikle bakım ve / veya yeniden düzenleme sırasında bir programa daha fazla sınıf eklendikçe, bu sınıflar arasındaki iletişim sorunu daha karmaşık hale gelebilir. Bu durum, programın okunmasını ve sürdürülmesini zorlaştırır. Ayrıca, herhangi bir değişiklik diğer birkaç sınıftaki kodu etkileyebileceğinden programı değiştirmek zor olabilir.
Mediator pattern ile, nesneler arasındaki iletişim bir aracı nesne içinde yönetilir. Nesneler artık birbirleriyle doğrudan iletişim kurmaz, bunun yerine arabulucu aracılığıyla iletişim kurar. Bu durum, iletişim kuran nesneler arasındaki bağımlılıkları azaltır.
MassTransit, publisherlar, consumerlar ve sagalar için tam destek sağlayan bir mediator uygulaması içerir. MassTransit Mediator, proses içi ve bellek içi olarak çalışır. Maksimum performans için, mesajlar serileştirilmek yerine referans olarak aktarılır ve kontrol akışları doğrudan Publish / Send metotları ile consumer’a gönderilir.
Mediator Periodic Table Example
Mediator Periodic Table örneğinde, console üzerinden Atom Numarası(ElementId) ile istekte bulunulur ve MassTransit Mediator yapısı bu mesajı işleyerek Periodic Table Consumer’a iletir. Periodic Table Consumer kendisine gelen istekteki ElementId bilgisine göre Periodic Table Servisine sorgu atar ve sonucu console’a yazar.
Mediator Periodic Table Example Code
Bu örnekte , consumer’ı ayrı bir sınıfa alarak soyutladık, bu sayede uygulama içerisinde göndermek istediğimiz her mesaj için bağımsız consumer’lar oluşturabilir ve her birinin kendi görevinden sorumlu olduğu yapılar kurabiliriz.
RabbitMQ
Mediator yapısı bizlere, consumerları sistemden soyutlama imkanı sunar. Her mesaj için farklı bir consumer oluşturabiliriz ve mesajın iletilmesi durumunda gerekli işlemleri ilgili consumer’ın içerisinde yönetebiliriz.
Bunun yanında mediator yapısı tek bir process içerisinde çalışır. Diğer bir deyişle mikro servis mimarisine uygun değildir. Bu tarz ihtiyaçlar için bir mesaj broker yapısı kullanmalıyız.
Bu noktada devreye RabbitMQ girer. Bu örneğimizde RabbitMQ Consumer App ve RabbitMQ Publisher App diye iki uygulama oluşturacağız ve her iki uygulama da konfigürasyon esnasında RabbitMQ ile çalışacak şekilde ayarlayacağız. Geri kalan her şeyi MassTransit bizim için yönetecektir.
Exceptions
Şu ana kadar yaptığımız örneklerde best-case üzerinden çalıştık. Peki istisnai durumlar oluştuğunda ne yapabiliriz. ElementId sayısal bir alan olmasına karşın, metin içeren ifadeler barındırırsa sistem nasıl davranır ve bunu nasıl yönetebiliriz. İşte tüm bu gibi durumlar için ExceptionHandling mekanizması gerekmektedir.
MassTransit, uygulamanızın exceptionlardan kurtulmasına ve bunlarla başa çıkmasına yardımcı olacak bir dizi özellik sağlar.
- Retry Configuration, Exception oluşması durumunda mesajın tekrar gönderilmesi için konfigürasyon ayarları;
None: Tekrar yok.
Immediate: Hemen bir daha dene.
Interval: Sabit bir süre sonra tekrar dene.
Intervals: Farklı süreler bekle ve dene.
Exponential: Üstel artan aralıklar ile dene.
Incremental: Sabit artan aralıklar ile dene. - Exception Filters, bazı durumlarda oluşan tüm exceptionlar için işlem yapmak istemeyebiliriz. Bu durumlarda Ignore ve Handle metodları yardımı ile ayarlamalarımızı yapabiliriz.
- Redelivery, sistemde oluşan hatalar her zaman kısa süreli problemlerden meydana gelmez. Uzak sunucu, SQL serverin çökmesi gibi durumlarda retry süremizi daha uzun vermek isteyebiliriz. Bu gibi durumlarda message scheduler kullanmamız gerekir. Scheduler yapısıyla daha uzun aralıklı süreler ile retry mekanizması kurabiliriz.
- Outbox, uzun süren hata durumlarında message scheduler’a alternatif olarak InMemoryOutbox kullanılabilir. Bu yapıda mesaj consume edilene kadar Outbox (Giden Kutusu) mesajı kendi bünyesinde barındırır ve uzun süren aralıklar ile retry imkanı sağlar.
- Faults, belirli bir mesaj için oluşturulan consumer’larda fırlatılan exceptionları yönetmek amacıyla ilgili consumer’a özel fault-consumer oluşturulabilir. Bu fault-consumer da exception’ı yakalayıp gerekli işlemleri gerçekleştirebiliriz.
RabbitMQ Periodic Table Exception Handling Example
Örnek için kullanım senaryoları;
- ElementId: 11 gönderirsek sonuç Na-Sodium(ElementAccepted) olacaktır.
- ElementId: 130 gönderirsek sonuç Element Not Found!(ElementRejected) olacaktır.
- ElementId: Na gönderirsek sonuç Na-Sodium(ElementAccepted) olacaktır.
- ElementId: Mm gönderirsek sonuç Element Not Found!(ElementRejected) olacaktır.
Hazırlık:
- İlk olarak Periodic Table Consumer’ımızın iki sonuç döndüğü yapıyı kuruyoruz. Bununla gelen ElementId parametresine karşılık bir element var ise ElementAccepted yok ise ElementRejected sonucunu döneceğiz.
- İkinci olarak, gelen ElementId alanı sayısal bir alan değilse exception throw edeceğiz.
- Daha sonra Periodic Table Consumer’ımız için Periodic Table Fault Consumer oluşturacağız. Fault Consumer sayesinde oluşan exceptionları yakalayabileceğiz.
- Exception konfigurasyonunda, hata durumunda 3 kez hemen deneme (Retry(Immediate(3))) yapısı kuracağız ve fault durumundaki queue’yu oluşturacağız.
Testing
MassTransit, tamamen bellekte çalışan ancak gerçek bir mesaj aracısına yakın davranan test araçları içerir. Bu test araçları, test edilmesi karmaşık olan asenkron işlemler için bize kolaylık sağlar.
Harness
Test harness, bus yapılandırmasını içerisinde barındırır. InMemory harness, kullanımı kolaydır, herhangi bir harici bağımlılık olmadan tamamen bellek içinde çalışır. Gönderilen, yayınlanan ve tüketilen mesajlar, serialization, dispatch, deserialization, consumer instantiation, ve asynchronous execution dahil olmak üzere tüm bir yaşam döngüsünden geçer. Bu da gerçeğe yakın simüle edilebilir bir test ortamı sağlar.
NUnit Test Examples
Fine Code Coverage
Bu tool sayesinde geliştirilen kodun ne kadarlık kısmının test edildiğini gözlemleyebiliriz. Free ve kullanımı basit bir araçtır.
Test Structure
Test örneğimize ilk olarak test dosya yapısından bahsederek başlamak istiyorum. Test dosyamız üç kısımdan oluşmakta;
- Inits, Bu kısma çalıştırılacak her test metodundan önce ve sonrasında yapılacak işlemleri koyacağız. Örneğimizde harness ve consumerların oluşturulduğu yer burası olacak.
- Utils, genellikle test metodlarının içeriği uzun olur ve ve gereğinden fazla kod tekrarı barındırır. Bu duruma engel olmak için sık kullandığımız kontrol metodlarını utils altına ekleyeceğiz ve test metotlarında kullanacağız.
- Tests, Gerçek test senaryolarını oluşturduğumuz ve test kodlarımızın olduğu bölümdür.
Test Inits
Her test metodundan önce SetUp ile harness ve consumerları oluşturuyoruz. TearDown’da ise test metodu tamamlandığında harness’ı durduruyoruz.
Test Utils
Test metodlarımızda kullanmak üzere kod tekrarının önüne geçmek için kontrol metodlarımızı koyduğumuz bölüm.
- IsConsumerWorked
harness, consumerHarness ve faultConsumerHarness’ın ilgili mesajları consume ettiğini kontrol ettiğimiz metottur. - IsEventsPublished
harness’ın ilgili mesajları publish ettiğini kontrol ettiğimiz metottur. - IsElementRejectedEquals
ElementRejected nesnesinin gönderilen değerler ile eşit olduğunu kontrol ettiğimiz metottur. - IsElementAcceptedEquals
ElementAccepted nesnesinin gönderilen değerler ile eşit olduğunu kontrol ettiğimiz metottur.
Test Cases and Test Structure
Aşağıdaki görselde 8 adet test senaryomuz ve test yapısı gösterilmektedir.
Should_publish_the_element_accepted_event; ElementId:11 olarak gönderdiğimizde, herhangi bir fault oluşmadan consumer’ın ElementAccepted mesajı döndüğü ve publisher’ın da bu mesajı aldığını test eden metot.
Should_publish_the_element_rejected_event; ElementId:130 olarak gönderdiğimizde, herhangi bir fault oluşmadan consumer’ın ElementRejected mesajı döndüğü ve publisher’ın da bu mesajı aldığını test eden metot.
Should_publish_the_element_accepted_event_after_fault; ElementId:Na olarak gönderdiğimizde, bir fault oluştuktan sonra fault-consumer’ın ElementAccepted mesajı döndüğü ve publisher’ın da bu mesajı aldığını test eden metot.
Should_publish_the_element_rejected_event_after_fault; ElementId:Mm olarak gönderdiğimizde, bir fault oluştuktan sonra fault-consumer’ın ElementRejected mesajı döndüğü ve publisher’ın da bu mesajı aldığını test eden metot.
Should_Element_Not_Found_return_when_send_130; ElementId:130 olarak gönderdiğimizde, herhangi bir fault oluşmadan consumer’ın ElementRejected mesajı döndüğü ve publisher’ın da bu mesajı aldığını, alınan mesaj içerisinde “130-Element Not Found!” olduğunu test eden metot.
Should_Na_return_when_send_11; ElementId:11 olarak gönderdiğimizde, herhangi bir fault oluşmadan consumer’ın ElementAccepted mesajı döndüğü ve publisher’ın da bu mesajı aldığını, alınan mesaj içerisinde “11-Na-Sodium” olduğunu test eden metot.
Should_Sodium_return_when_send_Na; ElementId:Na olarak gönderdiğimizde, bir fault oluştuktan sonra fault-consumer’ın ElementAccepted mesajı döndüğü ve publisher’ın da bu mesajı aldığını, alınan mesaj içerisinde “11-Na-Sodium” olduğunu test eden metot.
Should_Element_Not_Found_return_when_send_Mm; ElementId:Mm olarak gönderdiğimizde, bir fault oluştuktan sonra fault-consumer’ın ElementRejected mesajı döndüğü ve publisher’ın da bu mesajı aldığını, alınan mesaj içerisinde “Mm-Element Not Found!” olduğunu test eden metot.
Sonuç
MassTransit bizlere, queue yapıları ile çalışırken büyük kolaylıklar sağlamakta, exception handling, in-memory transfer, mediator pattern ve test harness gibi bir çok yardımcı tool sayesinde kapsamlı bir framework oluşturmaktadır.
Sonraki yazımda Messages, Consumers, Producers, Requests, Containers konularının ayrıntılarından ve geniş kapsamla Saga yapısından bahsedeceğim.