System.Transactions.IEnlistmentNotification: un approfondimento.


Qualche giorno fa ho avuto modo di (s)parlare di come poter implementare IEnlistmentNotification in una propria classe di business al fine di realizzare un resource manager che sia utilizzabile all’interno di un TransactionScope. Quello che però ho dato per scontato è che si sappia come funziona il giochino. Ho già avuto modo di parlarne ampiamente in un articolo per MSDN ma alcuni dettagli forse mancano ed è giusto riprenderli qui, partiamo da questo snippet:

using( TransactionScope scope = new TransactionScope() )
{
    //Code inside a transaction….
    scope.Complete();
}

la prima nota da fare è che la Dispose di TransactionScope non è una vera e propria Dispose() ma serve principalmente per concludere la transazione con un Rollback(), nel caso si sia verificata una exception e/o la Complete() non sia stata chiamata, o con una Commit() in caso di successo. Approfondendo ulteriormente quello che succede in caso di successo è:

  • viene invocato TransactionScope.Complete();
  • il transaction a manger si segna che la transazione è completata;
  • scorre i “vari” resource manager “enlisted” nella transazione e chiama Prepare/Commit o SinglePhaseCommit a seconda del tipo di enlistment;
  • si conclude definitivamente la transazione;

Se infatti realizziamo un’implementazione banalissima in una applicazione console:

MyResourceManager rm = new MyResourceManager();
using( TransactionScope tx = new TransactionScope() )
{
    Console.WriteLine( "TransactionScope created. ThreadId: {0}", Thread.CurrentThread.ManagedThreadId );
    rm.DoWork();
    tx.Complete();
    Console.WriteLine( "Complete() called inside using block. ThreadId: {0}", Thread.CurrentThread.ManagedThreadId );
}

Console.WriteLine( "Dispose() called outside using block. ThreadId: {0}", Thread.CurrentThread.ManagedThreadId );

dove MyResourceManager implementa IEnlistmentNotification, notiamo che:

image

Prepare e Commit sono successivi alla chiamata a Complete(), quindi un paio di tip:

  1. Non c’è nessuna garanzia che Prepare/Commit vengano chiamati nello stesso thread che sta gestendo la transazione, quindi non fate il benchè minimo affidamento su questo.
  2. Commit() e Prepare() non possono assolutamente fallire pena nessun Rollback(), per essere precisi all’interno di prepare avete l’ultima possibilità di forzare un Rollback(); attraverso preparingEnlistment.ForceRollback() peccatro che non funzioni neanche a pagarlo… indagando un po’ più a fondo, cioè senza usare MSDN… :-(, si scopre che lo scenario deve essere quello di una transazione distributia e il nostro resource manager deve essere un durable resource manager… cioè tutto un’altro pianeta ;-)
    Sta di fatto che quindi la Commit() non deve fallire, il lavoro deve essere fatto all’interno della DoWork() e li devo:
    1. Eseguire l’enlistment nella transazione corrente, se presente;
    2. eseguire le operazioni chje devo eseguire tenendo traccia dei singoli passaggi fatti in modo da poter fare un Rollback() granulare e preciso;
    3. eseguire il tutto eventualmente su variabili/risorse temporanee per rispettare i principi di isolamento;
    4. Nella Commit() confermare le operazioni fatte rendendo persistenti e visibili all’esterno le modifiche;
    5. nell’eventulae Rollback() ripristinare lo stato precedente l’inizio della transazione;
  3. Terza nota, non proprio semplicissima da implementare: il vostro resource manager deve rispettare il livello di Isolamento che c’è impostato sulla transazione e quindi ad esempio acquisire lock sulle risorse o mascherare/consentire la lettura dei valori in maniera diversa a seconda che il chimante sia all’interno della transazione o meno… mi aspetto per cui che in uno scenario come questo:
  4. TransactionOptions op = new TransactionOptions();
    op.IsolationLevel = System.Transactions.IsolationLevel.Serializable;
    using( TransactionScope tx = new TransactionScope( TransactionScopeOption.RequiresNew, op ) )
    {
        IPerson aPerson = Repository.GetPerson();
        aPerson.Name = "Mauro";

        tx.Complete();
    }

    la lettura di aPerson.Name all’interno della transazione ritorni “Mauro” mentre se un thread esterno cercasse di leggere verrebbe accodato in attesa del completamento della transazione o che la risorsa si comportasse in base al livello di isolamento impostato.
    Per raggiungere questo scopo è decisamente più semplice se l’implementazione di IEnlistmentNotification la fate direttamente sulla risorsa in questione (IPerson in questo caso, sul PersonProxy per essere pignoli) piuttosto che su un terzo attore quale potrebbe essere una ipotetica classe PersonResourceManager.

