Partecipare ad un Transazione gestita, System.Transactions.Transaction

di Mauro Servienti – Microsoft MVP

In questa pagina

Introduzione Introduzione
L’infrastruttura offerta da .NET Framework L’infrastruttura offerta da .NET Framework
I “resource manager” I “resource manager”
L’implementazione L’implementazione
Conclusioni Conclusioni

Introduzione

Sempre più spesso durante l’esecuzione di una serie di operazioni si ha la necessità di garantire che queste vengano viste dal sistema ospite (in questo caso la nostra applicazione) come un unico blocco: la sequenza quindi deve essere ritenuta completata se, e solo se, tutti i singoli passi all’interno della sequenza vanno a buon fine; in caso contrario, lo stato del sistema deve restare inalterato e, ogni singolo elemento e/o oggetto che ha subito modifiche durante la sequenza deve essere riportato allo stato originale, antecedente alle modifiche.

Questo sistema è ben noto nel mondo informatico con il nome di Transazione, che incapsula bene il principio di unità di lavoro (Unit Of Work). Una transazione deve rispettare quattro caratteristiche fondamentali, perchè possa essere definita tale, note con l’acronimo di ACID:

  • Atomic: una transazione deve essere atomica (tutte le operazioni comprese all’interno di un contesto transazionale o vanno a buon fine e devono essere considerate tutte come fallite);

  • Consistent: una transazione deve essere consistente (tra lo stato pre-transazione e lo stato post-transazione non devono essere violati i vincoli di integrità, i dati non devono cioè essere inconsistenti al termine della transazione);

  • Isolated: un contesto transazionale è isolato dal mondo che lo circonda (è quindi lecito aspettarsi che le modifiche apportate durante la transazione non siano visibili all’esterno del contesto transazionale fintanto che la transazione non si è conclusa);

  • Durable: i dati, al termine della transazione, devono essere registrati in maniera durevole;

Facciamo subito un esempio, per chiarirci bene le idee: supponiamo di avere il più classico degli “object model”, in cui figurano alcuni attori: l’anagrafica cliente, rappresentata da un oggetto Customer; il concetto di ordine, rappresentato dall’oggetto Order; infine il dettaglio dell’ordine rappresentato dagli oggetti OrderItem e OrderItemCollection.

Il nostro modello di business prevede che se la creazione di uno solo degli elementi che compongono l’ordine non va a buon fine l’ordine non debba poter essere creato. Abbiamo quindi un flusso che può essere così esemplificato:

Inizio del contesto transazionale

Creazione del “Customer

Creazione del “Order

Creazione della “OrderItemCollection

Non ci sono stati problemi

Conferma delle modifiche (Commit)

Altrimenti

Ripristino dello stato precedente (Rollback)

Chiusura del contesto transazionale

Come si vede bene dall’esempio tutte le operazioni racchiuse all’interno del contesto transazionale saranno ritenute valide se al termine delle operazioni non si sono verificati problemi tali da richiedere un ripristino (Rollback) dello stato alla situazione antecedente l’inizio del contesto transazionale.

Molto spesso quando si parla di transazioni la prima cosa a cui si pensa sono i database e si pensa subito alle possibilità che gli RDBMS offrono in termini di transazioni: è infatti possibile chiedere al database server di eseguire una serie di comandi (statement sql) all’interno di un contesto transazionale garantendoci, di fatto, che l’operazione vada a buon fine se, e solo se, tutti comandi eseguiti hanno esito positivo.

Ma il mondo dei database non è l’unico che offre contesti transazionali: all’interno del mondo .NET, ad esempio, gli Enterprise Services offrono la possibilità di gestire contesti transazionali.

 

L’infrastruttura offerta da .NET Framework

Il .NET Framework 2.0 offre una infrastruttura completamente nuova che ci permette di fare uso delle transazioni in maniera decisamente semplice ed intuitiva. Non solo: la nuova infrastruttura ci offre la possibilità di inserire all’interno del contesto transazionale anche i nostri oggetti, fornendo alle nostre applicazioni, a tutti gli effetti, capacità transazionali.

