Archive for Maj, 2009

Definicja procesu (5) – obsługa błędów

Dziś poruszymy temat obsługi błędów w procesach. Zacznijmy od tego, że w NGinn każde zadanie, jeśli zostanie uruchomione, może zakończyć się z następującymi rezultatami:

  • zakończenie normalne (prawidłowe, na wyjściu otrzymujemy dane wyjściowe z zadania)
  • anulowanie
  • zakończenie na skutek błędu

Zakończenie normalne to przypadek omawiany już wcześniej – po zakończeniu zadania są produkowane tokeny w miejscach do których prowadzą połączenia wychodzące (oczywiście jeśli mamy split XOR lub OR to są jeszcze sprawdzane warunki dla tych przejść). Po anulowaniu tokeny wyjściowe nie są produkowane (jest taka opcja, ale o tym kiedy indziej). Po zakończeniu z błędem natomiast mamy pewne możliwości zareagowania na ten błąd i odpowiedniego dostosowania dalszego biegu procesu – w tym celu używamy ‘error handlerów’ czyli specjalnych przejść uaktywnianych tylko w  razie błędu:

Obsługa błędu (1)

Obsługa błędu (1)

 

W tym procesie normalny przebieg jest taki: start->T1->T2->end. Natomiast zadanie T1 ma dodatkowe przejście wychodzące, oznaczone na rysunku dwoma czerwonymi ukośnikami. To przejście jest uruchamiane w razie zakończenia zadania T1 z błędem (tzn jeśli T1 się ‘wywali’ to token pojawi się na przejściu T1->T1 error a nie na T1->T2), czyli kolejnym uruchomionym zadaniem będzie ‘T1 error’ – nasz ‘error handler’ dla T1.

No dobra, świetnie: potrafimy już przyczepić obsługę błędów do jakiegoś zadania. Ale przecież w procesie wiele rzeczy może się nie udać, jeśli zaczniemy wszędzie doczepiać obsługę błędów to szybko nasz diagram zamieni się w chaotyczną plątaninę strzałek. A poza tym: co się stanie jeśli zadanie się ‘wywali’ a nie ma zdefiniowanej obsługi błędów? Żeby odpowiedzieć na powyższe wątpliwości zobaczmy kolejny diagram:

 

Obsługa błędów (2)

Obsługa błędów (2)

 

Na tym schemacie mamy zadanie blokowe (to z przerywaną ramką) do którego dołączona jest obsługa błędu (T1 error). Zapis taki oznacza że w przypadku wystąpienia jakiegokolwiek błędu w zadaniu blokowym obsługujemy ten błąd w ‘T1 error’. A dokładnie: jeśli w trakcie wykonania zadania blokowego pojawi się błąd w zadaniu ‘Task1′ lub ‘Task2′ to całe zadanie blokowe zostanie zakończone z błędem. Jeśli były w nim jakieś inne aktywne zadania w momencie wystąpienia błędu to zostaną one anulowane – wszystko co było wewnątrz zadania blokowego zostaje zakończone i dopiero wtedy całe zadanie blokowe kończy się z błędem. Informacje o błędzie są propagowane w górę od zadania które spowodowało błąd – czyli kod błędu w zadaniu blokowym będzie taki sam jak kod błędu zwrócony przez zadanie wewnętrzne które zakończyło się błędem. Mętne? Czy jesteś czytelniku programistą? To pomyśl o konstrukcji try-catch: wszystko co jest wewnątrz zadania blokowego to ‘try’, zaś error handler z czerwonymi ukośnikami to ‘catch’.

A co jeśli zadanie blokowe także nie ma swojej obsługi błędów? Wtedy NGinn propaguje błąd dalej w górę aż dojdzie na poziom zadania blokowego które już ma error handler – czyli ta konstrukcja może być dowolnie głęboko zagnieżdżona. Tylko że do tej pory zakładam że w końcu wyjdziemy na taki poziom gdzie ta obsługa błędu jest. A co gdy nie ma? W końcu idąc w górę dojdziemy na poziom całego procesu – do procesu już nie można sobie doczepić ‘error handlera’ bo nie ma do czego. Czyli wtedy mamy sytuację że nasz ‘wyjątek’ nie jest obsługiwany w ramach procesu  (dla programistów: unhandled exception). W normalnych programach taka sytuacja jest awarią – system wyświetla informację o błędzie i natychmiast kończy działanie programu. A u nas? NGinn mógłby wtedy wpisać do logu że ‘proces XYZ niespodziewanie zakończył się z powodu nieobsłużonego błędu’ i usunąć ten proces, ale to nie jest działanie cywilizowane w świecie procesów biznesowych – pamiętajmy że ten proces reprezentuje jakieś dokumenty, ludzkie działania, decyzje w firmie i nie może tak sobie zniknąć.

