Archive for Marzec, 2009

Definicja procesu – podstawowe komponenty (1)

Definicja procesu w NGinn jest oparta na sieci Petriego – tutaj można trochę o niej poczytać. Zatem mamy trzy podstawowe elementy konstrukcyjne sieci Petriego:

  • miejsca (place) – na diagramie przedstawione jako kółeczkaplaceW procesach rozróżniamy trzy podstawowe rodzaje miejsc: miejsce startowe (w którym proces bądź podproces się zaczyna), miejsce pośrednie (czyli takie wewnątrz procesu) oraz miejsce końcowe w którym proces się kończy. Proces może mieć tylko jedno miejsce startowe i dowolną ilość miejsc pośrednich i końcowych. Miejsca końcowe nie mogą mieć połączeń wychodzących.
    Podczas wykonania procesu miejsca przechowują tokeny (żetony) – czyli takie znaczniki wędrujące przez nasz model procesu pokazujące w którym miejscu proces się znajduje, czyli jaki jest jego aktualny status. W momencie wykonania zadania tokeny z jego miejsc wejściowych są zabierane, a pojawiają się nowe tokeny w miejscach docelowych tego zadania. 
  • tranzycje – w naszej nomenklaturze znane jako zadania (task) – na diagramie rysowane w formie prostokątów:taskZadania są podstawowym budulcem procesu – odpowiadają one za wykonanie określonych akcji. W NGinn występuje kilkanaście rodzajów zadań, odpowiedzialnych np za: wysyłkę i odbieranie powiadomień, komunikację z aplikacjami zewnętrznymi, zlecanie zadań użytkownikom, odliczanie czasu itd. Dokładniejsze omówienie zadań pojawi się w kolejnych odcinkach. 
    Zadanie może zostać uruchomione wyłącznie jeśli posiada odpowiednią liczbę tokenów na wejściu (czyli w miejscach z których prowadzą strzałki do tego zadania). Jeśli zadanie się wykona, zjada tokeny wejściowe i produkuje tokeny na wyjściu (czyli w miejscach do których prowadzą strzałki z tego zadania). Liczba wymaganych tokenów na wejściu oraz tych produkowanych na wyjściu zależą od rodzaju synchronizacji (join) lub rozwidlenia (split) skonfigurowanego dla tego zadania (to też będzie dokładniej omówione w kolejnych odcinkach). W oryginalnej definicji sieci Petriego strzałki mogą łączyć wyłącznie miejsce z zadaniem (czyli na jednym końcu musi być miejsce a na drugim zadanie). W NGinn dozwolone są również połączenia zadanie – zadanie, w takim przypadku aby zachować ‘kompatybilność’ z sieciami Petriego system wstawia pomiędzy tak połączone zadania niejawne miejsce (nie jest ono rysowane na diagramach).
  • krawędzie – u nas znane jako połączenia albo przejścia albo strzałki (flow). Na diagramie występują w formie strzałek łączących miejsca i zadania:
    flow
    Połączenia określają nam zależności między zadaniami w procesie. Jeśli połączenia prowadzą od miejsca do zadania, są to tzw połączenia wejściowe zadania. Określają one w których miejscach muszą pojawić się żetony aby zadanie mogło zostać uruchomione. Połączenia prowadzące od zadania do miejsca albo do innego zadania to połączenia wyjściowe – określają one miejsca w których zostaną wyprodukowane tokeny po zakończeniu zadania. Czyli połączenia definiują nam możliwe ścieżki wędrówki tokenów od miejsca startowego aż do miejsc końcowych. 

W kolejnych odcinkach przyjrzymy się sposobom łączenia miejsc i zadań tak, aby uzyskać pożądane przebiegi procesów.

Obsługa danych w NGinn

