MokaByte Numero 46 - Novembre 2000
 
Le transazioni 
di Jini
di 
Raffaele Spazzoli
Come vengono gestite le transazioni 
nel modello distribuito di Jini

Terza puntata della serie sulle tecnologie di Jini: dopo avere parlato di lease e di eventi distribuiti in questo articolo completiamo la descrizione delle tecnologie su cui si fonda Jini parlando delle transazioni. Nella prossima ed ultima puntata parleremo dei protocolli di discovery e di join necessari alla creazione di un servizio Jini. 

Data la natura dei servizi Jini, vale a dire che terze parti possono definirne sia la semantica sia l’implementazione, non è possibile definire a priori un’implementazione completa delle transazioni cui questi servizi devono partecipare. Per questo Jini definisce un  framework per facilita lo sviluppo della semantica transazionale di un servizio e la gestione di transazioni scritte da terzi.
Per fare ciò le Jini Transaction Specification[1] definiscono alcune interfacce e classi contenute nei package: net.jini.core.transaction e net.jini.core.transaction.server. Inoltre queste specifiche definiscono alcune regole: le transazioni in Jini devono potere essere usate all’interno del two phase commit protocol e sono gestite col modello del Lease (cosa comune a tutte le risorse Jini); in pratica se un Lease scade prima che la transazione sia completata questa si considera abortita.
La Sun ci fornisce l’implementazione di un transaction manager che espleta pienamente il protocollo two phase commit e può essere visto come servizio Jini o servizio RMI. Unica limitazione dell’attuale versione è che non supporta le transazioni innestate (di cui peraltro non parleremo in quest’articolo).
Prima di passare alle transazioni in Jini facciamo un veloce ripasso di cosa sono le transazioni e come funziona il two phase commit protocol.
 
 
 

Le transazioni ACID
Una transazione è un insieme di operazioni che devono o tutte andare a buon fine o tutte fallire. In altre parole se una delle operazioni che compone la transazione non può essere eseguita correttamente, nessuna operazione deve essere eseguita e gli effetti delle operazioni eseguite prima di quella che fallisce devono essere annullati lasciando invariato lo stato del sistema in cui è eseguita la transazione. Inoltre finché la transazione non ha completato l’esecuzione di tutte le operazioni che la compongono nessuno degli effetti delle operazioni già eseguite deve essere visibile all’esterno della transazione stessa.
Affinché una transazione rispetti questa definizione deve possedere quattro proprietà (abbreviate nella sigla ACID [1]):

  1. Atomicità. Tutte le operazioni che compongono una transazione sono eseguite con successo, oppure nessuna deve essere eseguita.
  2. Consistenza. L’insieme delle operazioni eseguite deve lasciare lo stato del sistema in una situazione consistente. 
  3. Isolamento. L’effetto delle operazioni che compongono una transazione non deve essere visibile a oggetti esterni alla transazione fino a che questa non sia completata. Si noti che tali oggetti possono essere a loro volta altre transazioni o semplicemente altri tipi di attori del sistema.
  4. Persistenza (Durability) . Gli effetti della transazione devono essere durevoli e rimanere anche dopo il termine della transazione dunque devono essere memorizzati su un supporto sicuro (tipicamente un disco).
Il rispetto di queste norme comporta spesso l’insorgere di alcuni problemi ormai noti e spesso risolti da tempo.
Primo fra tutti è il problema dell’acquisizione (Locking) delle risorse in maniera esclusiva necessaria per il rispetto delle proprietà Atomicità e Isolamento. Una soluzione efficiente al problema del Locking comporta spesso il nascere del problema del Deadlock cioè la situazione in cui due processi chiedono entrambi di riservare una risorsa che però è già stata acquisita dall’altro rimanendo bloccati un in “abbraccio mortale”.
Questi ed altri problemi sono stati affrontati e risolti più o meno brillantemente da produttori di programmi che operino con le transazioni (tipicamente DBMS), altri problemi però devono essere risolti quando la transazione è distribuita.
 
 
 