.m

author: Mauro Servienti | posted @ martedì 26 agosto 2008 20.35 | Feedback (0)

Sql Compact 3.5 e le transazioni


al fine di fare un po’ di esperimenti sto usando il “nuovo” (si fa per dire) SqlCe 3.5 SP1 (o come diavolo si chiama… ;-)) ma sono incappato in un fastidioso comportamento che non è semplice da aggirare.

Se cercate di racchiudere una serie di “SqlCeCommand” in un blocco “TransactionScope” vi beccate una sonora Exception che vi informa che “The Connection object cannot be enlisted in the Transaction”… il motivo è molto semplice SqlCe non supporta le transazioni distribuite (nella versione precedente 3.1 non c’era neppure nessun supporto per System.Transactions, quindi non lamentiamoci troppo) quindi se avete più connessioni aperte verso lo stesso db, nonostante la connection string sia dientica, vengono interpretate come distribuite. Quindi questo si schianta:

using( TransactionScope tx = new TransactionScope() )
{
    using( SqlCeConnection cn = new SqlCeConnection( cnStr ) )
    {
        using( SqlCeCommand firstInsert = new SqlCeCommand( cmd, cn ) )
        {
            cn.Open();
            Int32 result = firstInsert.ExecuteNonQuery();
            cn.Close();
        }
    }

    using( SqlCeConnection cn = new SqlCeConnection( cnStr ) )
    {
        using( SqlCeCommand secondInsert = new SqlCeCommand( cmd, cn ) )
        {
            cn.Open();
            Int32 result = secondInsert.ExecuteNonQuery();
            cn.Close();
        }
    }

    tx.Complete();
}

la soluzione è semplice e consta nel condividere la stessa SqlCeConnection, che, bada ben, deve essere aperta prima del primo command e chiusa solo dopo l’ultimo non essendoci connection pooling:

using( TransactionScope tx = new TransactionScope() )
{
    using( SqlCeConnection cn = new SqlCeConnection( cnStr ) )
    {

        cn.Open();

        using( SqlCeCommand firstInsert = new SqlCeCommand( cmd, cn ) )
        {
            Int32 result = firstInsert.ExecuteNonQuery();
        }

        using( SqlCeCommand secondInsert = new SqlCeCommand( cmd, cn ) )
        {
            Int32 result = secondInsert.ExecuteNonQuery();
        }

        cn.Close();

    }

    tx.Complete();
}

Siamo sempre alle solite, se avete un’architettura “figosa” fatta a componenti/servizi realizzare questa cosa nel dal non è cero banale, ma “se po fa” :-D

.m

Technorati Tags: ,

author: Mauro Servienti | posted @ giovedì 14 agosto 2008 7.02 | Feedback (2)

System.Transactions.IEnlistmentNotification: interessante… decisamente!


Dunque, supponiamo di avere il seguente stralcio di codice:

IChangeTrackingServiceContainer container = ChangeTrackingServiceContainer.GetProvider();
IChangeTrackingService tracking = container.CreateTrackingService();

DataContextProcess dataContext = new DataContextProcess();
IEntityCollection<IPerson> allPersons = dataContext.GetAll<IPerson>();

IPerson first = allPersons.Where( p => p.LastName == "Servienti" ).First();
first.FirstName = "Mauro";

if( tracking.IsChanged )
{
    IEnumerable<IAdvisedAction> advisory = tracking.GetAdvisory();
    dataContext.Commit( advisory );
    tracking.AcceptChanges();
}