Dlatego w NGinn taką sytuacje traktujemy jako typową. To znaczy: normalne jest że definicja procesu nie zawiera obsługi błędów i takie procesy też powinny działać i dawać spodziewane wyniki. A jeśli wystąpi błąd to ma zostać o tym powiadomiony administrator systemu i powinien on mieć szansę ręcznej interwencji w celu usunięcia lub obejścia problemu. Jak to się dzieje? Otóż gdy NGinn w trakcie obsługi błędu stwierdzi że w całym procesie brak jakiejkolwiek obsługi tego błędu to błąd ten nie będzie propagowany w górę. Zamiast tego winowajca (zadanie które ten błąd wygenerowało) jest oznaczane jako ‘FailedActive’ – tzn zakończone z błędem, ale jeszcze nie wiadomo co z nim zrobić. Zadanie takie już się zakończyło, ale dla zadania nadrzędnego jest nadal aktywne i nie generuje żadnych dalszych zdarzeń. Administrator systemu może sobie takie przypadki odszukać (albo nawet zostać automatycznie powiadomiony) i wtedy może przeprowadzić następujące działania:

  • ponownie uruchomić zadanie (np gdy błąd to tylko przejściowy problem) 
  • potwierdzić że to zadanie to rzeczywiście błąd i przekazać jego obsługę NGinn (ale wtedy proces rzeczywiscie zostanie zakończony z błędem)
  • ‘oszukać’ NGinn i powiedzieć że to zadanie jednak zakończyło się sukcesem (wtedy musi jednak dostarczyć prawidłowe dane wyjściowe dla tego zadania, tak jakby rzeczywiście zakończyło się ono poprawnie)

Czasami będzie jeszcze musiał poprawić definicję procesu jeśli błąd powstał na skutek nieprawidłowości w definicji. 

Sposób udostępniana w/w operacji dla administratora może być różny. NGinn oferuje API do tego celu a udostępnienie tego w postaci GUI lub command line to zadanie dla aplikacji używającej NGinn.

Acha, oto definicja pierwszego procesu z dzisiejszego artykułu. W sekcji ‘flows’ jest zdefiniowana obsługa błędu dla zadania T1 – chodzi o przejście z taskOutPort=”Error”:


<?xml version="1.0" encoding="utf-8"?>
<process version="1" name="Failure" xmlns="http://www.nginn.org/WorkflowDefinition.1_0.xsd" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <dataTypes>
    </dataTypes>
    <variables>
		<variable name="v1" type="int" dir="In" required="true" />
    </variables>
    <body>
        <places>
            <place id="start" type="Start" />
            <place id="end" type="End" />
        </places>
        <tasks>

			<task id="T1" type="ReceiveMessage">
            </task>

			<task id="T2" type="Timer" splitType="AND">
            </task>

            <task id="T3_F" label="T1 error" type="Empty" splitType="AND">
			</task>

        </tasks>
        <flows>
            <flow from="start" to="T1" />
            <flow from="T1" to="T2" />
			<flow from="T1" taskOutPort="Error" to="T3_F" />
			<flow from="T3_F" to="end" />
			<flow from="T2" to="end" />
        </flows>
    </body>
</process>

Castle Windsor zamiast Spring

Poprzednia wersja NGinn została zbudowana w oparciu o kontener Spring. I to było dobre, Spring jest naprawdę świetnym narzędziem z doskonałą dokumentacją, przykładami oraz funkcjonalnością. Ale okazało się że wiele  komponentów które wykorzystuję w NGinn bazuje na innym kontenerze – Castle Windsor. Ponieważ używanie dwóch różnych kontenerów w jednym projekcie to trochę kiepski pomysł, postanowiłem przejść na Castle Windsor. Pierwsze próby były trudne, bo Windsor jest o wiele słabiej udokumentowany niż Spring a poza tym ma ewidentnie skromniejsze możliwości jeśli chodzi o konfigurację komponentów. Ale ma też dobre cechy – jest bardzo ‘lekki’ i nadaje się dobrze do konfiguracji programistycznej (Spring głównie poprzez XML). W dodatku jest dla niego rozszerzenie o nazwie Binsor, pozwalające konfigurować komponenty w oparciu o język Boo (Binsor jest przykładem DSL, czyli Domain Specific Language, zbudowanego w oparciu o Boo). Boo jest także wykorzystywany jako podstawowy język skryptowania procesów w NGinn – zatem decyzja mogła być tylko jedna: wchodzę w to. I na razie nie żałuję, choć pojawiło sie kilka trudności na horyzoncie – przede wszystkim brak możliwości wystawienia komponentów z kontenera poprzez .Net remoting tak jakbym chciał. Ale myślę że da się to łatwo przeskoczyć.

