.Net’de Database İşlemlerinde Bir Sorun Olması Durumunda Tekrar Deneme
Selamlar,
Aslında bu bu makalenin konusu, loglarda bolca “Transient failure” hata mesajı ile karşılaşılması sonucu ortaya çıkmıştır. Bu hata, genellikle ağ iletişimi, veritabanı bağlantısı veya diğer dış servislerle etkileşimde meydana gelen geçici anlık sorunlar için kullanılan bir terimdir. En temel sebebi ağ kopmaları (Network, Firewall, Virus uygulamaları, Rate Limiting), zaman aşımı, servis kesintileri veya yoğun yükte oluşabilecek geçici kaynak yetersizlikleridir. Biz de bu makalede, bu temel soruna çözüm olabilecek birkaç yöntemden, “Yeniden Deneme (Retry)” methodunu global bir şekilde uygulamaya çalışacağız.
.Net Üzerinde DBConnection Sorununa Default Çözüm:
Aşağıda görüldüğü gibi, DB üzerinde farklı tablolardan kayıt çekilmektedir. DB’ye erişim sorununda, belli bir sayıda tekrardan denenmesi amaçlanmaktadır.
Evet, .Net üzerinde Entity DBContext tarafında “OnConfiguring()” methodunda aşağıdaki gibi bir hata alırsan tekrarla şeklinde bir ayar vardır.
Ya da “program.cs”‘de gene aşağıdaki gibi bir ayar ile DB bağlantı hatası durumunda, tekrardan denenmesi için bir çözüm yolu bulunmaktadır.
Fakat bunların tamamı DBConnection hatası karşı alınmış bir çözümdür. Peki ya DB Connection sağlanıp, daha sonrasında bir hata ile karşılaşılır ise, işte bu soruna bir çözüm sunulmamaktadır.
Şimdi gelin önce zun yolu bir görelim:
Aşağıdaki örnekde, DBUser ve DBSecurityRole tablolarından kayıt çekilmektedir. “{try}/{catch}” bloğu içinde bir hata ile karşılaşılır ise, “retryCount” belirlenen max değere gelene kadar, ilgili operasyon manuel olarak tekrarlanır. Bu arada her hataya düşüldüğünde logfile’a bir kayıt atılmaktadır.
Fakat bu yöntemin, tüm projeye implemente edilmesi, sanıldığı kadar kolay da değildir. Hatta Global bir Exception yapınız var ise, tüm süprizi bozacaktır :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
[HttpGet("GetUserList")] public List Get() { var delay = TimeSpan.FromSeconds(5); int retryCount = 0; const string _logFilePath = @"C:\Logs\ReconnectLog.txt"; var directory = Path.GetDirectoryName(_logFilePath); // Check if the directory exists; if not, create it if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } while (true) { try { var users = from user in _context.DbUser join role in _context.DbSecurityRole on user.IdSecurityRole equals role.IdSecurityRole into roleLeft from role in roleLeft.DefaultIfEmpty() select new CustomUserModel { Name = user.Name, LastName = user.LastName, UserName = user.UserName, Password = user.Password, Email = user.Email, Gsm = user.Gsm, IsAdmin = user.IsAdmin, SecurityRoleName = role.SecurityRoleName, IdSecurityRole = role.IdSecurityRole, IdUser = user.IdUser, CreDate = user.CreDate }; return users.ToList(); } catch (Exception ex) { retryCount++; if (retryCount == 3) { System.IO.File.AppendAllText(_logFilePath, $"Failed after maximum retries. Exception Message: {ex.Message}" + Environment.NewLine); throw ex; } System.IO.File.AppendAllText(_logFilePath, $"Connection lost, retry attempt {retryCount} at {DateTime.Now}. Exception Message: {ex.Message}" + Environment.NewLine); Thread.Sleep(delay); } } } |
Daha sağlıklı bir yol için benim önerim Polly kütüphanesini kullanmaktır.
Öncelikle hadi gelin tekrar amaçlı Global bir servis yazalım:
DatabaseReconnectSettings: Database ile alakalı herhangi bir hata durumunda, kaç kere tekrar edileceği ve her seferinde ne kadar bekleneceği bu config dosyasında tanımlanmaktadır.
1 2 3 4 5 6 7 8 |
namespace LinqToDBBlog.Models { public class DatabaseReconnectSettings { public int RetryCount { get; set; } public int RetryWaitPeriodInSeconds { get; set; } } } |
appsettings.json: Aşağıdaki config örneğinde tekrarlama sayısı 3 ve iki hata arasında bekleme süresi 2sn olarak tanımlanmıştır.
1 2 3 4 |
"DatabaseReconnectSettings": { "RetryCount": 3, "RetryWaitPeriodInSeconds": 2 } |
DatabaseRetryService:
Service/IDatabaseRetryService: Aşağıda görüldüğü gibi, senkron ve asenkron olarak Polly kütüphanesi ile çalışacak iki tip tekrar Methodu, tanımlanmaktadır.
1 2 3 4 5 6 7 8 |
namespace LinqToDBBlog.Service { public interface IDatabaseRetryService { public T ExecuteWithRetry(Func action); Task ExecuteWithRetryAsync(Func<Task> action); } } |
Service/DatabaseRetryService:
1-) Aşağıdaki kod parçasında Constructor’da, DatabaseReconnectSettings config sınıfı parametrik olarak alınmıştır. Ayrıca constructorda yani sadece bir kere sınıf ayağa kalkarken, logun yazılacağı dosyanın konacağı folderın var olup olmadığı kontrol edilmiştir.
2-) Aşağıdaki kod parçasında Polly kütüphanesi kullanılarak, senkron bir şekilde “Handel(Exception)” yani sarmalandığı yerde oluşabilecek herhangi bir hata durumunda, “WaitAndRetry()” methodu kullanılarak bekle ve tekrar et şeklinde (“retryCount” kaç kere tekrarlanacağı, “retryAttempt” ne kadar sürede bir tekrarlanacağı ve “onRetry() yani hataya düşüldüğü durumda ne işlem yapılcağı mesela Log yazılacağı”) tanımlanmıştır.
3-) Aşağıdaki kod parçasında “Fallback“, yani tanımlanan tekrar sayısı aşılmış ise yapılacak işlem tanımlanmıştır. Yani işlemin başarısız olduğu kabul edildiği durumlarda, bu örnekde olduğu gibi yazılacak işlem iptal logu, aşağıdaki gibidir.
4-) Aşağıdaki örnekte Polly kütüphanesi kullanılarak, asenkron bir şekilde “Handel(Exception)” yani, sarmalandığı scoped içinde geçen asenkron actionın hataya düşmesi durumunda, “WaitAndRetryAsync()” methodu ile tanımlanan tekrar sayısı kadar çağrılacaktır.
5-) Aşağıdaki kod parçasında “FallbackAsync“, yani tanımlanan tekrar sayısı aşılmış ise, asenkron yapılacak işlem tanımlanmıştır. Kısaca asenkron işlemin başarısız olarak kabul edildiği durumlarda, bu örnekde olduğu gibi yazılacak işlem iptal logu aşağıdaki gibidir.
6-) Aşağıda görüldüğü gibi hata durumunda çağrılacak log methodu ve ilgili folderın var olup olmadığını belirleyen method, aşağıdaki gibi tanımlanmıştır.
Service/DatabaseRetryService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
using LinqToDB.SqlQuery; using LinqToDBBlog.Models; using Microsoft.Extensions.Options; using Polly; namespace LinqToDBBlog.Service { public class DatabaseRetryService : IDatabaseRetryService { private readonly IOptions _databaseReconnectSettings; private readonly string _logFilePath = @"C:\Logs\ReconnectLog.txt"; public DatabaseRetryService(IOptions settings) { _databaseReconnectSettings = settings; EnsureLogDirectoryExists(); } public TResult ExecuteWithRetry(Func action) { var retryPolicy = Policy .Handle() .WaitAndRetry( _databaseReconnectSettings.Value.RetryCount, retryAttempt => TimeSpan.FromSeconds(_databaseReconnectSettings.Value.RetryWaitPeriodInSeconds), onRetry: (exception, timeSpan, retryCount, context) => { LogRetryAttempt(retryCount, exception.Exception); }); var fallbackPolicy = Policy .Handle() .Fallback( fallbackValue: default(TResult), // Return default value for TResult (null for reference types) onFallback: (exception) => { File.AppendAllText(_logFilePath, $"Failed after maximum retries. Exception Message: {exception.Exception.Message}" + Environment.NewLine); throw exception.Exception; }); var retryWrapPolicy = Policy.Wrap(fallbackPolicy, retryPolicy); return retryWrapPolicy.Execute(() => action()); } public async Task ExecuteWithRetryAsync(Func<Task> action) { var retryPolicy = Policy .Handle() .WaitAndRetryAsync( _databaseReconnectSettings.Value.RetryCount, retryAttempt => TimeSpan.FromSeconds(_databaseReconnectSettings.Value.RetryWaitPeriodInSeconds), onRetry: (exception, timeSpan, retryCount, context) => { LogRetryAttempt(retryCount, exception.Exception); }); var fallbackPolicy = Policy .Handle() .FallbackAsync( fallbackValue: default(TResult), // Return default value for TResult (null for reference types) onFallbackAsync: async e => { await Task.Run(() => File.AppendAllText(_logFilePath, $"Failed after maximum retries. Exception Message: {e.Exception.Message}" + Environment.NewLine)); throw e.Exception; }); var retryWrapPolicy = Policy.WrapAsync(fallbackPolicy, retryPolicy); return await retryWrapPolicy.ExecuteAsync(() => action()); } private void LogRetryAttempt(int retryCount, Exception exception) { System.IO.File.AppendAllText(_logFilePath, $"Connection lost, retry attempt {retryCount} at {DateTime.Now}. Exception Message: {exception.Message}" + Environment.NewLine); } private void EnsureLogDirectoryExists() { var directory = Path.GetDirectoryName(_logFilePath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } } } } |
Şimdi Gelin Yazdığımız Bu DatabaseRetryService Nasıl Kullanılıyor Hep Beraber İnceleyelim:
Öncelikle ilgili servis program.cs’e, aşağıdaki gibi eklenir.
1 |
builder.Services.AddSingleton<IDatabaseRetryService, DatabaseRetryService>(); |
Sıra geldi ilgili servisi Controllerda kullanmaya. Öncelikle senkron çalışan bir Servis örneği yazılmıştır.
Aşağıda görüldüğü gibi hata olması durumunda “ExecuteWithRetry()” methodu içinde kalan kısım, belirlenen tekrar sayısı kadar çağrılacaktır. Bu örnekte ilgili LinqQuery “ToList()” methodu çağrılana kadar Execute olmayacaktır. ToList() methodu çağrıldıktan sonra, ilgili LinqQuery execute olacak ve hata durumunda tekrarlama işlemi yapılacaktır.
Kısaca, proje içinde karşılaşılabilecek hata durumunda:
- “try{} catch{}” blokları yazmadan
- Hata sayısı saydırılmadan
- While döngüsü içine alınmadan
Belirtilen sayıda tekrarlama işlemi yapılacak, başarısız olunması durumunda ilgili hata logu yazılacaktır.
Senkron WebApi Örneği GetUserList():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
namespace LinqToDBBlog.Controllers { [ApiController] [Route("[controller]")] public class LinqToDbController : ControllerBase { private readonly DashboardContext _context; private readonly IDatabaseRetryService _databaseRetryService; public LinqToDbController(DashboardContext context, IDatabaseRetryService databaseRetryService) { _context = context; _databaseRetryService = databaseRetryService; } [HttpGet("GetUserList")] public List Get() { var users = from user in _context.DbUser join role in _context.DbSecurityRole on user.IdSecurityRole equals role.IdSecurityRole into roleLeft from role in roleLeft.DefaultIfEmpty() select new CustomUserModel { Name = user.Name, LastName = user.LastName, UserName = user.UserName, Password = user.Password, Email = user.Email, Gsm = user.Gsm, IsAdmin = user.IsAdmin, SecurityRoleName = role.SecurityRoleName, IdSecurityRole = role.IdSecurityRole, IdUser = user.IdUser, CreDate = user.CreDate }; return _databaseRetryService.ExecuteWithRetry(() => { return users.ToList(); }); } |
Asenkron WebApi Örneği GetUserListFromTableName():
Yukarıdaki örneğe benzeyen ama bu sefer Asenkron olan WebApi örneğimizde, ilgili “ToListAsync()” çağrısı “ExecuteWithRetryAsync()” methodu ile sarmalanmış ve hata durumunda belirlenen tekrar sayısı kadar çağrılması sağlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[HttpGet("GetUserListFromTableName/{tableName}")] public async Task<List> GetFromTableName(string tableName) { var users = from user in _context.Set().ToLinqToDBTable().TableName(tableName) join role in _context.DbSecurityRole on user.IdSecurityRole equals role.IdSecurityRole into roleLeft from role in roleLeft.DefaultIfEmpty() where user.Deleted != true select new CustomUserModel { Name = user.Name, LastName = user.LastName, UserName = user.UserName, Password = user.Password, Email = user.Email, Gsm = user.Gsm, IsAdmin = user.IsAdmin, SecurityRoleName = role.SecurityRoleName, IdSecurityRole = role.IdSecurityRole, IdUser = user.IdUser, CreDate = user.CreDate }; return await _databaseRetryService.ExecuteWithRetryAsync(async () => { return await LinqToDB.AsyncExtensions.ToListAsync(users); }); } |
Test Edelim
Aşağıdaki videoda Senkron ve Asenkron methodlar için custom olarak yazılmış “databaseRetryService” kütüphanesinin, DB’den kayıt çekerken anlık erişim problemi olduğunda, tanımlanan tekrar sayısı kadar nasıl deneme yaptığı ve DBConnection sağlandığında eğer maximum tekrar sayısı aşılmamış ise kayıtları nasıl getirdiği swagger üzerinden testler ile gösterilmiştir.
Geldik bir makalenin daha sonuna. Bu makalede anlık geçici DB Sorunlarından dolayı (“Transient failure”), çalışan operasyonların nasıl yarıda kalmadan tamamlanabileceği anlatılmıştır. Bunun için kullanılabilecek farklı çözüm yolları vardır.
Aslında “Transient failure” (geçici hata) genellikle ağ iletişimi, veritabanı bağlantısı veya diğer dış servislerle etkileşimde meydana gelen geçici ve çözülmesi olası sorunlar için kullanılan bir terimdir. Bunun çözümü için ilk olarak implementasyonu uzun anam babam usulu :) Retry mekanizması kullanılmıştır. Try{} Catch{} blokları içinde, while ile tekrar tekrar koşan bir sistemin implementasyonu gerçekten çok zordur. Özellikle Global Exception yönetimi olan projelerde ekstra önlemlerin alınması gerekmektedir. Diğer bir yöntem ise Devre Kesici (Circuit Breaker)‘dır. Devre kesici, hatalar belirli bir eşik seviyesine ulaştığında hataları otomatik olarak geçici olarak engelleyen bir stratejidir. Polly, devre kesici kullanımını da destekler. Belki bir başka makalede bu konuya da detaylıca değinebiliriz.
Geçici hataların nedenlerini anlamak için, iyi bir izleme ve loglama stratejisi kullanmak, bize çokça zaman kazandırabilir. Bu, hangi hataların geçici olduğunu ve hangi hataların kalıcı olduğunu belirlememize yardımcı olabilir. Serilog, NLog, veya log4net gibi kütüphaneler, loglama ve izleme işlemlerini kolaylaştırabilir. Veritabanı veya ağ bağlantılarında kullanılacak uygun bağlantı havuzlama ve zaman aşımı ayarları, geçici hataların etkisini azaltabilir. Bağlantı havuzları, yeni bağlantılar oluşturmak yerine mevcut bağlantıları yeniden kullanarak performansı artırabilir ve geçici hataların etkilerini azaltabilir. Son olarak geçici hataların yönetimini test etmek için uygulamaları yük testlerine tabi tutmak faydalı olabilir.
Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
GitHub: https://github.com/borakasmer/DatabaseRetryPollyService
Source:
- https://www.pollydocs.org/
- https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly
Son Yorumlar