Nulla di trascendentale… ma come molti di voi sapranno, se avete mai dato un occhio a NSK o vi siete mai cimentati nella realizzazione di una Unit of Work, quello che ci si aspetta è che il lavoro fatto dal metodo Commit() del data context sia come minimo transazionale.

In questo caso c’è però il rischio di incappare in un interessante problema, supponiamo che abbiate anche la seguente situazione:

VersioningEngineProcess<IPerson> vep = new VersioningEngineProcess<IPerson>();
Int32 revision = vep.GetRevision( first );

avete cioè un sistema che vi da una banale informazione di quante volte sia stata “salvata” una determinata entity, nella realtà dei fatti la cosa è molto più ostica ma qui ci basta il concetto, questa informazione è sia sul db e viene mantenuta in fase di Update/Insert ma è anche tracciata all’interno della entity stessa (nel proxy per essere precisi) in modo da evitare noiosi roundtrip sul db che non è detto che servano.

Quel versioning engine quindi si rivolge alla entity (al proxy) e le chiede la revision. Quello in cui rischiate di incappare è questo:

  • Apertura della transazione per il commit;
  • Verifica che la commit sia possibile e non generi conflitti (Concorrenza Ottimistica, Versioning del Dato etc.. etc..);
  • Update fisico sul db;
  • Aggiornamento del valore di Revision;
  • Commit della transazione;

Se tutto va bene nessun problema…ma siccome siamo in un mondo difficile <cit.> non è tutto oro quel che luccica e… potreste avere una condizione un filino più complessa:

IPerson first = allPersons.Where( p => p.LastName == "Servienti" ).First();
first.FirstName = "Mauro";

first.Addresses[ 0 ].City = “Treviglio”;

In questo caso quello che succede è:

  • Apertura della transazione per il commit;
  • Verifica che la commit di IPerson sia possibile e non generi conflitti (Concorrenza Ottimistica, Versioning del Dato etc.. etc..);
  • Update fisico sul db di IPerson;
  • Aggiornamento del valore di Revision di IPerson;
  • Verifica che la commit di IAddress sia possibile e non generi conflitti (Concorrenza Ottimistica, Versioning del Dato etc.. etc..);
  • Update fisico sul db di IAddress;
  • Aggiornamento del valore di Revision di IAddress;
  • Commit della transazione;

Se una delle operazioni fatte su IAddress fallisce otterremmo, giustamente un Rollback della transazione, ma incapperemmo nello spiacevole fatto che il valore di Revision memorizzatro nel proxy di IPerson non verrebbe “rolled back” e, in un’architettura articolata non è affatto operazione banale andare a farne il Rollback correttamente.

La soluzione c’è, è elegante, interessante e decisamente produttiva oltre ad incastrarsi perfettamente in un’architettura articolata: System.Transaction.IEnlistmentNotification, su cui ho scritto anche un articolo l’anno scorso per MSDN.

Una volta capito come funziona il modello transazionale/di enlistment di System.Transactions e in generale di una transazione il giochetto è abbastanza semplice, se poi abbiamo una architettura a servizi lo è ancora di più.

Nell’esempio di prima quello che succede, sulla Commit del DataContext, è più o meno questo:

using( TransactionScope tx = new TransactionScope( TransactionScopeOption.RequiresNew, options ) )
{
    actions.ForEach( action => action.Execute() );
    tx.Complete();
}

abbiamo una lista di azioni che devono essere eseguite, una di queste, quella per l’update di IPerson (idem quella di IAddress), fa una cosa del tipo:

ConflictDetectionEngineProcess<T> cdep = new ConflictDetectionEngineProcess<T>();
cdep.EnsureSafeUpdate( target, options );

PersistenceServiceProcess<T> svc = new PersistenceServiceProcess<T>();
svc.Update( target );

VersioningServiceProcess<T> vsp = new VersioningServiceProcess<T>();
vsp.MarkRevisioned( target );

Siccome siamo in una transazione è lecitissimo aspettarsi un rollback corretto, la prima operazione non scrive nulla fa solo alcuni test ed eventualmente scatena una exception, la seconda scrive fisicamente i dati su Sql Server, la terza aggiorna una “variabile” e qui casca l’asino perchè se dopo questa operazione abbiamo una exception non ne facciamo rollback.