A co na horyzoncie: wykorzystanie prawdziwego ESB do przekazywania komunikatów w NGinn – flirtuję z projektem Rhino Service Bus. To on właśnie wymaga Castle Windsora. A poniżej można zobaczyć jak wygląda konfiguracja NGinn w Boo:


import System.IO
import Castle.Facilities.FactorySupport from Castle.MicroKernel
import Castle.Facilities.Startable from Castle.MicroKernel
import Rhino.ServiceBus.Impl from Rhino.ServiceBus
import NGinnBPM.Lib.Interfaces
import NGinnBPM.Lib.Interfaces.MessageBus
import NGinnBPM.Services
import NGinnBPM.Lib.Services
import NGinnBPM.Dao
import System.Collections.Generic

cfg = Kernel.Resolve(NGinnBPM.Lib.Util.DefaultConfigProvider)
baseDir = cfg.ResolveVariable("ng.configdir")
endp = cfg.ResolveVariable("NGinn.MessageQueue")
connstr = cfg.ResolveVariable("NGinn.ConnectionString")

Facility FactorySupportFacility
Facility StartableFacility
Facility ("remoting", Castle.Facilities.Remoting.RemotingFacility, {
	@isServer: true,
	@registryUri:   "nginn.kernel.rem",
	@remotingConfigurationFile: "NGinnBPM.Engine.WindsorHost.exe.config"
})

Facility("rhino.esb", RhinoServiceBusFacility, {
	bus: {
		@endpoint: endp,
		@threadCount: 1
	},
	messages: {
		add: {@name: "NGinnBPM.Runtime.Events", @endpoint: endp}
	}
})

Component("TaskInstanceFactory", ITaskInstanceFactory, TaskInstanceFactory)
Component("TaskDefinitionFactory", ITaskDefinitionFactory, TaskDefinitionFactory)
Component("ProcessScriptManager", IProcessScriptManager, NGinnBPM.Services.Scripting.BooScript.BooProcessScriptManager, LifestyleType.Singleton,
	BaseDirectory: "${baseDir}\\temp\\BooProcessScriptManager")
Component("TaskInstanceRepository", ITaskInstanceRepository, RawSqlTaskInstanceRepository, LifestyleType.Singleton,
	ConnectionString: connstr)
Component("MessageBus", IMessageBus, NGinnBPM.Services.MessageBus.SqlMessageHub, LifestyleType.Singleton,
	Endpoint: "sql://nginn/MQueue", ConnectionStrings: {"nginn": connstr})
Component("PackageRepository", IProcessPackageRepository, NGinnBPM.Services.FSProcessPackageRepository, LifestyleType.Singleton,
	BaseDirectory: "${baseDir}\\PackageRepository")
Component("MessageCorrelationResolver", IMessageCorrelationIdResolver, NGinnBPM.Dao.RawSqlMessageCorrelationIdResolver, LifestyleType.Singleton,
	ConnectionString: connstr)
Component("LockManager", IProcessInstanceLockManager, LocalProcessInstanceLockManager, LifestyleType.Singleton)

Component("Environment", INGEnvironment, NGinnBPM.Runtime.NGEnvironment, LifestyleType.Singleton)
Component("NGEngine", NGinnBPM.Runtime.NGEngine)

Konfigurując nasze komponenty w pliku konfiguracyjnym, czy jest to XML czy Boo, zyskujemy ważną rzecz – nie wprowadzamy zależności w kodzie, nawet gdy komponenty pochodzą z jakichś zewnętrznych assembly. Czyli nie musimy już mieć w kodzie miejsca które odwołuje się do wszystkich wykorzystywanych bibliotek, takie miejsce jest tylko w konfiguracji. I nie myślmy za dużo o tym że Boo to też kod, tylko w innym języku.