Il namespace di riferimento è System.Transactions, che contiene al suo interno un numero non indifferente di classi ed interfacce tutte destinate a gestire i contesti transazionali.

L’infrastruttura offre due sistemi per gestire una transazione: un sistema che chiameremo esplicito basato sulla creazione esplicita di una transazione (Transaction) e un sistema implicito basato su un oggetto che si chiama TransactionScope che ha lo scopo di semplificare l’intera gestione del ciclo di vita di una o più transazioni.

Sarà nostra cura cercare di appofondire quest’ultimo aspetto, che è quello ampiamente consigliato dalla documentazione del .NET Framework.

L’apertura di un contesto transazionale è un’operazione piuttosto semplice:

using( TransactionScope scope = new TransactionScope() )
{
Console.WriteLine( "This code is executed inside a transaction!" );

scope.Complete();
}

La classe TransactionScope (che implementa l’interfaccia IDisposable) espone un metodo Complete() se al termine delle operazioni il metodo non viene chiamato la transazione viene considerata fallita e lo stato degli oggetti, a patto che questi supportino contesti transazionali, viene ripristinato.

E’ decisamente comodo gestire un’istanza della classe TransactionScope all’interno del costrutto using di C#, perchè qualora si arrivasse alla fine del blocco using senza aver chiamato Complete(), volutamente o a causa di una eccezione la transazione verrebbe automaticamente ripristinata.

Il costruttore della classe TransactionScope offre vari overload che permettono di specificare come vogliamo che la transazione venga creata, per i dettagli delle singole implementazioni vi rimando alla documentazione presente su MSDN e riportata in calce.

Trattiamo qui solo ed esclusivamente uno degli overload del costruttore:

public TransactionScope( TransactionScopeOption )
{

}

L’enumerazione TransactionScopeOption permette di specificare come vogliamo che la transazione venga creata, nello specifico i valori disponibili sono:

  • Required: viene creata una nuova transazione nel caso non ne sia presente già una, (questo è il comportamento predefinito);

  • RequiresNew: viene creata una nuova transazione anche se ne esiste già una;

  • Suppress: viene esplicitamente richiesto che la porzione di codice all’interno di quell’ambito non sia soggetta a transazioni;

Mi soffermo solo sull’ultimo punto che ritengo meriti un piccolo esempio per capire quali sono le sue applicazioni/implicazioni. Supponiamo di avere un sistema di tracing del nostro codice che esegue trace verso un database: ci ritroveremmo nella spiacevole situazione in cui, qualora il codice di trace fosse racchiuso in un contesto transazionale sarebbe anch’esso soggetto a Rollback nel caso in cui la transazione fallisca. Questo è proprio quello che non vogliamo perchè perderemmo tutte le informazioni relative all’errore che ha causato il Rollback. In questo caso è quindi necessario racchiudere il codice che esegue il tracing in un nuovo contesto che chiede esplicitamente di non essere incluso in nessuna transazione.

Torniamo a TransactionScope e cerchiamo di capire in quale modo si possa trarre vantaggio dall’infrastruttura. Un esempio decisamente semplice e al contempo lampante è proprio quello di una transazione distribuita su più database:

using( TransactionScope scope = new TransactionScope() )
{
//Apriamo una prima connessione (1) verso il DbA sul Server1

//Eseguiamo un comando Sql a fronte della connessione (1)

// Apriamo una seconda connessione (2) verso il DbB sul Server2

//Eseguiamo un comando Sql a fronte della connessione (2)

/*
 * Se su uno dei 2 db si verifica un problema entrambe 
 * le operazioni verranno annulate, preservando l’integrità
 * del sistema
 */

scope.Complete();
}

Cosa succede, in breve, quando viene eseguito questo stralcio di codice?

Il DbCommand, all’atto dell’esecuzione, si accorge che c’è una transazione attiva, verificando che la proprietà statica Transaction.Current sia diversa da null, e decide di partecipare (Enlistment) alla transazione.

Il processo di enlistment non fa altro che notificare al transaction manager quali siano i partecipanti alla transazione, il transaction manager sarà quindi in grado di controllare lo stato di ogni partecipante e di gestire lo stato di avanzamento delle operazioni.