Per fare in modo che il tutto sia implicito, automatico e trasparente è più che sufficiente implementare IEnlistmentNotification sul componente che si occupa di fare l’ultimo step:

sealed class ConcretePersonVersioningService : Service, IVersioningService<IPerson>, IEnlistmentNotification
{
    TransactionEnlistmentHelper transactionHelper = null;
    Boolean markRevisionedCalledInTransaction = false;
    PersonProxy entityCalledInTransaction;

    public void MarkRevisioned( IPerson entity )
    {
        this.CheckIsDisposed();

        /*
         * Passando true come parametro ensureTransaction non ci interessa più
         * verificare che siamo 'enlisted' perchè se non c'è una transazione
         * non arriveremmo mai qui ma otterremmo una eccezione.
         */
        transactionHelper = new TransactionEnlistmentHelper();
        transactionHelper.EnlistInTransaction( true, this, EnlistmentOptions.None );

        /* 
          * A questo punto siamo perfettamente consci del fatto che sappiamo
          * fare l'operazione e che non ci sarebbero exception, del resto è la set
          * di un field. ma siccome siamo in una Transazione ci limitiamo a tener
          * traccia del fatto che dobbiamo farla in fase di commit.
          * 
          * Se qualcosa non va è qui che dobbiamo notificarlo con una Exception
          * per fare in modo che ci sia un Rollback "gracefull".
          */        
        this.markRevisionedCalledInTransaction = true;
        this.entityCalledInTransaction = ( PersonProxy )entity;
    }

    void IEnlistmentNotification.Commit( Enlistment enlistment )
    {
        this.CheckIsDisposed();

        /*
         * Facciamo l'effetiva operazione, in questo modo
         * rispettiamo in pieno il principio ACID
         */
        if( this.markRevisionedCalledInTransaction && this.entityCalledInTransaction != null )
        {
            this.entityCalledInTransaction.Revision++;
            enlistment.Done();
        }
        else
        {
            throw new InvalidOperationException();
        }
    }

    void IEnlistmentNotification.InDoubt( Enlistment enlistment )
    {
        this.CheckIsDisposed();
        enlistment.Done();
    }

    void IEnlistmentNotification.Prepare( PreparingEnlistment preparingEnlistment )
    {
        this.CheckIsDisposed();

        if( this.markRevisionedCalledInTransaction && this.entityCalledInTransaction == null )
        {
            /*
             * Se per assurdo qualcosa fosse andato storto
             * siamo in grado di bloccare l'operazione
             */
            preparingEnlistment.ForceRollback();
        }
        else
        {
            preparingEnlistment.Prepared();
        }
    }

    void IEnlistmentNotification.Rollback( Enlistment enlistment )
    {
        this.CheckIsDisposed();

        /*
         * Rimettiamo a posto le cose come se
         * nulla fosse mai accaduto
         */
        this.markRevisionedCalledInTransaction = false;
        this.entityCalledInTransaction = null;
    }

    #endregion
}

Il tutto è abbastanza semplice: quando viene chiamato il metodo MarkRevisioned() ci rivolgiamo ad un TransactionEnlistmentHelper, che altro non è che un semplice warpper per facilitare la gestione delle transazioni, e chiediamo l’enlistment nella transazione corrente e poi aspettiamo che il tutto avvenga: Prepare() –> Commit() oppure Rollback().

Semplice, efficiace e decisamente bello da veder funzionare ;-)

.m

author: Mauro Servienti | posted @ mercoledì 13 agosto 2008 8.15 | Feedback (3)

[OT] La “Grande Punto” SP1…


Tempo fa avevo accennato che ero diventato un felice possessore di una “Windows Mobile enabled Car”, bene da un paio di mesi ho installato il Service Pack 1… per l’esattezza ho cambiato macchina e adesso ho una nuova fiammante Fiat Grande Punto “vNext”, in soldoni il restyling, che però ha apportato tutta una serie di migliorie alla parte software della macchina.

Non solo è migliorata la parte entertainment gestita da Windows Mobile for Automotive, adesso non ho più mezza magagna con l’iPod e ho finalmente i miei 8k brani a disposizione, ma è decisamente migliorata la parte di gestione del cambio automatico che prima lasciava un po’ a desiderare con qualche magagna decisamente fastidiosa.

Molto bene! e poi è rossa… :-D

.m

author: Mauro Servienti | posted @ mercoledì 13 agosto 2008 6.50 | Feedback (0)

VS2008 SP1 e Net fx 3.5 SP1


Non ci credo che sono il primo ad accorgersene… di solito arrivo sempre giorni e giorni dopo… ma vabbè :-D

Habemus SP1:

Aggiungo:

.m

author: Mauro Servienti | posted @ lunedì 11 agosto 2008 17.56 | Feedback (5)

Una nota curiosa sulla keyword “throw”


Stamattina mentre sistemavo un po’ (un bel po’…) di warning generati dalla Code Analysis ho “scoperto” una cosa che non sapevo, come se fosse una delle poche ;-).

Osserviamo il seguente esempio:

public void MyMethod()
{
    try
    {
        //Do something interesting
    }
    catch( Exception x )
    {
        tracer.Critical( x );
        throw x;
    }
}

In C# è possibile utilizzare sia “throw x;” che solo “throw;” il risultato è sempre lo stesso: l’exception viene rilanciata al chiamente.

C’è però un’importante differenza: se analizziamo lo stack trace dell’esempio scopriamo che l’origine dell’exception è la riga “throw x;” e non l’effettiva locazione dell’errore cosa che invece otterremmo con l’uso del semplice “throw;”.

.m

author: Mauro Servienti | posted @ venerdì 1 agosto 2008 7.36 | Feedback (4)

[OT] Me, myself & I


“…No more turning away
From the weak and the weary
No more turning away
From the coldness inside
Just a world that we all must share
Its not enough just to stand and stare
Is it only a dream that therell be
No more turning away?…”

.m

author: Mauro Servienti | posted @ giovedì 24 luglio 2008 14.36 | Feedback (1)

[OT] Il coinquilino :-D


L’ho sorpreso oggi che gironzolava per il mio giardino…

IMG_1163 IMG_1164 

Mi sta molto simpatico!

.m

author: Mauro Servienti | posted @ giovedì 24 luglio 2008 14.33 | Feedback (1)

VSX Tales…


Avete mai pensato di sfruttare Visual Studio come piattaforma di sviluppo oltre che come fantastico ambiente RAD e non?

A caldo la prima cosa che mi verrebbe da rispondere se qualcuno me lo chiedesse è: “lascia perdere…è un delirio ;-)”. Se però siete come me e vi intestardite sulle cose che non capite fino in fondo allora forse potremmo darci man forte analizzando insieme pro e contro di un task che non è certo semplice: Sviluppo di un VSPackage.

Cercheremo di sviscerare i passi da seguire per arrivare a costruire un proprio Project System per Visual Studio, con:

  • Supporto per i propri project tempate;
  • Supporto per i propri item template;
  • Integrazione con la Toolbox;
  • Integrazione di un proprio custom designer;
  • intergrazione con la property grid;

Un paio di screenshot, giusto per farvi gola :-D:

ProjectSystem ItemTemplate
SolutionExplorerToolbox

Ci si vede la?

.m

author: Mauro Servienti | posted @ venerdì 27 giugno 2008 7.17 | Feedback (5)

[OT] Summit della Bresaola IV


Indovina chi è:

  • Ho installato un TFS 2008 in italiano, con Sql in italano su Windows Server 2008 in italiano...;
  • Non parlatemi di COBOL, dell'ambiente di lavoro poi...
  • Adesso come adesso parteciperei anche allo sviluppo di una applicazione web…;
  • Ho appena mandato a ca*are un cliente via mail…;
  • Piuttosto che scrivere Java vado a fare il magazziniere, detto fatto…;

Ieri sera solita bellissima serata, con mangiata di quelle che si rispettano e si ricordano ;-)

Grazie a tutti!

.m

author: Mauro Servienti | posted @ sabato 21 giugno 2008 13.23 | Feedback (3)