Operacja na żywym organiźmie czyli wprowadzanie zmian w procesach

Jeśli zajmujemy się tworzeniem oprogramowania na zamówienie, zwłaszcza oprogramowania używanego do obsługi jakichś procesów w firmach, wcześniej czy później stajemy przed koniecznością dokonywania zmian w istniejących funkcjach systemu. Największe problemy w takich przypadkach wynikają z niekompatybilnością wstecz – gdy nowa logika systemu jest istotnie różna od tej która była wcześniej, a dane zebrane w aplikacji do tej pory mają ’starą’ strukturę i trzeba je jakoś zaktualizować aby system mógł działać dalej po wdrożeniu zmian. Jeśli aplikacja jest intensywnie modyfikowana a nie zapewnia jakiegoś sensownego podejścia do aktualizacji to raczej szybko doprowadzimy do koszmarnej sytuacji kiedy 5% czasu poświęcamy na implementację zmiany, a pozostałe 95% na migrację istniejących danych oraz obsługę wygenerowanych przy tej okazji problemów. Dziś spróbuję podpowiedzeć jak wykorzystując NGinn można sobie ułatwić przyszły rozwój systemu.

W przypadku NGinn mówimy o aktualizacji procesów. Załóżmy że działamy w firmie i mamy zaimplementowany proces który jest w użyciu od dłuższego czasu – to oznacza że mamy mnóstwo spraw obsługiwanych tym procesem, część z tych spraw jest zakończona a część jest w trakcie realizacji na różnych etapach. Jeśli tak po prostu zmodyfikujemy definicję tego procesu to prawdopodobnie spowodujemy że toczące się sprawy staną się niekompatybilne z nową definicją i z powodu błędów nie będą mogły się zakończyć. Tego bardzo nie chcemy, bo jeśli to my dostarczamy oprogramowanie to po takim wdrożeniu zostaniemy zmuszeni do posprzątania całego bałaganu. O tej strategii nie będę więcej pisał, wszyscy wiedzą że można jej użyć w każdym przypadku ale lepiej wiedzieć co się robi. Przyjrzyjmy się lepiej innym sposobom:

1. Wersjonowanie procesu

NGinn pozwala używać kilku wersji definicji tego samego procesu (w tym celu jest numerek wersji w nazwie pliku z definicją), więc w wielu przypadkach najprościej jest zdefiniować nową wersję procesu która będzie używana dla spraw które dopiero zostaną zarejestrowane. Aktualnie toczące się sprawy pozostają na starej definicji procesu i mogą spokojnie się zakończyć ‘po staremu’. Unikamy w ten sposób jakiejkolwiek migracji danych więc jeśli uda się klienta przekonać do takiego rozwiązania sprawy to mamy sukces gwarantowany.

2. Anulowanie i ponowne uruchomienie

W niektórych przypadkach można rozwiązać problem aktualizacji poprzez anulowanie aktualnie toczących się spraw i ponowne ich uruchomienie już na nowej definicji procesu. Jak to zrobić w NGinn? Ano, narzędzia do tego nie ma, ale jest API pozwalające anulować procesy i je uruchamiać oraz ‘grzebać’ w danych procesów i zadań. Zatem można stworzyć sobie narzędzie które nam taką migrację obsłuży. Problem może wystąpić jeśli do uruchomienia procesu w nowej wersji konieczne są inne dane wejściowe – wtedy trzeba też dokonać konwersji i ewentualnego uzupełnienia danych. Istotna rzecz: NGinn nie zapamiętuje oryginalnych wartości danych wejściowych procesu – pamięta tylko ich aktualne wartości. Zatem jeśli podczas działania procesu zmienne wejściowe są modyfikowane to może się okazać że stracimy informacje potrzebne do uruchomienia tego samego procesu w nowej wersji. Taka sytuacja jednak jest dość rzadka, zwykle dane wejściowe istotne dla sprawy nie zmieniają się w trakcie jej realizacji.

3. Użycie Barier