Il transaction manager è in grado di scalare in maniera autonoma da una transazione locale ad una transazione distribuita (gestita dal DTC) e supporta, per ora solo con Microsoft Sql Server 2005, un nuovo sistema chiamato LTM (Lightweight Transaction Manager).

E’ naturale che lo scalare verso il Distribuited Transaction Coordinator (DTC), nonostante porti l’indubbio vantaggio di poter gestire una transazione distribuita, comporta uno scotto in termini prestazionali.

Vista la semplicità del codice esposto risulta piuttosto naturale chiedersi come sia possibile partecipare ad una transazione tipo quella presentata, cercando però di inserire all’interno dell’ambiente transazionale un proprio oggetto, nello specifico un proprio “resource manager”.

 

I “resource manager”

Nel mondo delle transazioni .NET un “resource manager” è il vero e proprio gestore della transazione: è colui il quale è in grado di tener traccia delle richieste di variazione, di testare se le variazioni richieste possano o meno andare a buon fine, di gestire un eventuale Commit() o un Rollback() in caso di fallimento della transazione.

Quando un “resource manager” partecipa ad una transazione il transaction manager informerà ogni “resource manager” dello stato della transazione e chiederà di eseguire, a seconda del tipo di transazione, determinate operazioni aspettando dal “resource manager” un esito, che può essere positivo o negativo.

Il ponte tra un “resource manager” e il transaction manager è l’interfaccia IEnlistmentNotification, o una di quelle da essa derivata, che è il minimo che deve essere implementato per far sì che una classe possa essere considerata a tutti gli effetti un “resource manager”.

L’interfaccia IEnlistmentNotification è così definita:

public interface IEnlistmentNotification
{
void Commit( Enlistment enlistment );
void InDoubt( Enlistment enlistment );
void Prepare( PreparingEnlistment preparingEnlistment );
void Rollback( Enlistment enlistment );
}

Il metodo Commit() è quello che verrà chiamato dal gestore della transazione per informare il “resource manager” che la transazione deve essere conclusa e i dati registrati in maniera durevole.

Il metodo Rollback() verrà chiamato dal gestore quando è necessario ripristinare lo stato precedente all’inizio della transazione, in questo metodo sarà quindi necessario implementare tutto il codice necessario per garantire che i dati vengano ripristinati in maniera corretta e consistente.

I metodi Prepare() e InDoubt() sono invece relativi alle transazioni a due fasi. Esite infatti una sottile differenza tra una transazione ad una fase e una transazione a due fasi:

  • Transazione a due fasi: il transaction manager, prima di chiamare il Commit() su ogni singolo “resource manager”, scorre tutti i “resource manager” e chiama il metodo Prepare() passando un PreparingEnlistment; il “resource manager” sa quindi che deve ternersi pronto per eseguire il Commit della transazione e informa il transaction manager del suo stato chiamando il metodo Prepared() esposto dal PreparingEnlistment;

  • Transazione a una fase: il doppio passaggio (Prepare() / Commit()) non viene eseguito e viene invocato direttamente il commit della transazione.

Il metodo InDoubt() è anch’esso specifico delle transazioni a due fasi e viene invocato quando il transaction manager perde il contatto con uno o più “resource manager” tra la prima passata (Prepare()) e la seconda (Commit()); all’interno di questo metodo possiamo decidere come comportarci. L’unica cosa che sappiamo è che tutti i resource manager erano pronti, e in uno stato consistente, per eseguire la commit, ma ora uno, o più, non sono più raggiungibili.

 

L’implementazione

Fatta questa breve panoramica sull’interfaccia IEnlistmentNotification, vediamo ora una possibile implementazione di un “resource manager”: TransactionalStringManager.

L’esempio, pur essendo a scopo didattico, sviscera abbastanza bene i concetti base rendendo nel complesso semplice applicare la stessa logica a realtà più complesse.

/*
 * TransactionalStringManager è una semplice classe che dimostra una possibile
 * implementazione di IEnlistmentNotification per rendere transazionale
 * la variazione del valore della proprietà pubblica Value.
 * Ci sono n modi per ottenere questa funzionalità: in questo caso lo 
 * scopo era quello di spiegare come fosse possibile inserire un proprio
 * oggetto custom'interno dell'infrastruttura gestita da TransactionScope
 * o da una CommittableTransaction
 */
