SyncedOutputCache ile Aynı Anda Invalide Olan Output Cache Implementasyonları

By Burak Tungut - 2.3.2016 - Kategori Asp.Net MVC

Selam millet!

Küçük bir ev taşıma, yerleşme arasından sonra tekrar beraberiz. Aslında Elasticsearch ve SOLID konularında ayrı ayrı yazı dizileri başlatmıştım fakat okuyacağınız bu makalede bunlara hiç değinmedim. Geçtiğimiz haftalarda iş yerinde geliştirdiğimiz bir üründe doğan bir ihtiyacımıza karşı bir implementasyon yapmak durumunda kaldık. Size bu tecrübemi aktarmak istiyorum.

OutputCache Şart Oldu

Öyle bir ürün düşünelimki kullanıcı adı ve şifremiz ile authenticate oluyor ve uygulamada gezinmeye başlıyoruz. Fakat öyle sayfalarımız varki içerisindeki sayıların, data'ların uyuşması, tutarlı olması gerekli. Diğer bir yandan ise performans için disk'ler üzerindeki IO'yu düşürmek istiyoruz.

Böyle bir durumda akla Output Cache kullanmak gelebilir. Fakat Asp.Net MVC içerisinde halihazırda var olan OutputCacheAttribute bu ihtiyacımızı tam olarak karşılamayacaktır. Çünkü var olan output cache implementasyonu söz konusu action execute edildikten sonra çalışır. Bu da sayfaların (ya da partial'ların) cache'e alınmış hallerinin aynı anda invalide olmayacağı ihtimali anlamına gelir.

Anlaşılabilir olması için şöyle bir örnek ile ilerleyelim. A sayfamızda sipariş verilmiş ürünlerin sayısı gösterilirken, detayları içeren sayfası sipariş verilmiş olan ürünlerin detayını göstersin. Eğer A ve B sayfaları aynı source'u kullanmıyorsa bu sayfaların output cache'e alınması tutarlı sayıların gösterilmesi yönünde bizi zor duruma sokabilir. Birinin cache'i 3 dakika diğerinin 15 dakika sonra invalide olacağını düşünürsek yeni gelmiş olan kayıtlara ait data ya da sayılar birinde gözükürken diğerinde gözükmeyecektir.

İşte bunun gibi durumları ortadan kaldırabilmek için tüm cache'leri sync hale getirmemiz gerekebilir. Biz bunu yaparken var olan OutputCacheAttribute'ünü inherited etmiş yeni bir sınıf kullanacağız. Amacımız ise x zamanında gelen ve output cache'e alınmak istenen bir request'in x+10 ya da x+60 gibi bir sürede invalida olması değil, bir sonraki 10. ya da 60. (bu sayı bize kalmış, parametrikte olabilir) dakikada invalide olması.

Şöyle bir senaryo düşünebiliriz. A sayfasına 23.03'de girildi ve cache'e alındı. Bu cache'in invalide işlemi 23.10'da yapılmalı. Hal böyleyken 23.07 de dahi B sayfasına girilse burası için yaratılan cache'de 23.10'da invalide olacaktır.

Yani cache'lerimiz sonraki dakika bloğunu tamamlayacak şekilde expire olmalı.

Expire Zamanının Hesaplanması

Şimdi yukarıdaki anlatılan business'a göre expire zamanının hesaplanmasını gerçekleştirelim. Öyle bir method yazmalıyız ki parametre olarak expire edilecek dakikayı almalı (10,15,30 gibi) ve geriye söz konusu cache'in tam olarak hangi zamanda invalide edileceğini bildiren bir değer döndürmeli. Bu değerin DateTime olması daha anlaşılabilir olacağı için aşağıdaki gibi bir method yazabiliriz.

private DateTime GetExpireDateTime(int expirationMinute)
{
    var retVal = DateTime.Now;

    retVal = retVal
        .AddSeconds(retVal.Second * -1)
        .AddMilliseconds(retVal.Millisecond * -1);

    if (expirationMinute == 60)
    {
        retVal = retVal
            .AddHours(1)
            .AddMinutes(retVal.Minute * -1);
    }
    else
    {
        retVal = retVal
            .AddMinutes(expirationMinute - retVal.Minute);
    }

    return retVal;
}

Bu methoda gelecek olan expirationMinute aslında bir expire block niteliği taşıdığı için 60 gelme durumu da söz konusu olacaktır. Örneğin 15. dakikanın katlarında invalide olması istenen sayfalarımız varsa herhangi bir saatin 45. dakikasından sonra gelecek olan bir request sonraki saatin ilk saniyelerinde invalide olmalıdır. Bu nedenle 60 olması durumunda bir sonraki saatin dakika, saniye ve saliselerini siliyoruz.

Implementasyonun Tamamı

Bu ihtiyaç için benim yarattığım implementasyonun tamamı ise aşağıdaki gibi olacaktır. Her yiğidin kendine has yoğurt yiyişi olduğunu unutmadan inceleyelim :)

public class SyncedOutputCache : OutputCacheAttribute
{
    public int ExpireBlock { get; set; }

    public SyncedOutputCache()
        : base()
    {
        int currentMinute = int.Parse(DateTime.Now.ToString("mm"));
        int expirationMinute = (int)Math.Ceiling(((double)currentMinute + 0.1) / (double)ExpireBlock) * ExpireBlock;

        var expire = GetExpireDateTime(expirationMinute);

        base.Duration = (int)(expire - DateTime.Now).TotalSeconds;
    }

    private DateTime GetExpireDateTime(int expirationMinute)
    {
        var retVal = DateTime.Now;

        retVal = retVal
            .AddSeconds(retVal.Second * -1)
            .AddMilliseconds(retVal.Millisecond * -1);

        if (expirationMinute == 60)
        {
            retVal = retVal
                .AddHours(1)
                .AddMinutes(retVal.Minute * -1);
        }
        else
        {
            retVal = retVal
                .AddMinutes(expirationMinute - retVal.Minute);
        }

        return retVal;
    }
}

Artık yeni Output Cache implementasyonumuzu aşağıdaki gibi kullanabiliriz;

public class HomeController : Controller
{
    [SyncedOutputCache(ExpireBlock = 15)]
    public ActionResult A()
    {
        return View();
    }

    [SyncedOutputCache(ExpireBlock = 15)]
    public ActionResult B()
    {
        return View();
    }
}

Bu sayede her 15. dakika ve katlarında söz konusu action'a ait olan output cache invalide olacaktır. Sonraki yazımda görüşmek üzere :)

Yorum Bırak

Facebook
Son Yorumlar