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

../_images/rmi-arch.svg.png

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.

Przykłady

Dokładny tutorial i przykład prostego systemu RMI można znaleźć na stronie Oracle.

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

../_images/atomic-rmi-arch.svg.png

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) – kopia gv 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 globalnego gv każdego obiektu jakiego chce używać. Następnie gv jest kopiowane do licznika prywatnego pv. 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 obiektu o lv jest mniejsze od pv 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 proxy p do momentu kiedy licznik lv wyrówna się ze stanem licznika pv bez sup. Odpowiada to stanowi licznika globalnego w chwili rozpoczęcia działania metody. Po wykonaniu metody licznik lv 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 licznika lv do wartości pv.

    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

../_images/atomic-rmi-sched.svg.png

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 konta A i B jako obiekty zdalne typu Account i trzy rodzaje klientów:
  • Audit sprawdza i wypisuje stan każdego z kont,
  • Transfer dokonuje przelewu z konta A na konto B 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 typu Transactable).
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.

Ankieta

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.