public class TransactionalStringManager : IEnlistmentNotification
{
public readonly String ID;
private TransactionCompletedEventHandler onTransactionCompletedHandler;

public TransactionalStringManager( String ID, String value )
{
this.ID = ID;
this._value = value;

this.onTransactionCompletedHandler = new 
TransactionCompletedEventHandler(
this.OnEnlistedTransactionCompleted );
}
}

Cominciamo con il realizzare una semplice classe che implementa l’interfaccia IEnlistmentNotification. Il costruttore prende due parametri: il primo, ID è a puro scopo esemplificativo e serve semplicemente per tenere traccia di quello che succede durante l’esecuzione, il secondo, invece, serve per impostare un valore iniziale.

All’interno del costruttore prepariamo infine un’istanza di un delegato che ci servirà per essere notificati al termine della transazione.

/*
 * filed privato per tenere traccia dell'eventuale 
 * transazione in cui siamo coinvolti
 */
private Transaction _enlistedTransaction;
private Boolean EnlistedInTransaction
{
get { return this._enlistedTransaction != null; }
}

All’interno della nostra classe siamo interessati a tenere traccia dell’eventuale transazione in cui siamo coinvolti: per fare questo teniamo un riferimento ad un’eventuale oggetto Transaction, memorizzato nel campo privato _enlistedTransaction, e realizziamo una semplicissima proprietà, EnlistedInTransaction, al fine di rendere più leggibile il codice, che determina se siamo o meno coinvolti in una transazione

void EnlistInTransaction( String value )
{
if( !this.EnlistedInTransaction )
{
Console.WriteLine( "{0}: EnlistInTransaction", this.ID );

/*
 * Dobbiamo inserirci in una Transazione:
 *-teniamo un riferimento alla transazione in 
 *cui andiamo ad inserirci
 *-chiediamo alla transazione di inserirci all'interno 
 * del suo processo
 */
this._enlistedTransaction = Transaction.Current;
this._enlistedTransaction.EnlistVolatile( 
this, 
EnlistmentOptions.None );

/*
 * Ci interessa tenere traccia di quando la transazione finisce
 * per liberare un po' di risorse
 */
this._enlistedTransaction.TransactionCompleted +=
this.onTransactionCompletedHandler;

/*
 * Teniamo traccia del valore in modo da poter 
 * effettuare un Rollback
 */
this.temporaryValue = value;
}
}

Il metodo EnlistVolatile() infine è un metodo che centralizza la gestione dell’ingresso nella transazione, facendo una serie di operazioni:

  • Verifica che non siamo già coinvolti in una transazione, nel qual caso non fa nulla;

  • Nel momento in cui non siamo già coinvolti in una transazione:

    • Prende un riferimento alla transazione corrente, proprietà static Current della classe Transaction;

    • chiama il metodo EnlistVolatile() passando un riferimento al “resource manager” che gestirà la transazione, in questo caso un riferimento all’istanza della classe stessa, e il modo in cui desideriamo venga gestisto l’inserimento;

    • aggancia un gestore all’evento TransactionCompleted per essere notificato della conclusione della transazione;

    • e in ultimo salva il valore della “stringa” in una variabile temporanea in modo da tener traccia della variazione al fine di eseguire un eventuale Rollback;

Il metodo EnlistVolatile() merita un’approfondimento: se osserviamo i metodi esposti dalla classe Transaction notiamo che ci sono tre diverse possibilità di “enlistment”:

  • EnlistVolatile(): consente di inserie all’interno della transazione un “resource manager” dichiarando che il gestore inserito non supporta il recovery in caso di failure del sistema; questo significa che il nostro gestore non è in grado di persistere le informazioni sul suo stato durante la transazione al fine di riprendere la transazione dal punto in cui era stata interrotta;

  • EnlistDurable(): il gestore che stiamo inserendo ha la capacità di gestire una transazione anche se questa dovesse venire interrotta a seguito di un blocco del sistema; è quindi in grado di tenere traccia, su un supporto persistente, del suo stato e eventualmente di riprendere la transazione dal punto in cui era stata interrotta;

  • EnlistPromotableSinglePhase(): stiamo inserendo un gestore che richiede che la transazione sia gestita a singola fase e supporta il “promoting”: è cioè in grado di gestire la promozione della transazione che può così scalare da una singola transazione ad una distribuita notificando i partecipanti dell’accaduto.