Jeśli koniecznie chcemy aktualizować toczące się sprawy ‘na żywca’, to bardzo nam przeszkadza fakt że znajdują się one na rozmaitych etapach realizacji i trzeba to uwzględnić wprowadzając poprawkę – po prostu musi być ścieżka migracji dla każdego etapu na którym może się znajdować sprawa. W niektórych przypadkach nie trzeba nic robić – na przykład jeśli sprawa znajduje się na początku procesu, a my modyfikujemy jego końcówkę to nic złego się nie wydarzy. Zatem jeśli mieli byśmy taką sytuację że wszystkie sprawy znajdują się poza regionem procesu który modyfikujemy – tzn albo jeszcze do tego regionu nie doszły, albo już go opuściły – to zwykle migracja była by o wiele łatwiejsza albo wręcz niepotrzebna.

Żeby stworzyć taką możliwość w NGinn została wprowadzona funkcja “barier”. Bariera zatrzymuje nam działanie procesów w określonym miejscu i pozwala je wznowić po usunięciu bariery. Barierę stawiamy w wybranym miejscu, bądź miejscach, w procesie (chodzi o te kółeczka, czyli places).Jeśli w danym miejscu postawiona jest bariera to tokeny z tego miejsca nie mogą iść dalej, czyli nie jest uruchamiane zadanie wychodzące z tego miejsca. Tokeny czekają na usunięcie bariery czyli cała sprawa jest wstrzymana, natomiast sprawy które zdążyły pójść dalej zanim postawiliśmy barierę kontynuują proces bez przeszkód.

Jak tego użyć? Stawiamy barierę przed regionem w procesie który modyfikujemy i czekamy aż wszystkie sprawy opuszczą ten region (czyli tokeny we wszystkich instancjach tego procesu znajdą się poza modyfikowanym regionem). Bariera gwarantuje że żadne nowe tokeny nie napłyną więc oczyszczenie naszego pola działania jest tylko kwestią czasu. Wtedy zmieniamy definicję procesu i zdejmujemy barierę – wypuszczone sprawy potoczą się już wg zaktualizowanej definicji.

Czy to znaczy że właśnie opisałem stuprocentowo skuteczny sposób na aktualizację logiki systemu? No nie do końca… Przede wszystkim nie było mowy o danych. Co z tego że zaktualizujemy definicję procesu, kiedy w nowej wersji będą potrzebne dodatkowe dane (których przecież nie ma w aktualnie toczących się sprawach)? Nie będziemy w stanie ruszyć do przodu bez uzupełnienia tych danych. Do uzupełnienia czy modyfikacji danych musimy jednak zbudować własne narzędzie – NGinn daje do tego API, ale to jakie zmiany wprowadzić w danych musimy wymyślić sami. Bariera może się też tutaj przydać – jeśli sobie ’spiętrzymy’ sprawy w określonym miejscu to przynajmniej będziemy wiedzieli w których zadaniach należy zaktualizować dane.

Definicja procesu (4) – anulowanie zadań

Dzisiejszy artykuł będzie o sposobach anulowania zadań w procesie – to znaczy o tym, jak spowodować aby jakieś zadanie które jest w trakcie realizacji zostało anulowane po wystąpieniu określonego zdarzenia. Może nie jest to oczywiste na pierwszy rzut oka, ale taka funkcjonalność jest bardzo często potrzebna. Jednym z takich przypadków jest decyzja post factum (deferred choice), omówiona w tym artykule. Deferred choice jest jednak dość szczególnym przypadkiem stosowanym w określonej sytuacji, a w NGinn mamy do dyspozycji trochę więcej konstrukcji z anulowaniem.

Jako przykład na dziś weźmy sobie taki scenariusz ‘biznesowy’:

Do firmy wpływają reklamacje od klientów. Każda reklamacja powinna zostać obsłużona w ciągu 30 dni. Reklamacja jest wciągana do systemu ‘Trouble Ticketing’ i w tym systemie przypisywana do odpowiedniej jednostki organizacyjnej. Chcemy aby system automatycznie przypominał tym ludziom że zbliża się termin na rozpatrzenie reklamacji – na przykład poprzez wysłanie wiadomości email na 10 dni przed terminem rozpatrzenia, potem na 3 dni przed terminem no i  w momencie upływu terminu rozpatrzenia – wtedy z kopią do szefa działu obsługi reklamacji. Kiedy reklamacja zostanie rozpatrzona (niezależnie od tego z jakim wynikiem), system powinien zaprzestać wysyłania e-maili z eskalacjami.

Jest to prosty przykład i można go zaimplementować na wiele różnych sposobów – na przykład tak:

 

Proces reklamacji

Proces reklamacji

