Caching Library

Se ritieni utile questo articolo, considera la possibilità di effettuare una donazione (il cui importo è a tua completa discrezione) tramite PayPal. Grazie.

Caching Library è un framework open-source di gestione della cache a più livelli, disegnato per disaccoppiare il front-end applicativo dall'implementazione concreta del sistema di cache, essere scalabile e riusabile. Sono previsti due livelli di persistenza della cache, utilizzati in modo elettivo: il livello più basso è il file system (più lento ma più capiente) mentre quello più alto è il sistema di cache nativo di .NET (System.Web.Caching.Cache). La libreria implementa anche un sistema di controllo dello stato della cache, definito "scavenging" che consente la rimozione automatica degli elementi scaduti e contiene le dimensioni allocate dalla cache di sistema rimuovendo, se necessario, gli elementi meno necessari mediante la definizione automatica della priorità degli oggetti in cache. La priorità di un elemento è calcolata bilanciando i due algoritmi più diffusi: MFU (Most Frequently Used) e LFU (Least Frequently Used). All'articolo sono abbinati i sorgenti del progetto, i file binary, la documentazione (scaricabile in formato .chm o consultabile on-line) ed una applicazione dimostrativa di utilizzo.

Introduzione

L'articolo Caching Architecture Guide for .NET Framework Applications pubblicato da MSDN illustra in modo esaustivo i requisiti che deve avere un buon sistema di cache e definisce l'architettura ottimale per la sua implementazione. I punti principali che emergono dalla lettura del suddetto articoli (ed in particolare dalla sezione "Understanding Advanced Caching Issues") sono:

  • Disaccoppiare il front-end applicativo dall'implementazione interna della persistenza della cache e dalle funzioni di accesso

  • Fornire elevate prestazioni e una soluzione scalabile

  • Implementare funzionalità avanzate specifiche, come dipendenze, tempo di vita e strumenti di gestione (ad esempio lo scavenging), consentendo agli utilizzatori di realizzare le proprie implementazioni di controllo

Architettura

