Katedra Architektury Systemów Komputerowych Wydział Elektroniki
Transkrypt
Katedra Architektury Systemów Komputerowych Wydział Elektroniki
Katedra Architektury Systemów Komputerowych Wydział Elektroniki, Telekomunikacji i Informatyki Politechniki Gdańskiej dr inż. Paweł Czarnul [email protected] Integracja usług w Internecie LABORATORIUM 6 POWIADOMIENIA ORAZ ZARZĄDZANIE ZASOBAMI W GLOBUS TOOLKIT 4 Tematem niniejszego laboratorium są: 1. powiadomienia (ang. notifications) o zmianach w zasobach tj. klient może zostać asynchronicznie powiadomiony o zmianach w zasobie np. zmianie wartości pola typu int, co więcej może (po uprzednim ustawieniu odpowiedniego parametru) odczytać z powiadomienia zarówno poprzednią jak i nową wartość takiego pola. Powiadomienia stanowią znaczne ułatwienie dla programisty jak również zwiększają wydajność i oszczędzają przepustowość łącza komunikacyjnego. Wynika to z faktu, iż alternatywne rozwiązanie – odpytywanie zasobu o zmiany (ang. polling) powoduje konieczność: • okresowego wysyłania zapytań do serwera – powstaje kwestia okresu – zbyt krótki spowoduje znaczne obciążenie sieci zaś zbyt długi znaczne opóźnienie w propagacji informacji o zmianie w zasobie – w konsekwencji zapewne spadek wydajności, • zakodowania logiki odpytującej serwer. Mechanizm powiadomień umożliwia zaś dokonanie subskrypcji informacji o zasobie i jedynie implementację metody wywoływanej po stronie klienta po wystąpieniu zmiany na serwerze. 1. zarządzanie zasobami – klient może nie tylko tworzyć zasoby jak pokazano we wcześniejszych laboratoriach, ma również możliwość ich niszczenia, w szczególności w dwóch trybach: • natychmiastowym (ang. immediate destruction) – zasób jest niszczony natychmiast po wywołaniu metody, • z opóźnieniem (ang. scheduled destruction) – zasób jest niszczony po upływie zadanego czasu. Ze względu na znakomite przykłady dostarczone z tutorialem Globus Toolkit 4, również dotyczącymi powiadomień i zarządzania zasobami, niniejsze opracowanie rozważy problem, do rozwiązania którego wykorzystany zostanie jedynie mechanizm powiadomień. Ze względu na prostotę mechanizmu niszczenia zasobów, autor odwołuje Czytelnika do przykładów dostarczonych z ww. tutorialem. Znajdują się one pod adresem: Rozważa się następujący problem: Zaimplementować zlecanie zadań przez klienta serwerowi w taki sposób, aby klient zlecał nowe zadania gdy serwer zakończy zadanie(a) zlecone poprzednio. Zakłada się, iż wykonanie zadań może zająć różną ilość czasu, co więcej czas ten może być znaczący. Zakładając dostępność mechanizmu powiadomień, wydaje się, iż problem ten rozwiązać można stosunkowo prosto tj.: 1. Serwer wykonuje zadanie po czym zmienia stan zasobu, na który klient wcześniej dokonał subskrypcji. 2. Zmiana stanu zasobu powoduje przesłanie powiadomienia do klienta, w rzeczywistości powoduje wywołanie odpowiedniej metody. 3. Klient w ww. metodzie wywołuje po raz kolejny metodę zlecającą serwerowi wykonanie kolejnego zadania. Jest to tym bardziej ułatwione, iż odpowiedni EPR jest dostępny w ww. metodzie. Powyższe rozwiązanie, choć funkcjonalnie bez zarzutu, może zostać ulepszone ze względu na wydajność. Otóż pomiędzy momentem zakończenia wykonywania zadania a momentem rozpoczęcia wykonywania kolejnego zadania upływa czas (powiadomienie, decyzja klienta o uruchomieniu nowego zadania, przesłanie danych) – znaczący w przypadku rozważanej technologii. Kod prezentowany poniżej rozwiązuje ten problem w jeden z możliwych sposobów tj.: 1. Po stronie serwera tworzone są dwa wątki wykonujące zadania, podobnie do rozwiązania w instrukcji do laboratorium nr 4. Każdemu z wątków przypisane jest pole zasobu (typu int). Wątek otrzymuje zadanie do wykonania, gdy nie ma zadania, wywołuje metodę wait() przechodząc w stan oczekiwania. • w momencie wywołania metody zlecenia zadania, serwer wybiera wolny wątek i zleca mu wykonanie zadania, • wątek po wykonaniu zadania modyfikuje przypisane mu określone pole zasobu; powoduje to przesłanie informacji o zmianie stanu zasobu do klienta, który wcześniej dokonał subskrypcji na dany zasób – w rzeczywistości uruchamiane są dwa programy klientów, którzy odpowiednio czekają na zmiany ww. dwóch pól (innymi słowy zakończenie wykonania zadania przez dwa wątki). Kod po stronie serwera definiuje pola zasobu wykorzystywane odpowiednio przez pierwszy i drugi wątek: /* Resource properties */ private ResourceProperty valueRP; private ResourceProperty value1RP; Są one inicjalizowane jak w przykładach w poprzedzających instrukcjach. Ponadto, usługa musi implementować interfejs TopicListAccessor, dodatkowo zawiera: /* Topic list */ private TopicList topicList; /* Configure the Topics */ this.topicList = new SimpleTopicList(this); valueRP = new ResourcePropertyTopic(valueRP); ((ResourcePropertyTopic) valueRP).setSendOldValue(true); value1RP = new ResourcePropertyTopic(value1RP); ((ResourcePropertyTopic) value1RP).setSendOldValue(true); this.topicList.addTopic((Topic) valueRP); this.topicList.addTopic((Topic) value1RP); Umożliwia to klientowi subskrypcję zmian ww. pól. Dodatkowo wywołanie metody setSendOldValue(true); powoduje, iż klient będzie w stanie pobrać zarówno nową wartość i jak i ustawioną poprzednio. Kod serwera implementuje w inny sposób metodę add(int). Autor zakłada tutaj, iż metoda ta powinna przpisać dane wejściowe (w tym przypadku liczbę typu int) do wolnego wątku i zwrócić sterowanie. W rzeczywistości metoda add(int) zakodowana jest w następujący sposób: public AddResponse add(int a) throws RemoteException { assignDataToThread(a); return new AddResponse(); } zaś metoda przydzielająca wątek do danych wejściowych: synchronized private void assignDataToThread(int a) throws RemoteException { for (int i=0; i<computeThread.length; i++){ if ( computeThread[i].isWaitingForNewRequests()) { // now assign the value to the resource if (i==0) { computeThread[i].performComputation(valueRP,a); System.out.println("starting thread 0 with"+a); } else if (i==1) { computeThread[i].performComputation(value1RP,a); System.out.println("starting thread 1 with"+a); } return; } } throw new RemoteException("No worker thread available"); } Metoda performComputation(...) przyjmuje dane wejściowe oraz pola zasobu, które będą ustawiane po zakończeniu operacji na danych. Faktyczne operacje na danych będą zakodowane w wątku obliczeniowym, który może np. sprawdzać czy dana liczba jest pierwsza czy też nie. Konkretnie, kod wątku obliczeniowego ComputeThread jest analogiczny do wątków całkujących z poprzedniego laboratorium. Po zakończeniu obliczeń zmienia wartość pola zasobu co powoduje powiadomienie klienta: valueRP.set(0,new Integer(data)); Początkowo wartości ww. pól ustawione są na 0. Po zakończeniu pierwszych wywołań (jak opisano poniżej) wartości ustawione są na argumenty podane w wywołaniu klienta z linii poleceń, w kolejnych wywołaniach generowanych już automatycznie po otrzymaniu powiadomienia, iż wątek jest bezczynny – wartości te ustawiane są na kolejne liczby naturalne. Oczywiście, wynik obliczeń może być ustawiany jw. 1. Kod, analogicznie do przykładu dotyczącego powiadomień w tutorialu Globus Toolkit 4, składa się z dwóch klas: • klient wywołujący metodę zlecającą jedno zadanie, • klient nasłuchujący na zmianę zasobu (klient nasłuchuje na zmianę pola typu int przypisanego do pierwszego wątku – patrz wyżej); po otrzymaniu informacji o zmianie zasobu (innymi słowy zakończeniu zadania przez pierwszy wątek) metoda ta zleca wykonanie kolejnego zadania. Kompilacja źródeł aplikacji po stronie serwera (w katalogu, który powstanie po rozpakowaniu archiwum): globus@wolf:~/tilab6$ ./globusbuildservice.sh notifications Wdrożenie aplikacji: globus@wolf:~/tilab6$ globusdeploygar org_globus_examples_services_core_notifications.gar Uruchomienie serwera: globus@wolf:~/tilab6$ globusstartcontainer nosec Następnie należy skompilować źródła trzech klientów: globus@wolf:~/tilab6$ javac org/globus/examples/clients/MathService_instance_notif/ValueListener.java globus@wolf:~/tilab6$ javac org/globus/examples/clients/MathService_instance_notif/ValueListener1.java globus@wolf:~/tilab6$ javac org/globus/examples/clients/MathService_instance_notif/ClientAdd.java Następnie należy uruchomić programy klientów (w osobnych konsolach, w podanej kolejności): 1. globus@wolf:~/tilab6$ java DGLOBUS_LOCATION=$GLOBUS_LOCATION classpath ./build/stubs/classes/:$CLASSPATH org/globus/examples/clients/MathService_instance_notif/ValueListener http://127.0.0.1:8080/wsrf/services/examples/core/notifications/MathService Waiting for notification. CtrlC to end. 2. globus@wolf:~/tilab6$ java DGLOBUS_LOCATION=$GLOBUS_LOCATION classpath ./build/stubs/classes/:$CLASSPATH org/globus/examples/clients/MathService_instance_notif/ValueListener1 http://127.0.0.1:8080/wsrf/services/examples/core/notifications/MathService Waiting for notification. CtrlC to end. 3. Po uruchomieniu klientów nasłuchujących, należy uruchomić klienta, który zleci zadanie do wykonania najpierw pierwszemu a następnie drugiemu wątkowi (wątki zostaną przypisane automatycznie przez metodę po stronie serwera). Argumentem wywołania jest liczba typu int, która jest przekazywana do każdego wątku. • globus@wolf:~/tilab6$ java classpath ./build/stubs/classes/:$CLASSPATH org/globus/examples/clients/MathService_instance_notif/ClientAdd http://127.0.0.1:8080/wsrf/services/examples/core/notifications/MathService 1000 Value RP: 0 LastOp RP: NONE należy odczekać kilka sekund i: • globus@wolf:~/tilab6$ java classpath ./build/stubs/classes/:$CLASSPATH org/globus/examples/clients/MathService_instance_notif/ClientAdd http://127.0.0.1:8080/wsrf/services/examples/core/notifications/MathService 2000 Value RP: 0 LastOp RP: NONE globus@wolf:~/tilab6$ Wywołania powinny zwrócić sterowanie od razu, zaś wątki rozpoczną pracę, co zostanie udokumentowane komunikatami po stronie serwera: starting thread 0 with1000 Thread[Thread0,5,main].starting computations starting thread 1 with2000 Thread[Thread1,5,main].starting computations Thread[Thread0,5,main].***1000*0 Następne zadania będą już zlecane automatycznie poprzez klientów nasłuchujących (ValueListener oraz ValueListener1) tj. po wykonaniu zadania przez jeden wątek, jeden z klientów zostanie powiadomiony i prześle kolejne zadanie (w tym czasie drugi z wątków po stronie serwera będzie zajęty). To samo tyczy się drugiego wątku. Zakończenie kolejnych zadań spowoduje uruchomienie nowych itd. Komunikaty widoczne po stronie klientów nasłuchujących będą miały postać: globus@wolf:~/tilab6$ java DGLOBUS_LOCATION=$GLOBUS_LOCATION classpath ./build/stubs/classes/:$CLASSPATH org/globus/examples/clients/MathService_instance_notif/ValueListener1 http://127.0.0.1:8080/wsrf/services/examples/core/notifications/MathService Waiting for notification. CtrlC to end. A notification has been delivered Old value: 0 New value: 2000 A notification has been delivered Old value: 2000 New value: 2001 A notification has been delivered Old value: 2001 New value: 2002 A notification has been delivered Old value: 2002 New value: 2003 i dla drugiego klienta: globus@wolf:~/tilab6$ java DGLOBUS_LOCATION=$GLOBUS_LOCATION classpath ./build/stubs/classes/:$CLASSPATH org/globus/examples/clients/MathService_instance_notif/ValueListener http://127.0.0.1:8080/wsrf/services/examples/core/notifications/MathService Waiting for notification. CtrlC to end. A notification has been delivered Old value: 0 New value: 1000 A notification has been delivered Old value: 1000 New value: 1001 A notification has been delivered Old value: 1001 New value: 1002 A notification has been delivered Old value: 1002 New value: 1003 Wynika to z faktu, iż klient nasłuchujący odczytuje nową wartość zmienionego zasobu, dodaje 1 i wywołuje ponownie metodę po stronie serwera: public class ValueListener implements NotifyCallback { /* This method is called when a notification is delivered */ public void deliver(List topicPath, EndpointReferenceType producer, Object message) { ResourcePropertyValueChangeNotificationElementType notif_elem; ResourcePropertyValueChangeNotificationType notif; notif_elem = (ResourcePropertyValueChangeNotificationElementType) message; notif = notif_elem.getResourcePropertyValueChangeNotification(); MathServiceAddressingLocator instanceLocator = new MathServiceAddressingLocator(); if (notif != null) { System.out.println("A notification has been delivered"); System.out.print("Old value: "); System.out.println(notif.getOldValue().get_any()[0].getValue()); System.out.print("New value: "); System.out.println(notif.getNewValue().get_any()[0].getValue()); try { MathPortType math = instanceLocator.getMathPortTypePort(producer); // Perform addition math.add(Integer.valueOf(notif.getNewValue().get_any()[0].getValue()).intValue() +1); } catch (Exception e) { System.out.println("Error"); e.printStackTrace(); } ... Pomimo tego, iż implementacja osiągnęła cele wymienione powyżej, w rzeczywistości idea polega na uruchomieniu dwóch klientów (drugi z pewnym opóźnieniem), z których każdy jest powiadamiany o dostępności “jego” wątku i ponownie zleca mu zadanie. Uruchomienie z opóźnieniem powoduje, iż serwer jest zajęty obliczeniami drugiego wątku, gdy pierwszy przesyła kolejne dane (i odwrotnie). Autor pozostawia poniższe kwestie do analizy Czytelnikowi co powinno dalej poprawić kod tj.: 1. Dotychczasowa implementacja wykorzystuje różne kody klientów. 2. Serwer wykorzystuje dwa wątki do obliczeń. Alternatywą byłoby wykorzystanie tylko jednego wątku do obliczeń oraz umożliwienie klientowi umieszczenia nowych danych po stronie serwera w czasie gdy wątek przetwarza wcześniej zapisane dane. Wątek, po przetworzeniu danych, pobiera kolejne, które już znajdują się po stronie serwera. Tego typu techniki opisane są również w kontekście aplikacji mobilnych w technologii J2ME. Autor odsyła Czytelnika do artykułu dostępnego pod adresem: http://developers.sun.com/techtopics/mobility/ midp/articles/threading/.