Il protocollo two phase commit
Per transazione distribuita si intende una transazione in cui siano coinvolti più processi (partecipanti) che possono essere ospitati anche su host differenti. Si pensi per esempio all’immissione di un ordine in un sistema amministrativo, evento che potrebbe da una parte scatenare la fatturazione dell’ordine, dall’altra avvisare il magazzino di approntare la spedizione.
Quando la comunicazione fra i partecipanti alla transazione non è sicura (come avviene per le comunicazioni via rete) nasce il problema di conoscere e pilotare lo stato di avanzamento della transazione in ogni partecipante al fine di rispettare la proprietà di Atomicità.
Si può dimostrare che in un ambiente “bizantino” (cioè un ambiente in cui i messaggi possono essere andare perduti e intrusi possono spacciarsi per partecipanti alla transazione e spedire messaggi fasulli) non è possibile sviluppare un protocollo di comunicazione fra partecipanti che consenta di rispettare le quattro proprietà delle transazioni. Se però supponiamo di essere in un ambiente in cui sia possibile che si perda un messaggio per errori di comunicazione o dove sia possibile che uno dei partecipanti si blocchi per errori interni o dovuti al sistema in cui è ospitato, allora la letteratura ci viene incontro col two phase commit protocol, un protocollo che, con un buon grado di sicurezza ed efficienza, consente la coordinazione dei partecipanti alla transazione e dunque il rispetto della atomicità della stessa.
Il protocollo two phase commit prevede la presenza di un transaction manager col ruolo di coordinatore dei partecipanti e garante della correttezza nella esecuzione della transazione . Inoltre deve essere presente il client della transazione cioè colui che la scatena.
Il protocollo two phase commit a grandi linee funziona in questo modo:
 

  1. Il client elegge o crea il Transaction Manager (TM). Come ciò avvenga è indipendente dal protocollo.
  2. Il client notifica al TM tutti i partecipanti alla transazione.
  3. Il client esegue tutte le operazioni della transazione sui partecipanti, dopodiché chiede al TM di eseguire il COMMIT (cioè l’azione di portare a completamento con successo la transazione).
  4. Il TM manda il comando di PREPARE (to COMMIT) a tutti i partecipanti. Ricevuto il comando i partecipanti non possono più ricevere operazioni dal client e devono stabilire se le operazioni richieste in precedenza possono essere portate a compimento con successo. In caso affermativo rispondono positivamente al TM e si posizionano nello stato di PREPARED (cioè pronti ad eseguire il COMMIT del loro pezzo di transazione). In caso negativo rispondono negativamente al TM e si posizionano nello stato di ABORTED (cioè transazione fallita).
  5. Il TM raccoglie le risposte. Se sono tutte affermative manda a tutti i client il comando di COMMIT. A questo punto la transazione non può più essere abortita. Se anche una sola risposta è negativa il TM manda a tutti i partecipanti il comando di ABORT provocando l’aborto della transazione.


Se durante lo svolgimento del protocollo dovessero avvenire degli errori di comunicazione o dei crash dei partecipanti alla transazione la convenzione generale è che ogni comunicazione si attende una risposta entro un certo timeout scaduto il quale la transazione si considera abortita (compito del TM mandare ai partecipanti il comando di ABORT). Questa regola ha però un’eccezione quando il TM manda il comando di COMMIT, in questo caso il TM deve assicurarsi che ogni partecipante abbia ricevuto ed eseguito il comando. Un partecipante che, nello stato di PREPARED non abbia ricevuto nessun comando, dopo un certo tempo, deve contattare il TM e chiedergli l’esito della transazione. E’ dunque prudente che lo stato di una transazione gestita da un certo TM non scompaia subito dopo il COMMIT della stessa, ma che il TM attenda per un certo tempo, salvandosi da qualche parte l’esito della transazione.
 
 
 

Le interfacce 
Dal punto di vista del client tutto avviene mediante l’interfaccia Transaction. Essa rappresenta una transazione distribuita. Dato un TM (oggetto che verrà descritto in seguito) si può ottenere una transazione gestita da quest’oggetto chiamando il metodo create() dell’oggetto TransactionFactory e passando il TM come argomento. La transazione viene restituita in un oggetto di tipo TransactionCreated che agisce da contenitore per la transazione e per in lease ad essa associato. Il TM consentirà al client di agire sulla transazione fino allo scadere del lease. 
 
 

Figura 1: Diagramma degli stati degli stati dell’oggetto Transaction. Quando la create(metodo della classe TransactionFactory) ritorna la transazione è nello stato attivo. In questo stato il client esegue le operazioni sui vari partecipanti oppure può abortire nel caso vada qualcosa storto. Quando il client decide di lanciare il commit() la transazione va nello stato di VOTING durante il quale i vari partecipanti si pronunciano sull’esito della transazione. In questo momento scatta il two phase commit protocol che è gestito dal transaction manager. Di tutto questo il client non vede niente tranne il risultato finale che può essere ABORTED o COMMITTED. In entrambi i casi il client può eseguire il cleanup delle risorse eventualmente allocate per la transazione.

Come mostrato in Figura 1 sulla transazione sono possibili solo due operazioni ( in versione bloccante e con timeout ) commit() a abort(). Commit() va invocata dopo che il client ha fatto eseguire tutte le operazioni componenti la transazione sui partecipanti, abort() invece può essere chiamata in qualunque momento. 
Come abbiamo detto al momento della creazione una transazione è associata ad un TM; i TM in Jini sono costituiti da oggetti che implementano l’interfaccia TransationManager. Internamente l’interfaccia TransationManager deve identificare le transazioni che controlla tramite dei long. Questi identificativi di tipo long sono passati come parametri nei vari metodi dell’interfaccia TransactionManager per indicare la transazione su cui si vuole agire.
Anche l’interfaccia TransactionManager espone i due metodi commit() e abort() in versione bloccante e con timeout, essi vengono implicitamente chiamati dall’oggetto Transaction a fronte di una analoga chiamata sull’oggetto Transaction stesso. 
L’interfaccia TransactionManager espone anche due metodi rivolti ai partecipanti la transazione. Il primo, join(), viene chiamato dal partecipante quando questo dichiara al TM di fare parte della transazione. Nella chiamata a questo metodo viene passato anche un parametro di tipo long chiamato craschCount: esso definisce la versione dello stato del partecipante. Ogni volta che il partecipante perde il proprio stato (per esempio a causa di un crash) deve cambiare il valore di crashCount. Se un partecipante tenta di unirsi ad una transazione alla quale si era precedentemente unito con un valore di crashCount differente la richiesta viene negata e la transazione abortita.
 In realtà un modo più semplice per un partecipante di unirsi ad una transazione è quello di chiamare il metodo join() della classe ServerTransaction. Essa è l’implementazione standard dell’interfaccia Transaction proposta dalla Sun. E’ sempre sicuro fare un cast da un oggetto di tipo Transaction ad un oggetto ServerTransaction se l’oggetto di tipo Transaction è stato creato tramite la helpclass TransactionFactory. 
Il secondo metodo dell’interfaccia TransactionManager si chiama getSate() e serve ai partecipanti per conoscere lo stato di avanzamento della transazione. Gli stati di un transazione sono definiti nell’interfaccia TransactionConstants. Tipicamente un partecipante chiederà lo stato di una transazione nella fase di recovery da crash per conoscere l’esito della transazione.
Affinché un oggetto possa partecipare ad una transazione coordinata da un TM essa deve esporre l’interfaccia TransactionParticipant. 
 
 

Figura 2: Diagramma degli stati di un oggetto che implementa l’interfaccia TransactionParticipant. Dopo che il partecipante si è unito alla transazione il suo stato è ACTIVE. Solo in questo stato il client può eseguire operazioni sul partecipante. Quando al TransactionManager viene inviato il comando di commit() questo manda a tutti i partecipanti il comando di prepare(). A questo punto il partecipante entra nella fase di VOTING durante la quale deve decidere se la transazione dal suo punto di vista può andare a buon fine. Se la risposta è affermativa il partecipante si pone nello stato di PREPARED in attesa del commit() definitivo; se la risposta è negativa il partecipante si pone nello stato di ABORTED provocando l’abort dell’intera transazione. Esiste anche una terza possibilità: se lo stato del partecipante non è cambiato allora il partecipante si pone nello stato NOTCHANGED e lo comunica al TransactionManager il quale da quel momento in poi lo ignorerà. Il partecipante per contro può procedere al liberamento immediato di qualunque risorsa impegnata nella transazione. Nello stato di PREPARED il partecipante attende la comunicazione dell’esito della transazione da TransactionManager in caso positivo il partecipante si pone nello stato COMMITED ed esegue il commit del suo stato interno altrimenti entra nello stato ABORTED eseguendo il roolback del suo stato interno. In entrambi i casi il partecipante è autorizzato liberare eventuali risorse allocate per la transazione.

Tale interfaccia definisce i metodi necessari al TM per coordinare la transazione (si veda la Figura 2). Come questo avvenga è nascosto dall’implementazione del TM. 
 
 
 