L'implementazione di Caching Library prende spunto dall'architettura suggerita dalle linee guida di Microsoft, anche se, per semplicità, si è deciso di non implementare esplicitamente tutte le interfacce (in particolare quelle riguardanti i sistemi distribuiti). Al tempo stesso sono stati introdotti anche altri concetti che in qualche modo estendono le potenzialità del sistema di caching (si vedano ad esempio: la logica elettiva di utilizzo degli Storage e l'algoritmo di scavenging automatico).

Gli elementi principali del sistema, e le loro interazioni, sono mostrati nella figura seguente:

Diagramma degli elementi principali del sistema di caching

Le richieste di accesso alla cache vengono gestite dalla classe CacheManager che, oltre a recuperare o salvare i dati nello storage di persistenza della cache, si interfaccia con la classe CacheService, incaricata del controllo del sistema attraverso la gestione dei metadati degli elementi presenti in chache.

Cache Manager

Di seguito il diagramma relativo alla classe Cache Manager, ovvero l'interfaccia da cui l'applicazione accederà alla cache:

Cache Manager

La classe è statica, quindi non può essere istanziata.

I metodi pubblici da utilizzare sono:

  • Add per inserire o aggiornare un oggetto in cache. Gli oggetti, per essere archiviati in un Cache Storage, devono essere serializzabili, ovvero devono presentare l'attributo [Serializable].

  • Get per ottenere un oggetto dalla cache

  • Remove per eliminare un oggetto dalla cache

Per ogni operazione il CacheManager, come prima cosa, chiama il CacheStorage per l'accesso ai dati, quindi il CacheService per l'aggiornamento dei metadati relativi all'oggetto richiesto.

Il metodo "Scavenge", seppur visibile pubblicamente, è utilizzato internamente e non dovrebbe essere essere richiamato direttamente dal layer applicativo.

Cache Service

La classe Cache Service viene utilizzata internamente per controllare lo stato della memoria attraverso l'accesso ai metadati di ogni elemento presente in cache.

Cache Service

Il CacheService è implementato come singleton, quindi solo un'istanza della classe è presente in ogni dominio applicativo. Si utilizzi il metodo GetInstance per ottenere l'istanza concreta da utilizzare.

La classe espone i metodi per l'accesso ai metadati (Add, Get e Remove) ed implementa un meccanismo di controllo periodico dello stato della cache, attivato da un'istanza interna di tipo System.Threading.Timer. Ad ogni scadenza del timer viene eseguito il metodo Scavenge che rimuove dalla cache gli elementi scaduti e controlla che non sia stata superata la soglia massima di utilizzo degli storage. In qusto caso vengono rimossi dalla cache anche altri oggetti, fino a che l'utilizzo degli storage sia tornato sotto la soglia minima; gli elementi vengono eliminati in funzione della loro priorità calcolata (a questo proposito si veda la definizione della classe CacheItemMetadata).

Cache Storage

Il class diagram seguente mostra le entità relative allo storage dei dati e dei metadati:

Cache Storage

L'accesso agli storage viene fatto utilizzando l'interfaccia ICacheStorage, implementata da ogni storage concreto.

Al momento vengono definiti due storage: FileSystemCacheStorage (storage dei dati su file system) e SystemCacheStorage (storage dei dati nella cache nativa di ASP.NET, System.Web.HttpRuntime.Cache), utilizzati in modo elettivo (per una maggior comprensione del sistema elettivo di utilizzo degli storage si rimanda ai relativi casi d'uso).
Lo storage di sistema utilizza direttamente la cache di sistema, invocando il metodo Insert, aggiungendo sempre gli elementi con priorità pari a "CacheItemPriority.NotRemovable" poiché il controllo dello stato dello storage è affidato allo scavenging interno. Si noti che SystemCacheStorage espone anche un metodo RemovedCallback, attualmente non implementato (è disponibile per implementazioni o estensioni custom), utilizzato come delegate (System.Web.Caching.CacheItemRemovedCallback) per la notifica all'applicazione della rimozione di un elemento dalla cache.

Essendo gli storage implementati come singleton, il loro accesso viene fatto mediante l'utilizzo di una factory (CacheStorageFactory); il metodo GetCacheStorage restituisce l'istanza dello storage concreto richiesto sulla base dell'enumeratore CacheStorageProvider ricevuto.

Lo storage dei metadati (MetadataCacheStorage) risulta leggermente differente, in quanto implementato in modo specifico per ottimizzare l'accesso ai metadati informativi.

Metadati

I metadati contengono informazioni sugli elementi presenti in cache e sul loro utilizzo (richieste ricevute, tempo di vita, expiration, dimensione, ecc.):

Metadata

Per ogni tipologia di oggetto da salvare in cache è necessario fornire un delegate di tipo CacheItemMetadataCreation per la generazione dello specifico metadato (tempo di vita consentito, expiration assoluta, ecc.)

Come accennato parlando del metodo CacheService.Scavenge, i metadati, oltre alla proprietà HasExpired che indica che l'oggetto non è più attivo e deve essere pertanto eliminato, espongono un'altra importante proprietà calcolata: Priority. La priorità viene determinata in tempo reale contemplando diversi aspetti, così da considerare contemporaneamente i due algoritmi più diffusi per il controllo della cache: MFU (Most Frequently Used) e LFU (Least Frequently Used). In particolare viene utilizzata la seguente formula:

Priorità = |((1 / (Now - DataUltimoUtilizzo)) * (TotaleRichieste / (Now - DataCreazione)))| + 1

Gli elementi con HasExpired = true hanno priorità = 0, mentre per gli altri si valuta il tempo trascorso dall'ultimo utilizzo e l'utilizzo medio fatto dell'oggetto (il risultato è un numero >= 1).

Vengono utilizzate altre due classi (non rappresentate nel diagramma) per l'accesso alla collection dei metadati: CacheItemMetadataDictionary (di tipo "dizionario", ovvero consultabile per chiave) e CacheItemMetadataList (di tipo lista e ordinabile)

Eccezioni e utilità

A completamento dell'architettura del sistema di cache definiamo:

  • una classe (statica) per la generazione di chiavi univoche: KeyGenerator. Le chiavi vengono generate sulla base del tipo di oggetto (System.Type) e sui parametri che rendono univoco l'oggetto (ad esempio il valore della proprietà ID)

  • una classe CachingException che derivi da System.Exception per classificare in modo univoco le exception sollevate dal layer della cache in un'architettura multi-livello

Casi d'uso

Per meglio comprendere il funzionamento del sistema di cache analizziamo i casi d'uso per le operazioni CRUD (Create, Read, Update e Delete)

Inserimento o modifica

Caso d'uso: inserimento o modifica

Per inserire o modificare un elemento presente in cache viene invocato il metodo CacheManager.Add che inserisce l'oggetto nel FileSystemCacheStorage e rimuove eventuali copie dell'entità da SystemCacheStorage ed i relativi metadati, così da evitare l'accesso a elementi non aggiornati

Lettura

Caso d'uso: lettura

La lettura della cache (CacheManager.Get) richiede inizialmente l'oggetto al SystemStorage; qualora non sia possibile ottenere l'oggetto dalla cache di sistema, lo si cercherà nel FileSystemCacheStorage; se l'operazione ha successo l'oggetto viene aggiunto anche al SystemCacheStorage per gli accessi successivi. Contemporaneamente viene notificata al CacheService la richiesta ricevuta per l'aggiornamento dei metadati corrispondenti.

Cancellazione

Caso d'uso: cancellazione

La cancellazione della copia cache di un oggetto prevede la cancellazione dell'entità da entrambi gli storage (SystemCacheStorage e FileSystemCacheStorage) e la distruzione dei relativi metadati.

Configurazione

Il framework proposto per la gestione della cache può essere configurato direttamente dal file web.config con i seguenti parametri:

  • Caching.FileSystemCacheStorge.Path: percorso fisico per l'archiviazione della cache su file system

  • Caching.CacheService.ScavangeInterval: intervallo di esecuzione (in millisecondi) della pulizia della cache

  • Caching.CacheService.MaxMemoryUsage: memoria massima allocabile (in bytes) per la cache; al raggiungimento di questa soglia viene avviata la pulizia (Scavange)

  • Caching.CacheService.MinMemoryAvailableAfterScavenge: memoria minima disponibile (in bytes) per la cache dopo la pulizia

  • Caching.KeyGenerator.BaseNamespace: spazio dei nomi predefinito per la generazione delle chiavi

Di seguito un esempio:

<appSettings>

    <add key="Caching.FileSystemCacheStorge.Path" value="C:\cache\" />

    <add key="Caching.CacheService.ScavangeInterval" value="60000" />

    <add key="Caching.CacheService.MaxMemoryUsage" value="104857600" />

    <add key="Caching.CacheService.MinMemoryAvailableAfterScavenge" value="52428800" />

    <add key="Caching.KeyGenerator.BaseNamespace" value="G4N" />

</appSettings>

Scavenging

Come abbiamo visto il controllo dello stato della cache viene fatto automaticamente dal CacheService, che si preoccupa di analizzare e ripulire il SystemCacheStorage sulla base dei metadati. Per quanto riguarda il FileSystemCacheStorage, essendo predisposto per gestire diversi gigabyte (GB!) di dati, non è possibile pensare di controllare e ripulire in tempo reale lo spazio disco occupato. Di seguito si propone una possibile implementazione di gestione dello stato dello storage su file system, implementabile come applicazione console da eseguire periodicamente (Operazioni Pianificate di Windows) o come Windows Service in esecuzione in background. Viene proposto un algoritmo di pulizia molto semplice: la rimozione dei file a cui non si accede da oltre 7 giorni; chiaramente l'adozione di un meccanismo basato su questa logica prevede la corretta taratura in funzione del reale utilizzo della cache (il limite di una settimana di inattività potrebbe essere troppo o troppo poco in base al tipo di applicazione).

using System;
using System.IO;

public class ScavengeFileSystemCacheStorage
{
    // configuration
    private static string CACHE_PATH = @"C:\cache\";
    private static DateTime LAST_ACCESS_TIME_ACCEPTED = DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0));

    [STAThread]
    static void Main(string[] args)
    {
        DirectoryInfo root = new DirectoryInfo(CACHE_PATH);
        ProcessDirectory(root);
    }

    private static void ProcessDirectory(DirectoryInfo folder)
    {

        foreach (FileInfo fi in folder.GetFiles())
            ProcessFile(fi);
        
        foreach (DirectoryInfo di in folder.GetDirectories())
            ProcessDirectory(di);
        
        // remove empty folder
        if (folder.GetFiles().Length == 0 && folder.GetDirectories().Length == 0)
            folder.Delete(false);
    }

    private static void ProcessFile(FileInfo fi)
    {
        if (fi.LastAccessTime < LAST_ACCESS_TIME_ACCEPTED)
            fi.Delete();
    }

}

