Archive for Kwiecień, 2009

Definiowanie procesu (2) – rozwidlenia i złączenia

W dzisiejszym odcinku kilka kolejnych elementów języka opisu procesów NGinn – a mianowicie rozwidlenia i złączenia ścieżek w procesie, czyli narzędzia do wprowadzania zawiłości tam gdzie mogło być prosto. Jako źródło przykładów wykorzystamy proces przedstawiony na poniższym schemacie:

 

 

Schemat procesu

Schemat procesu

 

Od razu zaznaczam że proces tu przedstawiony nie ma żadnego głębszego sensu i nie jest oparty na żadnym wziętym z życia przykładzie (dlatego zadania mają klarowne nic nie mówiące nazwy). Łatwo się zorientować gdzie proces ma początek a gdzie koniec oraz że nie ma żadnych zadań blokowych. Miał być prosty i jest prosty.

Rozwidlenia i złączenia

Rozwidlenie (dalej nazywane “split” na cześć miasta w Chorwacji) to miejsce w którym pojedyncza ścieżka w procesie dzieli się na dwie lub więcej ścieżek. Podział taki następuje na wyjściu z zadania, tzn miejscem rozwidlenia jest zadanie. Mamy 3 podstawowe rodzaje rozwidleń:

 

  • AND
  • XOR
  • OR

 

W definicji procesu każde zadanie ma atrybut ’splitType’ mogący przyjąć jedną z powyższych wartości. Domyślnie (gdy go nie ma) przyjmowany jest AND. 

Złączenie (dalej nazywane join) to miejsce spotkania się kilku ścieżek w procesie, przy czym chodzi o spotkanie się na wejściu tego samego zadania. I znowu, mamy 3 rodzaje złączeń, dokładnie takie same jak wymienione wyżej rodzaje rozwidleń, czyli AND, XOR i OR. Nie jest to przypadek, gdyż jeśli zaczniemy konstruować proces okaże się że najczęściej do rozwidlenia typu AND pasuje złączenie typu AND, to samo dla typów XOR i OR.

Uwaga: mamy jeszcze jeden typ rozwidlenia – specjalny przypadek nazywany ‘deferred choice‘, który zasłużył sobie na osobny paragraf niżej.

XOR – czyli punkt decyzyjny

Rozwidlenie typu ‘XOR’ oznacza wybór dokładnie jednego z kilku wariantów. Na rysunku oznaczane jest pustym w środku rombem. 

 

XOR-Split

XOR-Split

Mamy tutaj rozwidlenie na zadaniu T1 rozdzielające nam ścieżkę na 3 warianty prowadzące do zadań T1_1, T1_2 i T1_4. Podczas wykonania procesu, w zależności od spełnienia określonych warunków, zostanie wybrana dokładnie jedna ze ścieżek i proces będzie kontynuowany w zadaniu T1_1 albo T1_2, albo T1_4. Dla programistów: jest to taka konstrukcja ’switch’  albo seria if – else if – end.

Warunki do spełnienia (nazywane warunkami wejściowymi – Input Condition) są przypisywane do każdej ze ścieżek, z wyjątkiem ostatniej – ona nie musi mieć warunku gdyż jest ścieżką domyślną, wybieraną wtedy gdy żadna inna nie zostanie wybrana. A jak stwierdzić która ścieżka jest pierwsza a która ostatnia? Widać to dopiero w definicji procesu – zgodnie z porządkiem w którym są umieszczone przejścia w definicji (można też jawnie podać porządek) – za chwilę to sobie zobaczymy.

Należy pamiętać o jednej rzeczy – w sieciach Petriego nie można łączyć zadania z zadaniem bezpośrednio. Po drodze musi być miejsce (place). Tutaj te miejsca też są, tylko że niewidoczne. Można przyjąć że w połowie każdej z trzech widocznych na rysunku strzałek jest place, czyli tak naprawdę pojedyncze przejście ma dwa etapy. Ma to znaczenie gdy obserwuje się XOR-split w działaniu. Otóż zadziałanie jednego z przejść XOR-splitu powoduje umieszczenie tokenu w miejscu do którego to przejście prowadzi. Dla pozostałych wariantów token nie będzie wyprodukowany. A oto jak wygląda definicja naszego XOR-splita:


<flow from="T1" to="T1_1" label="Yes">
	<inputCondition>data.answer == 'Yes'</inputCondition>
</flow>
<flow from="T1" to="T1_2" label="No">
	<inputCondition>data.answer == 'Yes'</inputCondition>
</flow>
<flow from="T1" to="T1_4" label="Not sure"/>

Widzimy trzy strzałki (flow), dwie z nich zawierają inputCondition – czyli wyrażenie które będzie sprawdzane w momencie napotkania XOR-split. Sprawdzane będą w kolejności występowania i pierwszy spełniony warunek wygeneruje token na wyjściu. Gdy żaden nie będzie spełniony token pojawi się w ostatnim przejściu, czyli tym oznaczonym ‘Not sure’.

A teraz zbadajmy złączenie typu XOR, czyli XOR-join

 