Można powiedzieć że definicja procesu składa się z dwóch części. Pierwszą częścią jest jego logika, czyli zadania wchodzące w skład procesu i powiązania między nimi. Drugą częścią są dane – czyli proces = logika + dane. Jak wygląda obsługa danych w NGinn?
Zacznijmy od podstaw. Co to są te ‘dane’? W naszym przypadku są to informacje na których operuje proces i zadania wchodzące w jego skład. Aby uruchomić jakikolwiek proces w NGinn należy przekazać mu odpowiednie dane wejściowe. Następnie proces się wykonuje, przetwarza te dane i po zakończeniu może nam zwrócić rezultaty, czyli dane wyjściowe. Czyli w ‘życiu’ procesu są dwa momenty wymiany danych – start i zakończenie – i nie interesuje nas co z danymi dzieje się pomiędzy nimi.
A co z zadaniami? Aby zachować przejrzystość projektu, z zadaniami jest dokładnie tak samo. Aby uruchomić zadanie należy mu przekazać dane wejściowe, po zakończeniu zwróci nam dane wyjściowe. Praktyczny efekt jest taki, że interfejs wymiany danych z zadaniami i z procesami jest dokładnie taki sam dla wszystkich rodzajów zadań i z naszego punktu widzenia zadania w procesie można dowolnie podmieniać o ile mają taką samą strukturę danych we/wy. Tak samo w miejsce zadania można wstawić inny proces. Czyli można powiedzieć że wyłącznie struktura danych wejściowych i wyjściowych określa nam interfejsy między zadaniami.
Są też dalsze implikacje przyjętej struktury. Skoro dane wymieniamy tylko przy starcie i zakończeniu zadania, to znaczy że zadanie do swojego działania nie może wymagać innych danych niż te które otrzymało na wejściu. Po prostu nie ma innej możliwości otrzymania danych i może korzystać wyłącznie ze swoich danych wejściowych. Nie muszę chyba dodawać co to oznacza: nie ma żadnego współdzielenia danych między zadaniami – każde z nich ma swoje własne dane i nie może wpływać na dane w innych zadaniach ani w nadrzędnym procesie.
A jak to wygląda praktycznie?
Przede wszystkim, dane reprezentujemy w postaci zmiennych. Czyli struktura danych dla zadania lub procesu to zestaw zmiennych.
Każda zmienna ma przypisany kierunek wymiany danych: są zmienne wejściowe (In), wyjściowe (Out), dwukierunkowe (InOut) oraz lokalne (Local). Zmienne lokalne nie są brane pod uwagę przy wymianie danych ze światem zewnętrznym, ale spełniają tę samą funkcję co zmienne lokalne w klasycznym programowaniu – tymczasowo przechowują dane.
Oprócz kierunku zmienna ma nazwę (unikalną w ramach zadania/procesu do którego należy) oraz typ.
Wśród typów danych mamy proste typy wbudowane (tzn takie których nie trzeba definiować):

  • string
  • int
  • bool
  • DateTime
  • double

oraz typy złożone, definiowane na potrzeby konkretnych procesów:

  • typy wyliczeniowe (enum) – możemy tworzyć własne, nazwane enumeracje
  • struktury (rekordy), czyli zestawy pól typu nazwa->wartość – odpowiednik ’struct’ w językach programowania. Pola w rekordzie mogą być dowolnego typu spośród opisanych tutaj, w szczególności mogą być rekordem – czyli definicje typów mogą być rekurencyjne

No dobra, to czego nam jeszcze brakuje do szczęścia? Tablic, brakuje nam zmiennych mogących przyjmować wiele wartości tego samego typu. W NGinn jest to rozwiązane tak, że każdą zmienną możemy oznaczyć jako typ tablicowy – czyli mając zmienną int możemy powiedzieć że to tablica, wtedy przyjmuje ona wiele wartości typu int. 

Ok, to chyba czas pokazać jak wygląda to od strony języka opisu procesu:


<variables>
<variable name="UserName" type="string" dir="In" required="true" isArray="false" />
<variable name="BirthYear" type="int" dir="In" required="true" isArray="false" />
<variable name="PhoneNumbers" type="string" dir="In" required="true" isArray="true" />
<variable name="RegistrationId" type="string" dir="Out" />
</variables>