L’implementazione della Sun
La Sun ci offre l’implementazione di un transaction manager. Mahalo è il nome di questo transaction manager e mahalo.jar è anche il nome del file in cui sono contenute le classi necessarie ad eseguirlo. Mahalo è un servizio attivabile dunque non viene eseguito fino a che non esplicitamente richiesto da un client RMI, inoltre Mahalo può essere registrato in due servizi di nomi: RMI registry e il lookup service di Jini. Infine Mahalo effettua il logging degli eventi dunque a fronte di un crash è in grado di ricostruire lo stato delle transazioni che stava gestendo. Per una più accurata descrizione delle caratteristiche di Mahalo si rimanda alla documentazione (per quanto piuttosto scarsa al momento) fornita dalla Sun.
Per una corretta invocazione da parte del client dei servizi di Mahalo è necessario che questi abbia accesso al file mahalo-dl.jar. Questo file può essere nel classpath del client oppure, meglio, può essere scaricato dalla rete al momento del bisogno dal client. Affinché ciò sia possibile è necessario specificare il parametro codebase quando si lancia Mahalo ed è necessario avviare un servizio che permetta il download di file (tipicamente http o ftp).
Da linea di comando inoltre è possibile specificare quale servizio di nomi deve usare Mahalo per registrarsi e con quale nome si deve registrare.
Se si usa il lookup di Jini, lo stub che viene scaricato al momento del lookup non implementa l’interfaccia TransactionManager come ci si aspetterebbe. L’oggetto ritornato è di tipo com.sun.jini.mahout.binder.RefHolder, invocando su tale oggetto il metodo proxy() si ottiene un riferimento all’interfaccia TransactionManager.
Mahalo rispetta pienamente il protocollo two phase commit, ma non supporta le transazioni innestate.
 
 
 

Un Esempio di Transazione Distribuita
Per costruire una transazione distribuita dovremo innanzi tutto procurarci un TransactionParticipant.
Per fare questo creeremo l’interfaccia TransactedStringHolder modificando l’interfaccia LeasedStringHolder che avevamo costruito nell’articolo sui lease. Dobbiamo aggiungere la nozione di transazione ai metodi dell’interfaccia e inoltre aggiungere un metodo che ci permetta di unire il nostro TransactedStringHolder ad una transazione. L’interfaccia viene così modificata:

public interface TransactedStringHolder extends 
TransactionParticipant, LeasedResource {

String getString(Lease lease,Transaction trx) throws 
        RemoteException,UnknownLeaseException,UnknownTransactionException;

void joinTransaction(Lease lease, Transaction trx) throws 
RemoteException,UnknownLeaseException;

void setString(Lease lease,Transaction trx) throws 
RemoteException,UnknownLeaseException,UnknownTransactionException;

}
 
 

Come possiamo vedere è stato aggiunto un argomento di tipo transaction ai due metodi setString() e getString(). Inoltre l’interfaccia StringHolder ora estende l’interfaccia TransactionParticipant. Il metodo joinTransaction() serve ad unire lo StringHolder alla transazione passata come parametro. Si noti che poiché StringHolder rimane una LeasedResource ogni suo metodo prevede il passaggio di un lease come parametro.
Nel package foele.jini.transaction.example c’è un’implementazione della interfaccia TransactedStringHolder (TransactedStringHolderImpl). In tale implementazione viene gestita una transazione per volta (coerentemente col fatto che lo StringHolder contiene una sola stringa che non può essere cambiata da più di un processo alla volta). Nel Package ho fornito anche una classe client tramite la quale si può osservare il comportamento del protocollo two phase commit: istanziando due TransactedStringHolderImpl (e il TM ovviamente), il programma Client crea due transazioni, nella prima esegue due setString() sui due TransactedStringHolder, nella seconda legge le stringhe precedentemente memorizzate mediante il metodo getString().
 
 
 

Conclusioni
Il framework introdotto dalla Sun per gestire le transazioni distribuite in Jini funziona correttamente e l’implementazione proposta del transaction manager (Mahalo) usa il protocollo Two Phase Commit tale quale a quello che si studia in ambito accademico. Mi rimane solo un dubbio: perché i progettisti di Jini non hanno esteso le JTA (Java Transaction API), un insieme di API proposto per l’implementazione di transazioni distribuite quando hanno scelto come definire le transazioni in Jini? Probabilmente la risposta è nel fatto che le JTA non contemplano il concetto di lease. Peccato che non si siamo potute riutilizzare le interfacce.
 
 
 

Bibliografia
[1] "Jini Tecnhology Core Platform Specification", Sun Microsystems, 2000
[2] "Jini API Documentation ", Sun Microsystems, 2000
[3] " A Collection of Jini™ Technology Helper Utilities and Services Specifications", Sun Microsystems, 2000
I documenti sono scaricabili al sito http://www.sun.com/products/jini.


Raffaele Spazzoli è laureato in ingegneria informatica. Da anni coltiva la propria passione per Java studiando e testando le soluzioni tecnologiche introdotte dalla Sun. Può essere contattato tramite e-mail all’indirizzo RaffaeleSpazzoli@mailandnews.com.

 

Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it