XOR-join

XOR-join

Mamy tutaj złączenie trzech ścieżek w jedną, prowadzącą do zadania T1_E. Złączenie typu XOR oznaczane jest pustym w środku rombem z wchodzącą strzałką. Ta konstrukcja oznacza, że w momencie pojawienia się tokenu na którejkolwiek ze ścieżek wchodzących zadanie T1_E zostanie uruchomione. 

No dobra, można zapytać, a co jeśli token pojawi się na dwóch lub wszystkich trzech ścieżkach jednocześnie?

Po pierwsze, jeśli podziału dokonaliśmy za pomocą XOR-split, to token pojawi się na dokładnie jednej ścieżce i problemu nie ma. No ale jeśli podział był robiony np AND-splitem, to mamy tokeny na wszystkich ścieżkach. W takiej sytuacji XOR-join spowoduje uruchomienie zadania T1_E trzykrotnie, za każdym razem wykonanie tego zadania będzie zjadać jeden token z wejścia aż wyczerpią się wszystkie tokeny. Zwykle takie zachowanie nie jest prawidłowe dlatego należy zwracać uwagę aby XOR-join miał zawsze dokładnie jeden token na wejściu.

 

AND – ścieżki równoległe

Rozwidlenie typu ‘AND’ powoduje wykonanie wszystkich ścieżek które się za nim znajdują, przy czym są one wykonywane równolegle, niezależnie od siebie.

AND-split

AND-split

Na rysunku z prawej jest narysowany AND-split prowadzący z zadania T0 do zadań T1 i T2. Nie ma on żadnego dodatkowego symbolu na strzałce i jest to domyślny rodzaj rozgałęzienia w NGinn (czyli jeśli nie podamy w definicji procesu rodzaju rozgałęzienia to zostanie przyjęty AND).

W tym przypadku zakończenie realizacji zadania T0 powoduje wygenerowanie tokenów na wszystkich wychodzących z niego ścieżkach, co oznacza że zadania T1 i T2 będą mogły zostać uruchomione. Od tej pory przebieg procesu ma dwie niezależne odnogi. Odnogi te pozostaną niezależne do momentu ich złączenia za pomocą AND-join, widocznego na rysunku:

 

AND-join

AND-join

Tutaj zadania T1_E i T2_E należące do równoległych ścieżek łączą się na wejściu do zadania T0_E. Taki zapis oznacza że zadanie T0_E nie może się rozpocząć do momentu zakończenia zarówno zadania T1_E jak i T2_E. Jeśli zakończy się tylko jedno z tych zadań, system zaczeka na zakończenie drugiego i dopiero wtedy uruchomi zadanie T0_E. Zasada działania AND-join jest taka jak w sieci Petriego – zadanie może zostać uruchomione dopiero wtedy, gdy we wszystkich jego miejscach wejściowych znajduje się jakiś token. 

Po zakończeniu zadania T0_E z każdego z jego miejsc wejściowych zostanie zjedzony jeden token, w tym przypadku będą to dwa tokeny. Jeśli teraz na wyjściu z T0_E pojawi się jeden token, to dokonaliśmy scalenia dwóch ścieżek z powrotem w jedną.

 

OR – wybór wielokrotny

Rozgałęzienie typu ‘OR’ jest wersją pośrednią między AND i XOR. Oznacza ono że może zostać wybrane kilka wariantów spośród wszystkich dostępnych. 

 

OR-split

OR-split

Mamy z prawej OR-split na zadaniu T2 i trzy ścieżki wyjściowe oznaczone ‘Pretty’, ‘Loud’ i ‘Tall’. OR-split jest oznaczany rombem z przekątnymi.  Każda ze ścieżek w OR-split ma zdefiniowany warunek (input condition), przy czym, w odróżnieniu od XOR, ostatnia ścieżka również zawiera warunek.

Efekt wykonania OR-split jest taki, ze token pojawia się na każdej ze ścieżek wyjściowych dla której spełniony jest warunek. System wymaga aby zawsze przynajmniej dla jednej ścieżki warunek był spełniony, jeśli żaden nie zostanie spełniony w procesie zostanie zgłoszony błąd. Jeśli wybrane zostanie kilka ścieżek będą one wykonywane równolegle.

Choć OR-split wydaje się bardzo fajnym, uniwersalnym narzędziem łączącym to co najlepsze w AND i XOR to  powinien być stosowany rozważnie, tylko tam gdzie to niezbędne. Jest tak dlatego że komplikuje on logikę procesu i utrudnia wykrycie ewentualnych błędów. Ponieważ nie wiadomo ile ścieżek wyjściowych się uaktywni po OR-join, nie można tych ścieżek łączyć za pomocą AND-join. XOR – join też jest niewskazany po OR-split bo jego działanie jest nieoczekiwane dla kilku tokenów na wejściu. Czyli zostaje nam OR-join.

 

 

OR-join

OR-join