W momencie startu tego procesu wykonywane jest zadanie ‘fork’ – nie robi ono nic oprócz uruchomienia czterech równoległych ścieżek w procesie. Pierwsza ścieżka kończy się zadaniem ‘Obsłuż reklamację’ – jest to zlecenie obsługi reklamacji przekazywane do Działu Obsługi Reklamacji. Pozostałe trzy ścieżki odpowiadają za eskalacje. Na każdej z nich jest zadanie ‘Timeout’ odliczające odpowiedni okres czasu (odpowiednio 20, 27 i 30 dni od daty startu), po upływie tego czasu uruchamiane są zadania eskalacyjne wysyłające odpowiedniej treści email.

Bez użycia anulowania ścieżki te wykonują się niezależnie od siebie, czyli eskalacje będą wysyłane nawet gdy zadanie ‘Obsłuż reklamację’ się zakończy. Aby przerwać wysyłanie eskalacji po zakończeniu obsługi reklamacji wprowadzamy przejścia anulujące, oznaczone na rysunku czerwoną przerywaną linią. Każde takie przejście po zakończeniu zadania ‘Obsłuż reklamację’ zabiera wszystkie tokeny z miejsca do którego prowadzi (o ile te tokeny tam są) – w tym przypadku chodzi o miejsca e1, e2 i e3. Zabranie tokenów z miejsc e1, e2 i e3 oznacza że jeśli któreś z zadań ‘Timeout’ jest aktywne to straci ono wymagany token na wejściu i zostanie anulowane. Oczywiście jeśli któryś z Timeoutów już się wykonał to zjadł token ze swojego wejścia, wtedy przejście anulujące nie będzie miało żadnego efektu.  Osiągamy w ten sposób swój cel – po zakończeniu zadania ‘Obsłuż reklamację’, jeśli były jakieś oczekujące eskalacje to zostaną anulowane a proces będzie się mógł zakończyć bo już nie będzie tokenów poza miejscem końcowym.

Definicja naszego procesu wygląda tak:

<?xml version="1.0" encoding="windows-1250"?>
<process version="1" name="Reklamacje" xmlns="http://www.nginn.org/WorkflowDefinition.1_0.xsd" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <dataTypes>
    </dataTypes>
    <variables>
		<variable name="v1" type="int" dir="In" required="true" />
    </variables>
    <body>
        <places>
            <place id="start" type="Start" />
            <place id="end" type="End" />
			<place id="e1" />
			<place id="e2" />
			<place id="e3" />
        </places>
        <tasks>
            <task id="T0" type="Empty" splitType="AND" label="fork">
            </task>

			<task id="T1" type="Manual" label="Obsluz reklamacje">
				<parameters>
				</parameters>
            </task>

			<task id="TE1" type="Timer" label="Timeout - 10 dni">
				<parameters>
					<param variable="ExpirationDate"><expr>DateTime.Now.AddDays(20)</expr></param>
				</parameters>
            </task>

			<task id="TE2" type="Timer" label="Timeout - 3 dni">
				<parameters>
					<param variable="ExpirationDate"><expr>DateTime.Now.AddDays(27)</expr></param>
				</parameters>
            </task>

			<task id="TE3" type="Timer" label="Timeout">
				<parameters>
					<param variable="ExpirationDate"><expr>DateTime.Now.AddDays(30)</expr></param>
				</parameters>
            </task>

            <task id="ESK1" label="Eskalacja 1" type="SendNotification" splitType="AND">
			</task>

			<task id="ESK2" label="Eskalacja 2" type="SendNotification" splitType="AND">
			</task>

			<task id="ESK3" label="Eskalacja 3" type="SendNotification" splitType="AND">
			</task>

        </tasks>
        <flows>
            <flow from="start" to="T0" />
            <flow from="T0" to="T1" />
			<flow from="T0" to="e1" />
			<flow from="T0" to="e2" />
			<flow from="T0" to="e3" />
			<flow from="T1" to="e1" cancelling="true" />
			<flow from="T1" to="e2" cancelling="true" />
			<flow from="T1" to="e3" cancelling="true" />
			<flow from="e1" to="TE1" />
			<flow from="e2" to="TE2" />
			<flow from="e3" to="TE3" />

			<flow from="TE1" to="ESK1" />
			<flow from="TE2" to="ESK2" />
			<flow from="TE3" to="ESK3" />
			<flow from="ESK1" to="end" />
			<flow from="ESK2" to="end" />
			<flow from="ESK3" to="end" />
			<flow from="T1" to="end" />
        </flows>
    </body>