I metodi EnlistDurable() e EnlistVolatile() prendono infine un secondo parametro EnlistmentOptions, che può avere i seguenti valori:

  • None: In questo caso, che è l’impostazione predefinita, non sarà possibile inserire, nella fase di Prepare, nuovi gestori;

  • EnlistDuringPrepareRequired: Se viene specificato EnlistDuringPrepareRequired, in una transazione a due fasi, sarà possibile inserire nella transazione nuovi gestori anche nella fase di Prepare; una casistica potrebbe essere quella di un gestgore che fa da front-end per un altro gestore e decide, solo nella fase di Prepare, di inserire all’interno della transazione il gestore ultimo al fine di garantire l’integrità.

Gli ultimi due elementi degni di nota sono la proprietà pubblica, con cui accediamo alla nostra classe, e l’implementazione dell’interfaccia IEnlistNotification:

/*
 * Il field "_value" ospita il valore della stringa
 * direttamente se non siamo coinvolti in una transazione
 * oppure solo ed esclusivamente al termine della stessa
 * dopo il Commit
 */
private String _value;

/*
 * il field "temporaryValue" serve per ospitare il valore
 * della stringa durante la transazione fino al Commit o
 * ad un eventuale Rollback
 */
private String temporaryValue;

/*
 * la proprietà pubblica a cui accedere per cambiare
 * il valore memorizzato
 */
public String Value
{
get 
{
if( !this.EnlistedInTransaction )
{
if( Transaction.Current == null )
{
/*
 * se NON siamo in una transazione e NON ce n’è
 * alcuna attiva ci limitiamo a ritornare il 
 * valore memorizzato, come farebbe una qualsiasi
 * classe senza il supporto per le transazioni
 */
return this._value;
}
else
{
/*
 * NON siamo in una transazione ma ce n’è una attiva
 * chiediamo di essere coinvolti nella transazione
 */
this.EnlistInTransaction( this._value );
}
}
else if( this._enlistedTransaction != Transaction.Current )
{
/*
 * Siamo già in una transazione che però è diversa 
 * da quella corrente. Non è possibile essere coinvolti
 * in 2 transazioni in contemporanea
 */
throw new InvalidOperationException( 
"Already Enlisted in a Transaction" );
}

/*
 * Se arriviamo qui significa che siamo in una transazione
 * ritorniamo quindi il valore reale al fine di rispettare il
 * 3° principo ACID: Isolated, questo ci garantisce che una 
 * richiesta del valore durante la transazione ritorni ancora
 * il valore vecchio; se non volessimo rispettare il principio
 * Isolated (che non è detto sia proprio un male) basterebbe
 * ritornare il valore di temporaryValue
 */
return this._value;
}
set 
{
if( !this.EnlistedInTransaction )
{
if( Transaction.Current == null )
{
/*
 * se NON siamo in una transazione e NON ce n’è
 * alcuna attiva ci limitiamo a settare il 
 * valore in arrivo, come farebbe una qualsiasi
 * classe senza il supporto per le transazioni
 */
this._value = value;
}
else
{
/*
 * NON siamo in una transazione ma ce n’è una attiva
 * chiediamo di essere coinvolti nella transazione
 */
this.EnlistInTransaction( value );
}
}
else if( this._enlistedTransaction != Transaction.Current )
{
/*
 * Siamo già in una transazione che però è diversa 
 * da quella corrente. Non è possibile essere coinvolti
 * in 2 transazioni in contemporanea
 */
throw new InvalidOperationException( 
"Already Enlisted in a Transaction" );
}
else
{
/*
 * Siamo già nella transazione, impostiamo quindi il valore in
 * arrivo nella nostra variabile temporanea
 */
this.temporaryValue = value;
}
}
}