OR-join jest oznaczany na schemacie rombem z przekątnymi umieszczonym na końcu strzałki. Jest to bardzo ciekawy przypadek złączenia, mówi on mniej-więcej: Poczekaj na wszystkie tokeny które mogą się pojawić na wejściu do zadania T2_E i dopiero wtedy uruchom zadanie T2_E. Czyli NGinn musi się jakoś domyślić ile tokenów może się pojawić na wejściu T2_E i czy są to już wszystkie tokeny które mogły tam dotrzeć, czy też należy czekać na nadejście kolejnych. Ponieważ tokeny mogą przychodzić na wejście OR-join z dowolnych miejsc w procesie (tzn z takich miejsc z których istnieje droga) to NGinn realizując OR-join musi brać pod uwagę nie tylko tokeny na wejściu do zadania T2_E, ale również stan pozostałych tokenów w sieci i jeśli stwierdzi że albo w sieci nie ma już więcej tokenów, albo te co są nie mają szans dotrzeć na wejście T2_E, dopiero wtedy uruchomi T2_E.

Uwaga: w obecnej wersji systemu użycie OR-join wymaga również podania listy miejsc z których tokeny mogą dotrzeć do OR-join. W przyszłości system automatycznie wyznaczy sobie taką listę.

 

Deferred choice – metoda faktów dokonanych

Czasami nie jesteśmy w stanie na etapie projektowania procesu powiedzieć która ścieżka powinna zostać wybrana. Może być tak wtedy, gdy nie znamy kryteriów według których określona ścieżka powinna zostać wybrana, lub gdy wybór jest dyktowany czynnikiem zewnętrznym w stosunku do naszego procesu. Mam nadzieję że zaraz to wyjaśnię.

 

Deferred choice

Deferred choice

Mamy tu miejsce p0, z którego wychodzą dwa zadania – T_V1 i T_V2. Każde z tych zadań do uruchomienia wymaga jednego tokenu – tego który znajduje się w p0. Czyli oba zadania mogą być uruchomione jeśli tylko token tam się pojawi. Natomiast tylko jedno z zadań może się zakończyć – zakończenie tego zadania oznacza skonsumowanie tokenu z p0, a wtedy drugie zadanie straci możliwość wykonania. NGinn w takim przypadku anuluje to zadanie.

Jakie jest praktyczne zastosowanie deferred choice? Po pierwsze możemy zaoferować kilka ścieżek do wyboru – wtedy prezentowane są wszystkie możliwości, a wybór jednej z nich powoduje anulowanie pozostałych. Nie wnikamy w to jakie są kryteria wyboru określonej ścieżki.

Drugim typowym przypadkiem jest implementacja ograniczenia czasowego (timeout) na wykonanie określonego zadania. Weźmy przypadek pisma do urzędu. Urzędnik ma powiedzmy 30 dni na odpowiedź na to pismo. Po upływie 30 dni chcemy uruchomić jakąś procedurę eskalacyjną. W naszym przykładzie niech T_V1 będzie zadaniem ‘Odpowiedz na pismo obywatela X’, zaś T_V2 – zegarem (zadanie typu ‘timer’) odliczającym 30 dni. W momencie wejścia tokenu do p0 startują oba zadania: urzędnik dostaje zlecenie obsłużenia pisma, zaś timer zaczyna odliczanie 30 dni. Jeśli urzędnik zdąży zakończyć zadanie przed upływem 30 dni, w momencie zakończenia zadania timer zostaje anulowany. Jeśli nie zdąży to zakończy się zadanie z zegarem i zostanie anulowane jego zlecenie. Inaczej niż na tym rysunku, za zadaniem T_V2 (timer) możemy umieścić procedurę eskalacji – np wysłanie powiadomienia do kierownika jednostki albo zlecenie wykonania telefonu do klienta z informacją ze jego pismo jest nieważne i musi zostać złożone ponownie ;)

Pętla

 

Pętla - XOR

Pętla - XOR

Oto przykład realizacji pętli za pomocą XOR-split oraz XOR-join. Startujemy w zadaniu T1_2, po jego wykonaniu mamy XOR-split, prowadzący do zadania T1_3 oraz T1_E. Z zadania T1_3 wracamy do zadania T1_2. W ten sposób stworzyliśmy pętlę. Warunek wyjścia z pętli umieszczamy w wyjściu z zadania T2. Zwykle zadanie T2 jest tylko punktem decyzyjnym, zaś właściwe ‘ciało’ pętli to zadanie T1_3. Można oczywiście jeszcze krócej i zwięźlej – wyjście z zadania T1_2 można skierować bezpośrednio do tego samego zadania, wtedy T1_2 będzie ciałem pętli a po jego zakończeniu będzie sprawdzany warunek wyjścia z pętli – czyli znajoma pętla repeat…until.

Uwaga: zastosowanie AND-split w pętli stworzy nam generator tokenów – czasami może być przydatny, ale zwykle to błąd.

Na tym przykładzie zakończymy temat podstawowych struktur sterujących w NGinn. Do omówienia zostały bardziej skomplikowane konstrukcje, czyli obsługa błędów, anulowanie zadań, zadania blokowe i podprocesy. Wszystko to w kolejnych odcinkach.