Sekcja ‘variables’ występuje w definicji procesu oraz w definicji zadania, w obu przypadkach ma dokładnie tę samą postać pokazaną wyżej. Mam nadzieję że nie ma potrzeby opisywania co znaczy ten xml – wszystko powinno być jasne po przeczynaniu tekstu powyżej.

No dobra, to wiemy jak zdefiniować zmienne w procesie oraz w zadaniu. W kolejnych postach dowiemy się jak zrobić, żeby zmienne z procesu trafiły do zadania, a po zakończeniu zadania aby wróciły do procesu i zaktualizowały jego dane. Będzie też artykuł o definiowaniu własnych typów danych. A potem jeszcze mnóstwo innych informacji.

Script.Net czy Boo?

Jakiś czas temu do ‘oskryptowywania’ procesów zacząłem używać języka Script.Net którego autorem jest ukraiński programista – Petro Protsyk. Script.Net jest prostym w użyciu skryptem szczególnie nadającym się do wbudowywania w aplikacje, tylko że wersje na których pracowałem ciągle pozostawiały wiele do życzenia jeśli chodzi o jakość. Ilość błędów przekreślała produkcyjne użycie. Dlatego zacząłem patrzeć na język Boo – to już nie jest skrypt tylko język kompilowany, jednak pozwalający na łatwą dynamiczną kompilację i posiadający bardzo przyjazną skryptową składnię. Za pomocą Boo zrobiłem prosty moduł reguł biznesowych oraz view engine dla ASP.Net MVC pozwalającą generować dynamiczny JSON i generalnie jestem wielkim zwolennikiem tego języka, więc przyjąłem że w 2.0 Nginn Boo będzie używany również do programowania procesów. Implementacja tego jest jednak trudna, a w dodatku Petro Protsyk ostatnio wypuścił nową wersję Script.Net więc widać że chłopak się zajmuje swoim projektem i zmierza do wersji stabilnej – w rezultacie mam dylemat który język wybrać. Jak to bywa w informatyce, gdy nie wiadomo które rozwiązanie wybrać najczęściej wybiera się oba na raz i robi się system pluginów pozwalający użyć albo jednego albo drugiego, ale ja chciałbym uniknąć wykonywania podwójnej roboty. Na razie potestuję więc tego Script.Neta.

NGinn 2.0 – kilka szczegółów

Od czasu ostatnich wieści na temat NGinn upłynęło trochę czasu. Wieści nie było głównie dlatego że nie miałem czasu zajmować się blogiem. Ale zajmowałem się NGinn, szczególnie koncepcją drugiej wersji języka i silnika BPM. Tak, powstaje druga wersja NGinn mimo tego że pierwsza nie miała żadnego wydania – uznałem że nie ma sensu kontynuować NGinn v1.0 z uwagi na potrzebę silnego przerabiania frameworku w celu doimplementowania nowych funkcji które w międzyczasie przyszły mi do głowy. Dlatego na bazie NGinn 1.0 jeszcze raz zdefiniowałem język i pracuję nad nową wersją silnika NGinn v 2.0. Dziś kilka informacji o tym co będzie w wersji 2.

Po pierwsze, nowy diagram, żeby było o czym dyskutować:

svg2raster1

Różnica jest już w wyglądzie schematu, bo używam nowego narzędzia do diagramowania – jest to pakiet ‘aiSee’ firmy AbsInt. Generuje fajne grafy i mimo tego że pakiet zawiera trochę bugów i czasami jest narowisty, to i tak uważam że jest lepszy niż darmowy graphviz (w sumie aiSee też jest darmowy dla niekomercyjnych zastosowań). No, ale nowy generator obrazków to żaden przełom, więc idźmy dalej