Integrazione

Il sistema di caching è stato progettato per essere utilizzato in applicazioni con architettura multi-livello (3-tier o n-tier); per una maggior comprensione di come il layer di caching possa essere integrato in un'applicazione n-tier si veda il progetto dimostrativo allegato.

Di seguito una possibile definizione di architettura (i livelli applicativi sono presentati con ordinamento bottom - up):

  • Data Layer - il livello di persistenza dei dati. Tipicamente viene utilizzato un database relazionale. Nell'esempio dimostrativo si veda il database SQL Server 2000 "Northwind", in particolare la tabella "Customers"

  • Data Access Layer - classi che implementano i metodi CRUD (creazione, lettura, aggiornamento ed eliminazione) dei dati persistenti. Nell'esempio dimostrativo si veda "CachingDemoLib/Customer/CustomerDataAccess.cs"

  • Caching Layer - classi per l'accesso allo storage dei dati in cache, così da ridurre gli accessi al Data Access Layer, aumentando di conseguenza le performance. Nell'esempio dimostrativo si veda "CachingDemoLib/Customer/CustomerCache.cs" e "CachingDemoLib/Customer/CustomerListCache.cs"

  • Business Logic Layer - classi che implementano la logica applicativa, le regole di business e quelle di validazione dell'applicazione. Nell'esempio dimostrativo si veda "CachingDemoLib/Customer/CustomerManager.cs"

  • Business Objects - classi che definiscono le entità (e le collezioni di entità) del dominio applicativo. La "view" (front-end) dell'applicazione dialoga esclusivamente con la Business Logic ed in termini di entità (o di tipi primitivi). Nell'esempio dimostrativo si veda "CachingDemoLib/Customer/Customer.cs" e "CachingDemoLib/Customer/CustomerList.cs"

  • Front-end - controlli, form, pagine dinamiche di presentazione delle entità a cui accede l'utente. Nell'esempio dimostrativo si vedano le pagine ASPX del progetto Web "CachingDemoWeb"

Alcune operazioni risultano ripetitive e, pertanto, è possibile creare framework o librerie per semplificarne e uniformarne l'esecuzione (si prenda ad esempio in considerazione la gestione delle connessioni al database, l'esecuzione di istruzioni SQL di scrittura e di lettura, ecc.). Allo stesso modo la libreria di cache proposta è stata ideata per gestire l'accesso alla cache: non contiene le logiche di un'applicazione specifica ma i metodi più generici e di uso comune.