</process>

Przejścia anulujące są zapisane w sekcji ‘flows’, to te z atrybutem cancelling=”true”. Mam nadzieję że ten przykład odpowiednio ilustruje przeznaczenie przejść anulujących.

Dzisiejszy przykład pokazuje jeszcze jedną fajną rzecz. Wyobraźmy sobie że zamiast zadania ‘Obsłuż reklamację’ mamy tam cały podproces obsługi reklamacji – z etapami, przechodzeniem przez różne działy, wysyłaniem pism itp. Widać wtedy, że gdy chcemy do takiego procesu dodać eskalacje możemy to zrobić w ogóle w niego nie ingerując – procedura eskalacyjna jest zupełnie zewnętrzna w stosunku do procesu nią objętego.

Zadania atomowe i blokowe. Składnia definicji procesu (3)

simple

Jakiś czas temu próbowałem krótko wyjaśnić z czego składa się proces w NGinn. Dziś kolejne przybliżenie tego tematu – zajmiemy się konstruowaniem prostego procesu. Najprostszy proces w NGinn to taki który nic nie robi. Ale nawet żeby nic nie robić to trzeba jednak trochę się napracować.

Zaczniemy od tego że każdy proces musi mieć miejsce startowe oraz miejsce końcowe (lub kilka miejsc końcowych). A ponieważ nie można łączyć miejsca  z miejscem, to pomiędzy nimi musi znaleźć się jakieś zadanie. Nasz najprostszy proces można podziwiać obok.

Definicja tego procesu to następujący XML:


<?xml version="1.0" encoding="utf-8"?>
<process version="1" name="Proc" xmlns="http://www.nginn.org/WorkflowDefinition.1_0.xsd">
    <body>
        <places>
            <place id="start" type="Start" />
            <place id="end" type="End" />
        </places>
        <tasks>
            <task id="T1" type="Empty">
            </task>
        </tasks>
        <flows>
            <flow from="start" to="T1" />
            <flow from="T1" to="end" />
        </flows>
    </body>
</process>

Mamy tu sekcję ‘body’ zawierającą kolejno:

  • miejsca (sekcja ‘places’)
  • zadania (sekcja ‘tasks’)
  • połączenia (sekcja ‘flows’)

Ważne jest to, że zarówno miejsca jak i zadania mają atrybut ‘id’ unikalnie identyfikujący miejsce/zadanie w procesie. Teraz rozbudujmy ten przykład:

 

Przykład 2

Przykład 2

Jest to proces zawierający trzy zadania, z czego jedno jest tzw zadaniem blokowym (composite), czyli takim które zawiera wewnątrz podproces. Definicja tego procesu wygląda tak:


<?xml version="1.0" encoding="utf-8"?>
<process version="1" name="Proc" xmlns="http://www.nginn.org/WorkflowDefinition.1_0.xsd">
    <body>
        <places>
            <place id="start" type="Start" />
            <place id="end" type="End" />
        </places>
        <tasks>
            <task id="T1" label="30 min" type="Timer">
				<parameters>
					<param variable="DelayAmount"><expr>TimeSpan.FromSeconds(30)</expr></param>
				</parameters>
			</task>

			<task id="T2" type="Empty">
            </task>

			<composite id="T3" label="">
				<body>
					<places>
                        <place id="T3_start" type="Start" />
                        <place id="T3_end" type="End" />
                    </places>
                    <tasks>
                        <task id="T31" type="Timer">
                        	<parameters>
								<param variable="DelayAmount"><expr>TimeSpan.FromMinutes(1)</expr></param>
							</parameters>
						</task>
                    </tasks>
                    <flows>
                        <flow from="T3_start" to="T31" />
                        <flow from="T31" to="T3_end" />
                    </flows>
				</body>
			</composite>

        </tasks>
        <flows>
            <flow from="start" to="T1" />
			<flow from="T1" to="T2" />
			<flow from="T1" to="T3" />
			<flow from="T2" to="end" />
			<flow from="T3" to="end" />
        </flows>
    </body>
</process>

