Transakcje Atomowe¶
Celem zajęć jest przestawienie mechanizmu transakcji atomowych na przykładzie ich implementacji w bibliotece Atomic RMI.
Java RMI (przypomnienie)¶
Java Remote Method Invocation (Java RMI) – interfejs programistyczny pozwalający wykonywać metody z innych maszyn wirtualnych. Działanie jest podobne do Remote Procedure Calls (RPC) ale nastawione na zgodność z paradygmatem programowania obiektowego, więc mówimy o wywoływaniu metod obiektów zdalnych a nie zdalnych procedur. Java RMI pozwala na relatywnie łatwe budowanie aplikacji rozproszonych.
Części składowe¶
System Java RMI składa się z następujących części.
Rejestr¶
Globalny rejestr wszystkich dostępnych obiektów zdalnych zlokalizowany na jednej z maszyn wirtualnych. Wymagane jest istnienie przynajmniej jednego rejestru, ale może być ich więcej.
Rejestr jest usługą sieciową z którą komunikować się można za pomocą interfejsu zawartego w klasie java.rmi.registry.Registry
. Usługę można odnaleźć znając jej adres sieciowy i port używając metody statycznej getRegistry
lub createRegistry
z klasy java.rmi.registry.LocateRegistry
.
Funkcjonalnie jest to mapa <String, Object>
gdzie kluczami są unikalne identyfikatory obiektów globalnych (jakiś napis) a wartościami są referencje do stub-ów tych obiektów.
Obiekty zdalne¶
Obiekty zdalne są to obiekty które zostały utworzone, wykonują obliczenia i przechowują dane na zdalnej maszynie wirtualnej. Każdy obiekt wirtualny jest rejestrowany w globalnym rejestrze za pomocą identyfikatora (unikalnego w skali rejestru) przez co staje się dostępny dla programów działających na innych maszynach wirtualnych.
Dla klienta obiekty zdalne są dostępne przez interfejsy (klient posiada interfejs obiektu zdalnego ale nie musi posiadać definicji klasy). Klasy są ładowane lokalnie na podstawie interfejsów przez RMI w sposób dynamiczny kiedy zachodzi taka potrzeba. Transmitowane są także dane obiektu (więc muszą być Serializable
).
Interfejsy zdalne muszą rozszerzać interfejs java.rmi.Remote
. Każda zadeklarowana metoda w interfejsie musi deklarować też wyjątek java.rmi.RemoteException
– jest rzucany przy wystapieniu problemów z RMI lub siecią np. przy zerwaniu połączenia z obiektem zdalnym.
Serwer¶
Program (zazwyczaj daemon) działający na zdalnej maszynie wirtualniej który tworzy i rejestruje w globalnym rejestrze (lub globalnych rejestrach) obiekty zdalne.
Klient¶
Program działający na maszynie lokalnej znajdujący obiekty zdalne za pomocą rejestru i wywołujący ich metody.
Konfiguracja¶
Java RMI wymaga ustalenia polityki bezpieczeństwa dla klienta i serwera. W praktycznym wykorzystaniu trzeba
Na potrzeby laboratorium wystarczy stworzyć pliki polityki bezpieczeństwa .policy
dla każdej z aplikacji z prawami dostępu bez ograniczeń w treści:
grant {
permission java.security.AllPermission;
};
Katalogi w których znajdują się pliki polityki muszą być ujęte w CLASSPATH
.
- Dodatkowo podczas uruchamiania należy ustawić następujące własności systemowe:
java.rmi.server.hostname
– nazwa lub adres serwera (niezbędne w serwerze i kliencie)java.rmi.server.codebase
– adres (ścieżka) do bytecode-u zdalnych obiektów (niezbędne w serwerze)java.security.policy
– plik z okresloną polityką bezpieczeństwa (niezbędne w serwerze i kliencie)
Przykład:
java -Djava.rmi.server.hostname=lab-143-1 \
-Djava.rmi.server.codebase=file:///home/student/Example/bin \
-Djava.security.policy=server.policy \
example.Server
Atomic RMI¶
Atomic RMI dodaje do Java RMI rozproszony mechanizm zarządzania współbieżnością za pomocą transakcji atomowych. Programista wyznacza w kodzie zakres transakcji i obiekty zdalne używane w transakcji wraz z supremum liczby wywołań ich metod. Atomic RMI używa wrapper-ów obiektów zdalnych do kontroli wykonania ich metod w celu zagwarantowania poprawnosci wywołania (izolacja).
Algorytm wersjonowania¶
- Atomic RMI używa Supremum Versioning Algorithm (SVA). SVA korzysta z 3 typów liczników do podjęcia decyzji czy metoda ma być dopuszczona do wykonania:
gv
– globalny licznik wersji (dla każdego obiektu) – suma zadeklarowanych użyć danego obiektu przez wszystkie transakcje,lv
– lokalny licznik wersji (dla każdego obiektu) – suma wykonanych użyć danego obiektu przez wszystkie transakcje,pv
– prywatny licznik wersji (dla każdego obiektu proxy odpowiadającemu parze transakcja-obiekt) – kopiagv
z chwili rozpoczęcia transakcji.
SVA wymaga podania maksymalnej liczby wywołań metod każdego z obiektów podczas działania transakcji. Informacja ta jest zapisywana w obiekcie proxy (sup
).
Początkowo wszystkie liczniki są wyzerowane.
for o in all_objects: o.gv = 0 o.lv = 0
W chwili rozpoczęcia działania transakcja
k
dodaje liczbęsup
do licznika globalnegogv
każdego obiektu jakiego chce używać. Następniegv
jest kopiowane do licznika prywatnegopv
. Inicjalizacja# o - remote object, # p - proxy of o for transaction k lock() for (o, p) in used_remote_objects: o.gv = o.gv + p.sup p.pv = o.gv unlock()
Jeśli przed chwilą kiedy wywoływana jest metoda
m
obiektuo
lv
jest mniejsze odpv
oznacza to, że transakcja została wyprzedzona lub pominięta, co jest błędem. Jeśli błąd nie wystąpił wywołanie jest opóźnione przez obiekt proxyp
do momentu kiedy liczniklv
wyrówna się ze stanem licznikapv
bezsup
. Odpowiada to stanowi licznika globalnego w chwili rozpoczęcia działania metody. Po wykonaniu metody liczniklv
odpowiedniego obiektu jest podwyższany o 1.assert(lv > p.pv) wait(p.pv - p.sup <= o.lv) ret = o.m(...) # the execution of the original method o.lv = o.lv + 1 return ret
Warto zauważyć, że może to pozwolić innej transakcji na wywołanie metody obiektu
o
jeśli obecna transakcja nie deklaruje, że będzie z niego dalej korzystać, czyli zwolnić obiekt przed zakończeniem się obecnej transakcji.W momencie zatwierdzania transakcji (
commit
) transakcja zwalnia wszystkie obiekty poprzez doprowadzenie dla każdego z nich licznikalv
do wartościpv
.for (o, p) in used_remote_objects: if o.lv >= p.pv - p.sup: if o.lv < p.pv: o.lv = p.pv else: fork() wait(o.lv < p.pv) o.lv = p.pv join()
W opisie powyżej dla uproszczenia pominięto kwestie odtwarzania stanu (związane z operacjami rollback
i retry
).
Przykład¶
Interfejs Programistyczny¶
- Mechaniz transakcji atomowych jest zaimplementowany jako biblioteka Atomic RMI:
Definicja zdalnych obiektów¶
Zdalne obiekty w Atomic RMI muszą mieć zdefiniowany interfejs tak samo jak zdalne obiekty Java RMI.
public interface Foo extends Remote {
void foo() throws RemoteException;
}
Klasy zdalnych obiektów Atomic RMI muszą implementować interfejsy zdalne oraz dodatkowo rozszerzać klasę soa.atomicrmi.TransactionalUnicastRemoteObject
w której znajdują się liczniki związane ze zdalnymi obiektami.
public class FooImpl extends TransactionalUnicastRemoteObject implements Foo {
// ...
}
Serwer umieszcza instancje obiektów zdalnych w rejestrze RMI.
Registry registry = LocateRegistry.createRegistry(1099)
registry.bind("foo", new FooImpl());
registry.bind("bar", new BarImpl());
Definicja transakcji¶
Przed utworzeniem transakcji (w kodzie klienta) należy uzyskać dostęp do rejestru RMI i pobrać za jego pomocą obiekty zdalne.
Registry registry = LocateRegistry.getRegistry("foobar-server", 1099);
Foo foo = (Foo) registry.lookup("foo");
Bar bar = (Bar) registry.lookup("bar");
Następnie tworzona jest transakcja i określone jest które obiekty zdalne będą używane i maksymalnie ile razy metody każdego z nich będą wywołane.
Transaction transaction = new Transaction();
foo = transaction.accesses(foo, 1);
bar = transaction.accesses(bar, 2);
Podczas wykonywania metody accesses
utworzony zostaje nowy obiekt który opakowuje obiekt zdalny dodając informację o lokalnych licznikach wywołań. Od tej pory nalezy posługiwać się wyłącznie opakowaną instancją obiektu zdalnego! Dlatego wpowyżej referencja do nieopakowanego obiektu jest gubiona.
W końcu należy zaimplementować kod transakcji. Transakcja musi rozpoczynać się wywołaniem metody begin
. Transakcja może zakończyć się wywołaniem metody commit
co oznacza prawidłowe wykonanie, lub rollback
co doprowadzi system do stanu sprzed wywoływania transakcji i porzucenie dalszego wykonywania.
transaction.begin(); // Rozpoczęcie.
foo.foo();
if(bar.bar()) {
bar.foobar();
transaction.commit(); // Zatwierdzenie wykonania.
} else {
transaction.rollback(); // Wycofanie wykonania.
}
Ręczne zwalnianie obiektu¶
Transakcja może nie dostać informacji o supremum, lub supremum może być zbyt wysokie:
Transaction transaction = new Transaction();
foo = transaction.accesses(foo);
bar = transaction.accesses(bar, 10);
Programista może ręcznie zwolnić obiekty:
transaction.begin();
while (random > 0) {
foo.foo();
bar.bar();
}
transaction.release(foo);
transaction.release(bar);
transaction.commit();
Użycie obiektu po zwolnieniu powoduje wyjątek (i anuluje gwarancje).
Niezależnie od tego, czy ręczne zwalnianie jest używane, transakcje będą próbować zwalniać obiekty jak najwcześniej na podstawie supremów.
Możliwe jest także zakończenie transakcji poprzez metodę retry
. W takim
wypadku transakcja zostanie wycofana i uruchomiona ponownie. Aby użyć metody
retry
należy jednak zdefiniować metodę jako klasę implementującą interfejs
soa.atomicrmi.Transactable
. Obiekt tej klasy jest przekazywany jako
argument metody start
. Poniżej używamy klasy anonimowej.
transaction.start(new Transactable() {
// ...
});
Interfejs Transactable
zawiera metodę atomic
wewnątrz której implementuje się kod transakcji.
public void atomic(Transaction transaction) throws RemoteException {
foo.foo();
if(bar.bar()) {
bar.foobar();
transaction.commit(); // Prawidłowe wykonanie.
} else {
transaction.retry(); // Wycofanie i ponowne wykonanie.
}
}
Przy użyciu klasy anonimowej obiekty foo
i bar
które są w niej używane, ale są zadeklarowane poza nią być oznaczone jako final
.
Bank Example – kompletny przykład¶
Przykład do ściągnięcia tutaj
(project Eclipse).
- Przykład zawiera serwer bankowy
Server
udostępniający dwa kontaA
iB
jako obiekty zdalne typuAccount
i trzy rodzaje klientów: Audit
sprawdza i wypisuje stan każdego z kont,Transfer
dokonuje przelewu z kontaA
na kontoB
z możliwością zatwierdzenia lub odrzucenia transakcji przez klienta na końcu,TransferRetry
dokonuje takiego samego przelewu, ale dodaje możliwość odwołania i powtórzenia operacji na końcu (przez co transakcja jest zaimplementowana jako klasa typuTransactable
).
- Poszczególne części systemu uruchamiane są w następujacy sposób:
Server
java -Djava.rmi.server.hostname=localhost -Djava.rmi.server.codebase=file://`pwd`/bin/ \ -Djava.security.policy=server.policy soa.atomicrmi.test.bank.Server
Audit
java -Djava.security.policy=client.policy soa.atomicrmi.test.bank.Audit localhost 1099
Transfer
java -Djava.security.policy=client.policy soa.atomicrmi.test.bank.Transfer localhost 1099
TransferRetry
java -Djava.security.policy=client.policy soa.atomicrmi.test.bank.TransferRetry localhost 1099
Więcej informacji w dokumentacji Atomic RMI 2.1.
Zadanie 1¶
Zaprojektuj i zaimplementuj prostą rozproszoną bazę danych typu \(K_1 \times K_2 \mapsto V\). Domena kluczy: \(K_1 \in { 0 \le i \le n }, K_2 \in { 0 \le i \le m }\). Struktura jest rozproszona na \(n\) serwerach i każdy serwer hostuje m komórek, z których każda przechowuje jakąś wartość. Dla uproszczenia niech \(V \in \mathbb{Z}\).
Przetestuj działanie struktury używając następujących typów klientów:
- Odczyt dowolny: klient atomowo odczytuje zakres \(k_r\) kluczów gdzie klucz to \((i, j)\) i gdzie \(i \subseteq K_1\) i \(j \subseteq K_2\).
- Aktualizacja: klient atomowo inkrementuje wartości \(k_w\) komórek z zakresu kluczów \((i, j)\), gdzie \(i \subseteq K_1\) i \(j \subseteq K_2\).
Zadanie 2¶
Zaprojektuj i zaimplementuj system symulujący działanie poniższego hotelu.
Hotel S***** posiada \(n\) pokojów i \(m\) sal konferencyjnych które udostępnia swoim klientom. Pokoje i sale konferencyjne mogą być wolne lub zajęte.
- Hotel gości trzy typy klientów:
- Turysta – rezerwuje \(1\) pokój i po upłynięciu jakiegoś czasu go zwalnia, a jeśli nie ma dostępnych pokojów rezygnuje z usług hotelu,
- Organizator spotkania – rezerwuje \(1\) salę konferencyjną i po upłynięciu jakiegoś czasu ją zwalnia, a jesli nie ma wolnych sal rewzygnuje z usług hotelu,
- Organizator konferencji – rezerwuje \(k\) sal konferencyjnych i \(l>k\) pokojów i po upłynięciu jakiegoś czasu wszystko zwalnia, a jeśli nie ma dostatecznej liczby wolnych pokojów odczekuje jakiś czas i próbuje jeszcze raz.
Przetestuj system na jednym komputerze, a następnie w środowisku rozproszonym.