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]):
-
Atomicità.
Tutte le operazioni che compongono una transazione sono eseguite con successo,
oppure nessuna deve essere eseguita.
-
Consistenza.
L’insieme delle operazioni eseguite deve lasciare lo stato del sistema
in una situazione consistente.
-
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.
-
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:
-
Il client
elegge o crea il Transaction Manager (TM). Come ciò avvenga è
indipendente dal protocollo.
-
Il client
notifica al TM tutti i partecipanti alla transazione.
-
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).
-
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).
-
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.
|