W tym procesie występuje kilka istotnych szczegółów. Przede wszystkim, w sekcji ‘tasks’ pojawiło się zadanie ‘composite’ – to jest właśnie zadanie blokowe. Wewnętrzna struktura tego zadania jest taka sama jak procesu, czyli zawiera sekcję ‘body’ a w niej miejsca, zadania i połączenia. Oczywiście dalsze zagnieżdżanie jest możliwe – nie ma ograniczenia na głębokość zagnieżdżenia zadań blokowych. W odróżnieniu od blokowych, zadania nie zawierające innych zadań określamy mianem atomowych (tj niepodzielnych) – są to te definiowane poprzez ‘task’.

No właśnie, druga sprawa to task typu ‘Timer’. Jest to zadanie odliczające podany okres czasu. Dla tego zadania podane jest to w parametrze o nazwie ‘DelayAmount’ a wartość parametru jest w formie wyrażenia wyliczanego przed uruchomieniem zadania. Każdy typ zadania w NGinn posiada określony zestaw parametrów – wszystkie są ustawiane w ten sam sposób.

Teraz pora dołożyć do naszego procesu jakieś dane. Robi się to za pomocą sekcji ‘variables’ – omawianej już wcześniej w tym wpisie. Zatem dołóżmy parę zmiennych do naszego procesu oraz zadań, a potem zdefiniujmy mapowania między zmiennymi procesu a zmiennymi zadań.


<process version="1" name="Proc" xmlns="http://www.nginn.org/WorkflowDefinition.1_0.xsd">
	<variables>
		<variable name="userName" type="string" dir="In" required="true" />
		<variable name="accountId" type="string" dir="Out" required="true" />
    </variables>
    <body>
        <places>
            <place id="start" type="Start" />
            <place id="end" type="End" />
        </places>
        <tasks>
            <task id="T1" label="30 min" type="Timer">
				<parameters>
					<param variable="DelayAmount"><expr>TimeSpan.FromSeconds(30)</expr></param>
				</parameters>
			</task>

			<task id="T2" type="Empty">
				<variables>
					<variable name="v1" type="string" dir="In" required="true" />
					<variable name="v2" type="string" dir="Out" required="true" />
				</variables>
				<inputBindings>
					<bind variable="v1"><sourceVar>userName</sourceVar></bind>
				</inputBindings>
				<outputBindings>
					<bind variable="accountId"><expr>data.v2 + '_XYZ'</expr></bind>
				</outputBindings>
            </task>

Dalszą część definicji obciąłem – jest taka sama jak wyżej. Mamy tu zdefiniowane dwie zmienne procesu – zmienną wejściową userName typu string, oraz zmienną wyjściową accountId też typu string. Kolejny ważny szczegół to zmienne zadania ‘T1′ – ma ono dwie zmienne, wejściową v1 oraz wyjściową v2. Dodatkowo, dla tego zadania są zdefiniowane mapowania pomiędzy jego zmiennymi a zmiennymi procesu. Mapowanie wejściowe (inputBinding) mówi że zmienna v1 otrzyma kopię zmiennej userName z procesu – odbędzie się to przy starcie zadania T1. Po zakończeniu zadania T1 natomiast zmienna v2 (wyjściowa) zostanie przedłużona o napis ‘_XYZ’ i przypisana do zmiennej accountId w procesie – mówi o tym mapowanie danych wyjściowych (outputBinding). 

Widzimy zatem jak działa wymiana danych – używamy mapowania danych wejściowych aby ustalić wartości zmiennych wejściowych zadania przy jego starcie, a po zakończeniu zadania wykorzystujemy mapowanie zmiennych wyjściowych na zmienne procesu lub zadania nadrzędnego. To działa rekurencyjnie – jeśli mamy zadanie blokowe to pod-zadania w nim zawarte muszą mapować swoje zmienne na zmienne zadania blokowego, a nie na zmienne procesu – po prostu występuje pełna izolacja danych w zadaniach i nie można sięgać do zmiennych innych zadań. Jedyna dozwolona wymiana danych odbywa się poprzez mapowania i dotyczy wyłącznie zmiennych zadania nadrzędnego lub procesu jeśli działamy bezpośrednio na poziomie procesu.

Kolejnym krokiem powinno być zbadanie jakie rodzaje zadań mamy do dyspozycji w NGinn. Będzie to opisane w kolejnych artykułach, gdyż w tej chwili zestaw zadań w NGinn nie jest jeszcze kompletny. Postaram się też wymyślić jakiś bliższy rzeczywistości przykład procesu do którego stworzenia wykorzystamy nowo poznane zadania.