La proprietà pubblica permette di modificare il valore memorizzato nella nostra classe e, sia in fase di lettura che di scrittura, fa una serie di valutazioni, ampiamente commentate nel codice di esempio, per determinare quale valore ritornare al chiamante o in che modo memorizzare il valore in fase di scrittura. Inoltre è in grado di determinare se siamo coinvolti in una transazione e in caso negativo decide autonomamente di partecipare ad una eventuale transazione attiva.

L’implemetazione dell’interfaccia è decisamente semplice:

void IEnlistmentNotification.Commit( Enlistment enlistment )
{
/*
 * In fase di commit non facciamo altro che memorizzare
 * definitivamente il valore che avevamo nella variabile
 * temporanea
 */
this._value = this.temporaryValue;

/*
 * Informiamo il Transation Manager che abbiamo completato
 * il nostro lavoro
 */
enlistment.Done();
}

In fase di Commit() ci limitiamo a persistere il nostro valore e a confermare la Commit;

void IEnlistmentNotification.InDoubt( Enlistment enlistment )
{
/*
 * InDoubt viene chiamato dal Transaction Manager nel momento
 * in cui, in fase di commit (Single Phase), uno degli attori
 * perde la connessione con lo storage persistente. Non siamo quindi
 * in grado di sapere lo stato della transazione
 * In questo caso di limitiamo ad accettare lo stato, nel nostro esempio
 * non verrà mai chiamato
 */
enlistment.Done();
}

Per il tipo di transazioni a cui participiamo InDoubt() non verrà mai chiamato, ci limitiamo quindi a confermare la nostra posizione;

void IEnlistmentNotification.Prepare( PreparingEnlistment preparingEnlistment )
{
/*
 * In una transazione a 2 fasi il Transaction Manager chiama
 * la Prepare per sapere se tutti i pertecipanti sono pronti
 * ad eseguire il Commit della transazione; la chiamata a 
 * Prepared() indica che siamo pronti ad eseguire la Commit()
 * Se in questa fase volessimo solo essere degli "spettatori"
 * è possibile chiamare Done() al posto di Prepared() e la Commit()
 * non verrà chiamata!
 * Se invece vogliamo chiedere che venga forzato un Rollback in questa
 * fase possiamo chiamare ForceRollback().
 */
preparingEnlistment.Prepared();
}

Anche la fase di Prepare() non comporta alcun lavoro da parte nostra quindi confermiamo che siamo pronti per il Commit;

void IEnlistmentNotification.Rollback( Enlistment enlistment )
{
/*
 * la Rollback viene chiamata in caso di fallimento della
 * transazione; nel nostro caso non facciamo nulla perchè
 * il valore temporaneo è già nella variabile temporanea
 * che al termine della transazione verrà svuotata facendo
 * sì che ad un eventuale accesso post transazione venga 
 * correttamente restituito il valore pre transazione
 */
enlistment.Done();
}

Infine in caso di Rollback() lasciamo che le cose fluiscano senza intervenire perchè durante la transazione ci siamo sempre appoggiati ad una variabile temporanea che fuori dalla transazione non verrà più usata.

void OnEnlistedTransactionCompleted( object sender, TransactionEventArgs e )
{
/*
 * La transazione è stata completata: qui non ci interessa sapere se
 * con successo o meno, ci limitiamo solo a liberare risorse
 */
this._enlistedTransaction.TransactionCompleted -= 
this.onTransactionCompletedHandler;
this._enlistedTransaction = null;
this.temporaryValue = null;
}

A transazione conclusa, sia con una Commit che con una Rollback, viene invocato l’evento TransactionCompleted in cui non facciamo altro che eseguire un po’ di pulizia.

 

Conclusioni

visto come la realizzazione di un “resource manager” che sia in grado di partecipare ad un Transazione gestita non sia poi uno scoglio insormontabile: questo apre lo scenario ad una serie infinita di possibilità che permettono di semplificare notevolmente l’uso del pattern Unit Of Work all’interno delle nostre applicazioni rendendo ancora più flessibile e manutenibile nel tempo il codice che scriviamo.

Risorse:

Per ulteriori esempi di codice: