in

DotNetSide

Dot Net South Italy Developers User Group

Articoli

Articoli pubblicati dagli iscritti a .netSide

SqlPersistenceService, Workflow, Ownership, Lock

 

Autore: Michele Locuratolo (Mighell)

Uno dei (tanti) pregi di Windows Workflow Foundation sono i servizi pluggabili al Runtime che permettono di "aggiungere" funzionalità molto comode e utili.
Uno di questi servizi è quello di persitenza (WorkflowPersistenceService) che troviamo implementato nella SqlWorkflowPersistenceService (che, come facile da immaginare, persiste l'istanza del WF su un DataBase Sql Server 2005).
Un primo articolo sul servizio di persistenza è disponibile qui. In questo secondo articolo, ci concentreremo su un altri importanti aspetti come il locking e la ownership dell'istanza.

Il concetto di Ownership

Per comprendere il concetto di ownership, partiamo da una considerazione legata al servizio di persistenza. Perchè serializziamo un Workflow su uno storage fisico? I motivi sono sostanzialmente 2:

  1. evitiamo che un long running workflow occupi inutilmente memoria. Persistendolo, oltre a liberare memoria, lo preserviamo da crash di macchina.
  2. per passarlo ad un'altra applicazione

(sul secondo punto si potrebbe aprire un dibattito sul fatto che lo si potrebbe passare in modo diverso senza passare da SQL Server ma, ai fini di questo articolo, poco cambia).

Non è raro che un workflow sia avviato da una applicazione e concluso da un'altra. Di esempi ne potremmo fare molti ma basti pensare ad un sistema di trouble ticket (il ticket viene aperto dall'applicazione del call center e chiuso dall'applicazione di un tecnico), o alla gestione di un carrello elettronico per un sito di e-commerce (il carrello viene aperto da una applicazione web e chiuso dal front end aziendale).
In questi casi, ci potremmo trovare di fronte a problemi di ownership (o proprietà) dell'istanza di Workflow. Il lock di una istanza infatti indica, ad un secondo runtime che cerca di deserializzarla ed usarla, che essa "appartiene" ad un altro runtime e che quindi non può "ancora" essere utilizzata. 
Il workflow runtime esegue un poll (purtroppo) del db a intervalli di tempo configurabili nel costruttore dell’instanza (o nel .config) prelevando i workflow persistiti non in lock; se si cerca invece di caricare, tramite il metodo GetWorkflow del workflow runtime o il metodo load di una istanza di workflow, un workflow in lock otteniamo l’eccezione WorfklowOwnershipException. La stessa eccezione si ottiene anche quando un runtime cerca di persistere un workflow che non gli appartiene più, come vedremo più avanti nell’esempio di codice. È molto importante quindi indicare valori corretti per i vari parametri che andiamo a indicare.   

La documentazione ufficiale, a tal proposito, è abbastanza chiara e recita (qui):

"The SqlWorkflowPersistenceService supports locking of workflow instances. This feature is used when several workflow runtimes share the same database. A column in the SQL database table is used to mark a workflow instance as locked whenever it is being used by a workflow runtime. The SqlWorkflowPersistenceService does not load a workflow instance that is marked as "in use" by another runtime. Typically these locks are released when the workflow instance is persisted, for example, on idle, completion, or termination. The locks can also be released automatically after a period of inactivity. This period of inactivity can be set using the constructor of the SqlWorkflowPersistenceService class. It can also be set through the configuration file"

Oltre a quanto già citato poco fa, da qui si evince anche che il locking può, in un certo senso, essere impostato nel costruttore del servizio di persistenza.
Vediamo ora un esempio pratico per meglio comprendere quanto appena detto.

Un esempio pratico

Vediamo un semplice esempio pratico utile a simulare il problema del locking.
Partiamo da un semplice workflow come quello in figura 1.  
Il workflow è un semplice sequenziale composto da poche e semplici activity utili al solo scopo dimostrativo. Il Workflow viene utilizzato da una applicazione console che mostra a video alcuni messaggi generati sia dal workflow stesso che dagli eventi da esso sollevati.
L'applicazione console fa uso del servizio di persistenza di cui, uno dei costruttori è il seguente:

public SqlWorkflowPersistenceService ( 
string connectionString,
bool unloadOnIdle,
TimeSpan instanceOwnershipDuration,
TimeSpan loadingInterval
)

Il primo parametro indica la connectionString al Data Base su cui vogliamo serializzare il WF. Il secondo parametro indica se vogliamo serializzare il Workflow quando è libero.
Il terzo parametro indica per quanto tempo il runtime è propietario di una determinta istanza del Workflow.
Il quarto ed ultimo parametro invece fa una cosa molto utile in alcuni contesti: indica l'intervallo di tempo durante il quale il servizio di persistenza deve fare il pull sul Data Base per deserializzare l'istanza (lo vediamo tra un po').
Ai fini del nostro esempio, per evitare di aggiungere troppa complessità, faremo in modo di far perdere subito l’ownership al runtime per verificare l’eccezione di cui sopra. Seguendo la definizione della documentazione ufficiale, il parametro instanceOwnershipDuration determina l'intervallo di tempo che a noi interessa quindi, impostiamolo ad un valore molto basso in questo modo:

SqlWorkflowPersistenceService persistence = new SqlWorkflowPersistenceService("server=(local);database=wfPersistence;Integrated Security=true;", true, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds( 3500 ) );

Dopo aver impostato correttamente il Data Base e predisposto i nostri messaggi di log sulla console (omessi in questo articolo per questioni di spazio, si faccia riferimento al file allegato), avviando il programma di test otterremo il seguente output:

[14/02/2007 19.19.56]; 7cb35b0c-3498-4bcb-8dc6-949de600099e <-- è il Guid dell'istanza del servizio di persistenza
[14/02/2007 19.19.56]; Started #1 <-- il runtime è stato avviato
[14/02/2007 19.19.56]; Prima Code_Activity <-- la prima code activity è stata eseguita
[14/02/2007 19.19.56]; Delay Activity (00:00:03) <-- siamo entrati nella delay activity ed il workflow viene persistito
[14/02/2007 19.19.56]; Persisted #1 <-- viene sollevato l'evento di workflow persisted

******* WF *********
This workflow is not owned by the WorkflowRuntime. The WorkflowRuntime's ownersh
ip has expired, or another WorkflowRuntime owns the workflow.
********************

[14/02/2007 19.20.00]; Aborted #1 <-- il workflow viene abortito in quanto l'istanza è in lock!

Come era facile aspettarsi, avendo impostato il tempo di ownership a 1 ms, il servizio di persistenza, trascorso tale tempo, NON PUO’ salvare l’instanza del workflow in quanto non gli appartiene più. A riprova di quanto appena detto, se eseguiamo una query sulla tabella InstanceState sul DB di persistenza troveremo:

Dall'immagine si nota che l'istanza è in lock ed l'id del proprietario è uguale al Guid del servizio di persistenza visto nell'output della console.
In questo esempio, sebbene il nostro runtime risulti proprietario dell'istanza, a livello temporale l'ownership è scaduta, quindi ci troviamo con l'eccezione di ownership.

Se riprendiamo i costruttori di SqlWorkflowPersistenceService, noteremo che il costruttore di default non ci chiede le tempistiche di ownership ma solo la stringa di connessione. In questo caso, come viene gestita l'ownership? Se ci armiamo di Reflector ed analizziamo il costruttore, noteremo che essi vengono impostati a dei valori di default:

public SqlWorkflowPersistenceService(string connectionString){
this._serviceInstanceId = Guid.Empty;
this.loadingInterval = new TimeSpan(0, 2, 0);
this.maxLoadingInterval = new TimeSpan(0x16d, 0, 0, 0, 0);
this.timerLock = new object();
this.infinite = new TimeSpan((long) (-1));
if (string.IsNullOrEmpty(connectionString)){
throw new ArgumentNullException("connectionString", ExecutionStringManager.MissingConnectionString);
}
this.unvalidatedConnectionString = connectionString;
}

In linea di massima, salvo in casi particolari, conviene impostare l'instanceOwnershipDuration al massimo (TimeSpan.MaxValue) in quanto, come si legge dalla documentazione, il lock viene comunque rilasciato quando il workflow viene scaricato dalla memoria (manualmente tramite il metodo UnLoad o nel caso di unloadOnIdle impostato a true).
Qualora ci trovassimo nelle condizioni di avere un workflow in lock e la necessità di sbloccarlo, abbiamo a disposizione il metodo UnlockWorkflowInstanceState che, opportunamente implementato, svolge questo compito.

Il loadingInterval

Chiarito il concetto di Ownership, vediamo a che serve il parametro loadingInterval. In breve, questo parametro indica l'intervallo di tempo durante il quale il servizio di persistenza deve fare il pull sul Data Base per deserializzare l'istanza. Ammetto che all'inizio ho avuto qualche perplessità a riguardo ma poi, ragionando sul servizio di persistenza, le idee si sono schiarite.
All'inizio abbiamo detto che persistiamo un workflow per liberare la memoria o per passarlo ad un altra applicazione. 
In entrambe i casi, è logico aspettarsi che il momento in cui eseguiamo la serializzazione, sia quando il Workflow è libero. Se ci troviamo nel primo caso però, una volta che il Workflow si è serializzato, come facciamo a deserializzarlo e a proseguirne l'esecuzione?
Nell'esempio, abbiamo usato una delay activity impostata a 3 secondi durante la quale il workflow passa nello stato di idle e può essere serializzato. Il loadingInterval fa in modo che il runtime, ogni x secondi, vada sul Data Base a controllare il campo NextTimer nella tabella InstanceState. Esso indica l'istante in cui l'istanza può, in un certo senso, essere effettivamente deserializzata.
Nel nostro esempio, avendo impostato la Delay activity a 3 secondi, il NextTimer sarà uguale all'istante in cui il workflow è stato serializzato + 3 sec. 
Se il nostro loadingInterval è impostato, ad esempio, ad 1 secondo, l'istanza verrà deserializzata dopo 3 cicli. Impostando il loadinInterval ad un tempo superiore, come è logico aspettarsi, dovremmo attendere che venga effettuato il pull su Data Base prima che l'istanza venga deserializzata e l'esecuzione continui.

Possiamo sperimentare questo comportamento impostando il servizio di persistenza come segue:

SqlWorkflowPersistenceService persistence = new SqlWorkflowPersistenceService("server=(local);database=wfPersistence;Integrated Security=true;", true, TimeSpan.MaxValue, TimeSpan.FromMilliseconds( 5000 ) );

e mandando in esecuzione l'applicazione, ottenendo il seguente output:

[15/02/2007 10.57.38]; 7f76f081-ae81-4462-ad3b-fb35236bdfa6 <-- è il Guid dell'istanza del servizio di persistenza
[15/02/2007 10.57.38]; Started #1 <-- il runtime è partito
[15/02/2007 10.57.38]; Prima Code_Activity <-- viene eseguita la prima code activity
[15/02/2007 10.57.38]; Delay Activity (00:00:03) <-- siamo entrati nella delay activity ed il workflow viene persistito
[15/02/2007 10.57.38]; Persisted #1 <-- il workflow viene persistito
[15/02/2007 10.57.43]; Seconda Code_Activity <-- viene eseguita la seconda code activity
[15/02/2007 10.57.43]; Persisted #1 <-- l'istanza viene eliminata dal DataBase
[15/02/2007 10.57.43]; Completed #1 <-- il workflow è completato
Press any key to continue . . .

Come si può notare dal log, negli orari evidenziati in rosso, sebbene nel DataBase il campo NextTimer permette la deserializzazione alle 10:57:41 (3 secondi della delay activity), avendo impostato il loadingInterval a 5 secondi, l'istanza viene effettivamente deserializzata alle 10:57:43 (ovvero dopo i 5 secondi).

Conclusioni

Il servizio di persistenza di Windows Workflow Foundation è uno strumento molto comodo quando lavoriamo con i workflow. La semplicità con cui è possibile usarlo (basta una riga di codice per attivarlo), nasconde però una elevata complessità implementativa. Conoscerne gli aspetti interni ci permette di sfruttarne al massimo le potenzialità dandoci la possibilità di realizzare applicazioni migliori.

Il sorgente degli esempi può essere scaricato da qui.

Ringraziamenti

Un ringraziamento all'amico William Franchini per avermi chiarito un paio di concetti sui temi trattati in questo articolo.
Un ringraziamento a MSDN Italia per la segnalazione.
Un ringraziamento a Roberto Brunetti per la review.

Only published comments... Feb 15 2007, 01:17 PM by DotNetSide Staff
Filed under:

Comments

 

Mighell's blog said:

Uno dei tanti pregi di Windows Workflow Foundation sono i servizi pluggabili al Runtime che permettono

February 15, 2007 4:36 PM
 

Mighell's blog said:

Il mio ultimo articolo relativo al lock e ownership del Workflow &#232; stato segnalato oggi nel Developer

February 21, 2007 4:24 PM
 

Mighell's blog said:

Qualche giorno fa, ho pubblicato un articolo sull'ownership ed il lock delle istanze diWindows Workflow

March 2, 2007 11:39 AM
 

Semplice said:

Link persistenza workflow

October 29, 2007 10:08 PM
Powered by Community Server (Commercial Edition), by Telligent Systems