Dość istotnym rozszerzeniem w stosunku do v1.0 jest dodanie podzadań złożonych (composite task), czyli takich podprocesów w wersji inline. Na obrazku jest to zadanie w przerywanej prostokątnej ramce, w środku zawiera taki mniejszy proces – i o to w sumie chodzi. Do czego jest nam potrzebne takie zadanie – otóż można bez niego żyć, ale co to za życie… Głównie chodzi o obsługę błędów – dla takiego zadania możemy globalnie obsłużyć błędy, podobnie jak w strukturalnych językach używając bloku try-catch. Zadanie złożone to taki wielki blok ‘try’, zaś doczepione do niego przejście z dwiema czerwonymi kreseczkami (double slash) reprezentuje blok ‘catch’ – czyli handler błędu. Działa to tak, że jeśli w trakcie realizacji podprocesu z zadania złożonego wystąpi jakiś błąd, to podproces ten zostanie zakończony i zostanie uruchomiona obsługa błędu. 

Ważna rzecz- zadania złożone można dowolnie zagnieżdżać – czyli możemy mieć wielopoziomową obsługę błędów zupełnie jak w zagnieżdżonych blokach try-catch. Ale nie tylko do obsługi błędów przydają się te zadania. Możemy na przykład bardzo uprościć strukturę grafu jeśli opisujemy ‘przejścia anulujące’ – czyli połączenia które powodują usuwanie tokenów z procesu. Na rysunku jest to czerwona kropkowana strzałeczka z zadania T3 do miejsca po_T4. Mówi ona ‘po zakończeniu T3 usuń wszystkie tokeny z miejsca po_T4 i anuluj wszystkie zadania które wymagają tokenu w miejsciu po_T4′. Używając zadania blokowego możemy anulować całą podgrupę zadań jednym połączeniem anulującym zamiast prowadzić takie połączenie do każdego zadania z grupy indywidualnie.

Kolejna rzecz to obsługa kompensacji – podobnie jak w ‘profesjonalnych’ (tych droższych :) komercyjnych systemach BPM-owych. Kompensacje to zadania wykonywane w celu anulowania skutków wykonania innych zadań. Będą zrealizowane podobnie jak obsługa błędów, ale to dopiero w NGInn v2.5

Jest jeszcze parę zastosowań zadań blokowych, ale o tym innym razem.

Co jeszcze? Możliwość kończenia procesu w wielu miejscach końcowych ( w NGinn 1.0 musiało być pojedyncze miejsce końcowe). Dochodzą też nowe zadania – ale o tym w kolejnych artykułach.

Jeśli chodzi o implementację silnika NGinn to w v 2.0 kompletnie zmienił się jego projekt. W 1.0 proces był pojedynczym ‘dokumentem’ scalającym w sobie stan wszystkich zadań, dane i historię. W v 2.0 postanowiłem maksymalnie ‘usamodzielnić’ zadania. Każde zadanie jest niezależnym bytem i może istnieć w oderwaniu od pozostałych. Oddzielnie przechowuje się jego stan – czyli stan procesu nie jest już jednym dokumentem a zestawem wielu dokumentów. Za koordynację takich samodzielnych zadań odpowiadają zadania blokowe – czyli te o których mowa była wyżej. Tak naprawdę to proces jest teraz takim dużym zadaniem blokowym, które może zawierać albo inne zadania blokowe, albo zadania atomowe (czyli te które faktycznie coś robią) – i tak dalej – dowolnie można to zagnieżdżać. Dlaczego taka decyzja – otóż autonomiczna obsługa zadań zwiększa nam stopień współbieżności w systemie, można jednocześnie aktualizować stan wielu zadań w tym samym procesie, łatwiej również zapanować nad rozmiarem danych opisujących zadania. Szybciej działa zapis i odczyt stanu pojedynczego zadania niż całego procesu – a zwykle interesuje nas dostęp do pojedynczego zadania i lepiej gdy nie musimy wczytywać w tym celu stanu całego procesu.