Deadlines ve Cancellation İle Güvenilir gRPC Servisleri Geliştirmek
Merhaba,
gRPC kütüphanesi, client’lar tarafından yapılan istekleri kontrol edebilmek ve özellikle ihtiyaca istinaden iptal edebilmek için Deadlines ve Cancellation özelliklerini sunmaktadır. Bu içeriğimizde gRPC sistemlerde sunulan bu Deadlines ve Cancellation özelliklerinin neden önemli olduklarını değerlendirecek ve nasıl kullanılabileceklerini ele alacağız.
gRPC’de Deadlines
Deadlines, herhangi bir gRPC client’ının yaptığı request’in ne kadarlık adil bir süreye sahip olabileceğini ifade etmektedir. Belirtilen tarih aşıldığı taktirde ilgili request iptal edilecek ve böylece olası hatalarda yahut yanlış davranışlarda servislerin sınırsız çalışmasını ve sunucu kaynaklarını tüketmesini engelleyecektir.
Deadlines, güvenilir uygulamalar için faydalı ve önerilen bir özelliktir.
Sistem tarafından tanımlanmış(default) deadline zamanı yoktur. Dolayısıyla manuel bir yapılandırmada bulunulmadığı sürece request’lerde herhangi bir zaman sınırı söz konusu olmayacaktır.
Örnek amaçlı aşağıdaki çalışmayı ele alırsak eğer;
Client:
var channel = GrpcChannel.ForAddress("https://localhost:5001"); var messageClient = new MessageClient(channel); MessageResponse response = await messageClient.SendMessageAsync(new MessageRequest { Name = "Gençay", Message = "Selam olsun..." }, deadline: DateTime.UtcNow.AddSeconds(5)); Console.WriteLine(response.ResponseMessage);
Görüldüğü üzere client(messageClient) nesnesinin ‘deadline’ parametresiyle ilgili request’in ne kadarlık(5 saniye) bir ömre sahip olduğunu bildirmiş bulunmaktayız.
Server:
public override async Task<MessageResponse> SendMessage(MessageRequest request, ServerCallContext context) { await Task.Delay(10000); Console.WriteLine($"{request.Name} isimli client'dan \"{request.Message}\" mesajı alınmıştır."); return new MessageResponse { ResponseMessage = $"Thanks - {request.Name}" }; }
Server’da ise 3. satırdaki await Task.Delay(10000)
komutu sayesinde client’ta ki deadline süresini aşacak şekilde bir oyalama yapıyoruz.
Bu vaziyette uygulamaları derleyip, çalıştırdığımızda… Aşağıdaki gibi sonuçla karşılaşmaktayız.
Evet… Yapılan request neticesinde server, client’ın belirlediği ‘deadline’ değerini aştığı için bir DeadlineExceeded hatası fırlatılmaktadır.
Ayrıca yapılan istek neticesinde server’a da deadline bilgisi ‘ServerCallContext’ parametresi üzerinden gönderilmektedir.
Bu şekilde ilgili veriye erişebilmek server açısından olası iptal durumunda önleyici aksiyonların alınmasını sağlayabilir.
İşlemekte olan bir gRPC servisi üzerinden, yeni bir gRPC çağrısı yapıldığı durumlarda son tarih bilgisini aktarma
Client’tan istek almış ve hala işlemekte olan bir gRPC servisi içerisinde bir başka gRPC çağrısı yapıldığı durumlarda deadline bilgisi içteki gRPC çağrısına iletilmelidir. Bunun için yukarıda gördüğümüz ‘ServerCallContext’ parametresi gayet kullanışlı olabilir.
public class MessageService : MessageBase { readonly Message2Client _client; public MessageService(Message2Client message2Client) { _client = message2Client; } public override async Task<MessageResponse> SendMessage(MessageRequest request, ServerCallContext context) { await Task.Delay(2000); Console.WriteLine($"{request.Name} isimli client'dan \"{request.Message}\" mesajı alınmıştır."); Message2Response response = await _client.SendMessage2Async(new Message2Request { Name = "Gençay", Message = "Selam olsun..." }, deadline: context.Deadline); return new MessageResponse { ResponseMessage = $"Thanks - {request.Name}" }; } }
Lakin her iç içe gRPC çağrısında deadline değerlerini bu şekilde manuel yazmak oldukça zahmetli olacaktır. Ayrıca bu şekilde bir operasyonun çok olduğu senaryolarda deadline değerinin gözden kaçırılma ihtimalini de değerlendirirsek eğer deadline’ın her isteğe karşılık otomatik bir şekilde geçmesini sağlamak daha efektif bir çözüm olacaktır.
Bunun için ‘Startup.cs’ dosyasında aşağıdaki konfigürasyonun yapılması yeterlidir.
public void ConfigureServices(IServiceCollection services) { services.AddGrpcClient<Message2Client>(o => o.Address = new Uri("https://localhost:5003")).EnableCallContextPropagation(); services.AddGrpc(); }
Görüldüğü üzere ilgili client servisi dependency injection için IoC container’ına verilirken ‘EnableCallContextPropagation’ metodu sayesinde ilgili client’a yapılacak isteklere bu servise gelen deadline bilgileriyle yapılmasını sağlamış bulunmaktayız. Ayrıca buradaki örneklendirmede sonraki makalelerimizde ele alacağımız gRPC Factory Integration tasarımından istifade edildiğinden bahsetmekte fayda olduğu kanaatindeyim.
gRPC’de Cancellation
Cancellation, gRPC istemcilerinin ihtiyaç halinde yahut var olan bir isteğin artık ihtiyacına gerek kalmadığı durumlarda ilgili isteği iptal etmesini sağlayan bir özelliktir. Bu durumu şöyle bir örnekle daha da netleştirebiliriz; Bir kullanıcının, girdiği bir web sayfası üzerinden gerçek zamanlı güncellemeleri yayınlayan gRPC isteği başlattığını düşünelim. Kullanıcıyı bu istek süresince sayfada tutabileceğimizin garantisi olmadığı ve her an çıkma ihtimali olduğu için, olası durumda akışı devam ettirmemeli ve sonlandırma işlemini gerçekleştirmeliyiz. İşte böyle bir durumda Cancellation özelliği devreye girmekte ve isteği iptal edebilmemizi sağlamaktadır.
Gerçi yapılan isteği iptal edebilmek için ayriyetten Dispose’da edebilmekteyiz. Şimdi gelin bu iki durumu da aşağıdaki örnekle değerlendirelim.
var channel = GrpcChannel.ForAddress("https://localhost:5001"); var messageClient = new MessageClient(channel); CancellationTokenSource cancellationToken = new CancellationTokenSource(); AsyncClientStreamingCall<MessageRequest, MessageResponse> call = messageClient.SendMessage(cancellationToken: cancellationToken.Token); var t1 = Task.Run(async () => { int count = 0; while (true) await call.RequestStream.WriteAsync(new MessageRequest { Message = $"mesaj {count++}" }); }); var t2 = Task.Run(() => { ConsoleKey key = Console.ReadKey().Key; if (key == ConsoleKey.D) { call.Dispose(); Console.WriteLine("Süreç dispose ile durduruldu."); } else if (key == ConsoleKey.C) { cancellationToken.Cancel(); Console.WriteLine("Süreç callationtoken ile durduruldu."); } }); await Task.WhenAll(t1, t2);
Yukarıdaki kod bloğunu incelerseniz eğer; 5. satırda AsyncClientStreamingCall<MessageRequest, MessageResponse>
türünden bir referansa alınan isteğimiz parametre olarak ‘cancellationToken’(CancellationTokenSource) değeri almaktadır. Haliyle referansa alınan bu istek ya 20. satırda olduğu gibi Dispose edilmekte ya da 25. satırda olduğu gibi cancellationToken ile iptal edilmektedir. Her iki durumda da ilgili istek iptal edilecek ve akış sonlandırılacaktır.
Böylece gerektiği zaman gRPC üzerinden gerçekleştirilen eşzamansız(asenkron/async) operasyonlar lüzumsuz yere sunucudaki kaynakları tüketmeyecektir.
Haa! Unutmadan şu önemli bilgiyi de sizlerle paylaşıp makalemizi öyle noktalandıralım… gRPC ile yapılan isteklerin iptal edilme durumlarında alt çağrılara da bunu bildirmek için yine yukarıda ele aldığımız ‘EnableCallContextPropagation’ fonksiyonunu kullanmanız yeterli olacaktır. İlgili fonksiyon, iptal edilen bir isteğin işlev sürecindeki alt istekleri de iptal edecek ve böylece tam teferruatlı bir çalışma gerçekleştirilmiş olunacaktır.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…