Archive for 2009

Wdrożenie NGinn – case study

NGinn nie tak dawno temu przeszedł swój chrzest bojowy – mianowicie doczekał się wdrożenia produkcyjnego i to na całkiem sporą skalę. Ponieważ od wdrożenia minęło już trochę czasu a system jest ustabilizowany i można spokojnie powiedzieć że działa, to dziś opowiem nieco o tym jak owo wdrożenie wyglądało i jaka jest w nim rola NGinn.

Krótko o procesie

NGinn został użyty do zautomatyzowania procesu indeksowania dokumentów w firmie telekomunikacyjnej która jest jednym z operatorów komórkowych w Polsce. Indeksowanie dokumentów jest to jeden z etapów procesu obsługi wchodzących do firmy dokumentów papierowych (listów, umów i innych dokumentów dotyczących klientów firmy), który przedstawia się mniej więcej tak:

  1. Przyjęcie dokumentów w kancelarii
  2. Sortowanie (wstępna klasyfikacja)
  3. Przygotowanie do skanowania (rozpakowanie, rozszycie etc)
  4. Skanowanie
  5. Indeksacja (klasyfikacja, opisanie dokumentu odpowiednimi danymi)
  6. Umieszczenie dokumentu w archiwum elektronicznym
  7. Dalsza obsługa dokumentu już w wersji elektronicznej (dalsze etapy są realizowane w innych systemach, proces obsługi jest różny dla różnych dokumentów)

W skrócie proces ten prowadzi do przekształcenia dokumentu na jego obraz cyfrowy (skan) z odpowiednimi metadanymi, dzięki czemu dalsza obsługa dokumentu może być prowadzona za pomocą systemów informatycznych. Firma dąży do maksymalnego zautomatyzowania tego procesu gdyż obsługuje dziesiątki tysięcy dokumentów dziennie i każda czynność ręczna generuje duże koszty (w obsługę wchodzących dokumentów zaangażowane jest kilkadziesiąt osób).

Celem naszego wdrożenia było zautomatyzowanie etapu indeksacji – czyli etapu w którym osoba skanująca dokument opisuje go odpowiednimi metadanymi. Te metadane zależą od rodzaju dokumentu, ale najczęściej są to podstawowe informacje jak rodzaj dokumentu, data wpłynięcia do firmy, data skanowania, identyfikator klienta, nr telefonu tego klienta, nr dokumentu, kod kreskowy, segment klienta i kilka innych atrybutów. Do tej pory musiały być odczytywane z dokumentu (lub wyszukiwane w innych systemach) i wpisywane ręcznie przez pracownika skanującego dokument, co miało swoje wady: wydłużało czas obsługi dokumentu i stwarzało ryzyko wystąpienia błędów w danych. W celu poprawienia efektywności tego etapu firma opracowała jego wersję zautomatyzowaną  w której praca ręczna została albo całkowicie wyeliminowana, albo ograniczona do minimum. Automatyzacja polegała na zmianie aplikacji skanującej tak aby sama wypełniała pola które może (datę skanowania, login osoby skanującej, kod kreskowy i kilka innych), zaś reszta danych pozyskiwana jest automatycznie z systemów zewnętrznych. Na przykład: jeśli na dokumencie występuje kod kreskowy z systemu sprzedaży to  dane klienta są pobierane z tego systemu. Jeśli kodu kreskowego brak to konsultant ma za zadanie wpisać ręcznie tylko identyfikator klienta – pozostałe dane klienta zostaną pobrane z systemu billingowego. Konsultant musi interweniować w sytuacjach wyjątkowych, np jeśli np kod kreskowy jest nieczytelny dla aplikacji skanującej (wtedy albo wpisuje go ręcznie albo nakleja nowy kod i podaje numer dokumentu). Procedura zautomatyzowana obejmuje również walidację danych (wykrywanie brakujących lub błędnych numerów klienta/numerów dokumentu/kodów kreskowych). W przypadku wykrycia nieprawidłowości odpowiedni pracownicy otrzymują zadanie poprawy indeksów w dokumencie.

Oszczędność na pojedynczym dokumencie jest rzędu minuty-dwóch. Ale po pomnożeniu tego przez ok 10-15 tysięcy dokumentów dziennie wychodzi nam oszczędność na poziomie trzydziestu osobo-dni (każdego dnia). Oznacza to trzydzieści etatów których nie trzeba tworzyć – czyli niewielkie w sumie usprawnienie w tak masowym procesie daje całkiem niezły efekt finansowy.

Realizacja

Do zrealizowania tej zautomatyzowanej procedury indeksacji użyliśmy NGinn. Działa to tak że po zeskanowaniu dokumentu jest on umieszczany w systemie EDMS (elektroniczne archiwum dokumentów), który to system powiadamia NGinn o pojawieniu się nowego dokumentu. W tym momencie NGinn uruchamia proces automatycznej indeksacji który odpowiada za uzupełnienie dokumentu o odpowiednie metadane. Oto schemat tego procesu:

indeksacja

  1. Pobranie dokumentu z systemu EDMS.
    Po zeskanowaniu dokumentu i umieszczeniu go w systemie EDMS system ten powiadamia NGinn o pojawieniu się nowego dokumentu przekazując wewnętrzny identyfikator tego dokumentu. Na podstawie tego identyfikatora NGinn pobiera dane dokumentu z systemu EDMS. Pobierane są m. in. typ dokumentu, nr dokumentu, kod kreskowy i identyfikator klienta.
  2. Pobranie danych klienta z systemu bilingowego. Jeśli dokument zawiera identyfikator klienta a nie zawiera kodu kreskowego, odpytujemy system bilingowy o dane klienta na podstawie podanego numeru klienta. Odpytanie odbywa się poprzez wywołanie odpowiedniego web service. Jeśli dane klienta zostaną odnalezione idziemy do kroku 4, w przeciwnym razie do kroku 5.
  3. Pobranie danych dokumentu z systemu sprzedaży – dokumenty z systemu sprzedaży zaopatrzone są w odpowiedni kod kreskowy, identyfikowane są też numerem dokumentu. NGinn odpytuje system sprzedażowy o dane tego dokumentu na podstawie kodu kreskowego lub nr dokumentu, wśród danych zwracanych przez system sprzedażowy są również informacje o kliencie. Jeśli dokument zostanie odnaleziony przechodzimy do kroku 4, w przeciwnym razie do kroku 5.
  4. Aktualizacja dokumentu w EDMS. Jeśli dane klienta/dokumentu zostały odnalezione w odpowiednim systemie, NGinn aktualizuje dane dokumentu w EDMS dzięki czemu jest on opisany pełnymi metadanymi.
  5. Zadanie ‘popraw indeksy’. Jest to zadanie ręczne zakładane w systemie obsługi zgłoszeń. Zadanie to pojawia się na liście spraw odpowiedniej grupy konsultantów (system obsługi zgłoszeń jest uniwersalnym systemem w którym rejestrowane są zgłoszenia klientów oraz inne rodzaje spraw, m.in. zadania). Zadanie zawiera odnośnik do dokumentu w EDMS oraz informacje o tym które indeksy należy poprawić. Po poprawieniu indeksów konsultant zgłasza zakończenie realizacji zadania i proces powtarza się od kroku 1. Istnieje możliwość zakończenia zadania z opcją ‘pomiń walidację’, która powoduje przejście do kroku 6 bez dalszej walidacji danych dokumentu – jest to potrzebne w wyjątkowych przypadkach, np gdy dane klienta w na dokumencie są nieaktualne.
  6. Poprawny przebieg procesu indeksacji kończy się przekazaniem dokumentu do systemu obsługi zgłoszeń. NGinn przekazuje dane dokumentu do systemu obsługi zgłoszeń, który na podstawie tych danych zakłada odpowiedniej treści zgłoszenie i przypisuje je do Back Office, do grupy konsultantów odpowiedzialnych za obsługę danego rodzaju zgłoszeń. NGinn na kroku 6 kończy swoją rolę i nie bierze udziału w obsłudze zgłoszenia.

W celu zaimplementowania tego procesu do standardowego zestawu zadań NGinn musiały zostać dołączone dwa typy zadań służące do integracji z systemem EDMS: zadanie “Pobierz dokument z EDMS” (krok 1) oraz “Zaktualizuj dokument EDMS”, czyli krok 4. Zadania te wykorzystują API integracyjne systemu EDMS. Pozostałe etapy procesu zostały zrealizowane za pomocą zadań standardowo udostępnianych przez NGinn. Dodatkowo, w systemie obsługi zgłoszeń został dodany interfejs integracyjny dla NGinn pozwalający NGinn zakładać zadania w tym systemie oraz raportować do NGinn zakończenie realizacji zadań przez konsultantów.

Jak łatwo policzyć, proces indeksacji wykorzystuje funkcje czterech różnych systemów: EDMS, billingu, systemu sprzedażowego oraz systemu obsługi zgłoszeń. Można powiedzieć zatem ze oprócz funkcji narzędzia ‘BPM’ NGinn pełni też rolę narzędzia integracyjnego zarządzającego przepływem danych między poszczególnymi aplikacjami.

Przebieg wdrożenia

Projekt był realizowany metodą szybkiego prototypowania. Klient bardzo szybko (w ciągu 2 tygodni) od rozpoczęcia prac otrzymał pierwszą wersję oprogramowania do testów. Podczas testów oprócz pewnej liczby błędów wykryto też rozmaite przypadki nietypowe które nie były przewidziane w wymaganiach. Każdy taki wyjątek wymagał modyfikacji procesu – praktycznie na bieżąco dostarczaliśmy klientowi zmodyfikowaną wersję procesu i testy mogły być kontynuowane. W sumie testowane było 7 albo 8 wersji procesu, każda kolejna zawierała obsługę przypadków nie przewidzianych przez poprzednią iterację. Testy trwały około 3 tygodni, później nastąpiło przeniesienie funkcjonalności na środowisko produkcyjne. W środowisku produkcyjnym wykryto jeszcze jeden albo dwa ‘nietypowe’ przypadki wymagające pilnego obsłużenia, co zaowocowało dwiema kolejnymi wersjami procesu.

Podczas testowania niektóre z cech NGinn okazały się bardzo przydatne:

  • możliwość używania kilku wersji procesu jednocześnie. W przypadku testów ‘na produkcji’ używana była stabilna wersja procesu zaś eksperymentalne były uruchamiane tylko na potrzeby testów. Po przetestowaniu nastąpiło przełączenie domyślnej wersji procesu na nową według której były obsługiwane kolejne dokumenty. Nie było potrzeby migracji procesów w toku – dokończyły one swój bieg według starej wersji.
  • zmiany definicji procesu ‘w locie’, bez zatrzymywania systemu pozwalały na bieżąco uwzględniać uwagi testerów a na produkcji przełączać się między wersjami procesu w sposób niezauważalny dla użytkowników

Po wdrożeniu

Obserwacja systemu po wdrożeniu pokazała kilka faktów:

  • Mimo niezbyt rozbudowanej konfiguracji serwera nie ma żadnych problemów wydajnościowych, NGinn obciąża procesor i pamięć w niewielkim stopniu. Baza danych bez problemu radzi sobie z obsługą danych generowanych przez NGinn.
  • Po ustabilizowaniu definicji procesu system działa praktycznie bezproblemowo 24 godziny na dobę,  7 dni w tygodniu. Przez ponad miesiąc nie było żadnego problemu technicznego, wszystkie dokumenty zostały zaindeksowane zgodnie z definicją procesu. Mam doświadczenie w pracy z różnymi systemami i naprawdę rzadko zdarza się żeby system działał tak stabilnie zaraz po uruchomieniu produkcyjnym.
  • NGinn jest bardzo odporny na awarie systemów zewnętrznych. W naszym procesie indeksacji biorą udział cztery systemy i dość często zdarza się że któryś z nich na jakiś czas ‘wypada’, np nie można się z nim połączyć lub zgłasza błędy. W takiej sytuacji NGinn jest w stanie przeczekać okres niedostępności systemu i wznowić procesy kiedy system zewnętrzny zacznie odpowiadać. Dzieje się tak dzięki mechanizmowi ponawiania w NGinn który przez okres max. 3 dni próbuje ponownie wykonać nieudane operacje. Oznacza to też że po ustąpieniu awarii administrator systemu nie musi nic robić żeby obsługa procesów w NGinn była kontynuowana.
  • Standardowo NGinn przechowuje stan wszystkich zadań w bazie danych. Informacje te nie są usuwane po zakończeniu zadania, dlatego ilość gromadzonych danych jest bardzo duża. Proces indeksacji składa się z 6-ciu zadań, co prawda nie wszystkie występują w każdym przebiegu, ale oznacza to że dla każdego indeksowanego dokumentu zapisywane jest 3-6 rekordów w bazie NGinn, co dziennie daje 50-100 tysięcy rekordów. Konieczny jest jakiś mechanizm czyszczący który będzie usuwał stan zakończonych zadań.
  • Przy dużej ilości przetwarzanych dokumentów potrzebne jest narzędzie monitorujące pozwalające śledzić pracę NGinn i wykrywać ewentualne problemy. Dobrze by też było mieć jakieś narzędzie pozwalające na raportowanie przebiegu procesów. Na potrzeby tego konkretnie wdrożenia zostało uruchomione niezbyt rozbudowane narzędzie pozwalające śledzić podstawowe informacje – np aktywność w NGinn w ciągu ostatnich 24 godzin – poniżej obrazek z tego narzędzia:

procesy_24h

Podsumowując, myślę że udało się pokazać że za pomocą NGinn można realizować rzeczywiste wdrożenia i narzędzie to potrafi wspierać procesy biznesowe nie tylko na papierze. Mam nadzieję na uruchomienie kolejnych procesów dla tego samego klienta i myślę że to pozwoli rozbudować NGinn o kolejne przydatne funkcje.

Jak wywołać dowolny web service bez WSDL i bez generowania kodu

W dzisiejszym artykule zajmiemy się wywoływaniem web services z poziomu procesu NGinn, przy okazji poznając alternatywny sposób korzystania z web services. Jak pewnie doskonale wszyscy wiedzą, web services są komponentami (uslugami) dostępnymi zdalnie, za pośrednictwem niezależnego od platformy protokołu SOAP. Wywołanie web service jest niczym innym jak przekazaniem komunikatu SOAP oraz odebraniem odpowiedzi (też SOAP), najczęściej za pomocą protokołu http.
Jednak gdy programista chce w swojej aplikacji wywołać jakiś web service zwykle nie posługuje się on bezpośrednio SOAPem, ale używa dostępnych w danym języku narzędzi ‘opakowujących’ które pozwalają mu wywoływać web service tak jakby to było wywołanie lokalnej funkcji . W Microsoft.Net posługujemy się na przykład narzędziem WSDL.exe które na podstawie XML-owego opisu usługi (WSDL) generuje nam kod w C#  (lub VB) który opakowuje nam web service do postaci ‘strawnej’ dla .Net, czyli obiektu określonej klasy z interfejsem zgodnym z definicją web service. Jest to na tyle wygodne i uniwersalne że w typowych zastosowaniach możemy polegać tylko na wsdl.exe i w ogóle zapomnieć o XML-u i http pod spodem. Ale czasami wsdl.exe nie wystarcza – na przykład gdy nie wiemy z góry jaki web service będziemy wywoływać.

WSDL.exe generuje kod opakowujący web service, co oznacza że struktura web service jest wkompilowana w program który z danej usługi korzysta – i gdy usługa się zmieni to należy ponownie wygenerować kod wsdl.exe’m i skompilować aplikację od nowa. Świetnie, ale gdy aplikacja musi się komunikować z web service którego nie znamy na etapie kompilacji to WSDL.exe do niczego się nie przyda.  NGinn jest właśnie takim przypadkiem ponieważ oferuje możliwość wywoływania web services w ramach procesów. Wszelkie potrzebne do wywołania informacje czerpie z definicji procesu a na podstawie tych informacji tworzy  i wysyła komunikaty SOAP.

Żeby wywołać web service z procesu NGinn trzeba posłużyć się zadaniem ‘XmlHttp’. Jest to zadanie pozwalające na wykonywanie wywołań http, między innymi takich które zawierają XML. Żeby wywołać web service trzeba tylko zadbać o to aby ten XML był SOAP-em o strukturze której spodziewa się nasz web service – czyli zapewnić ‘binding’ między danymi na których operuje nasz proces a odpowiednimi polami w komunikacie SOAP. Na aktualnym etapie rozwoju NGinn robi się to za pomocą transformacji XSLT. Żeby było łatwiej to wszystko przyswoić posłużmy się przykładem. Jest taka publicznie dostępna usługa http://aspspider.info/trutech/MarketWatch.asmx która pozwala m.in . zapytać się o kursy akcji, indeksy giełdowe i kursy metali szlachetnych.  Udostępnia ona operację ‘GetQuote’ za pomocą której będziemy odpytywać się o kursy akcji. Po wejściu na w/w stronę ukazuje się nam opis usługi (standardowo wygenerowany przez ASP.Net) z WSDL oraz z przykładami komunikatów SOAP. No i przykładowy komunikat SOAP dla zapytania o kurs akcji Microsoftu wygląda tak:

[xml]
<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<GetQuote xmlns="http://trutech.com/marketwatch/">
<credValidate>
<Email>trutech.shared@gmail.com</Email>
<Password>trutech.shared@gmail.com</Password>
</credValidate>
<MyScript>
<sScriptCode>MSFT</sScriptCode>
</MyScript>
</GetQuote>
</soap12:Body>
</soap12:Envelope>
[/xml]
Gdy wyślemy taki XML POST-em na adres http://aspspider.info/trutech/MarketWatch.asmx dostaniemy odpowiedź która wygląda mniej-więcej tak:
[xml]
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<GetQuoteResponse xmlns="http://trutech.com/marketwatch/">
<GetQuoteResult>true</GetQuoteResult>
<MyScript>
<sScriptCode>MSFT</sScriptCode>
<sScriptName>MSFT – Microsoft Corpora</sScriptName>
<sScriptNameFromServer>Microsoft Corpora</sScriptNameFromServer>
<sScriptStockExchange>NasdaqNM</sScriptStockExchange>
<sScriptLastPrice>28.22 Rs</sScriptLastPrice>
<sScriptPreviousClose>28.22 Rs</sScriptPreviousClose>
<sScriptChangeInRupees>0.00 Rs</sScriptChangeInRupees>
<sScriptChangeInPercentage>0.00 %</sScriptChangeInPercentage>
<sScriptVolume>0 Sh</sScriptVolume>
<sScriptDaysRange>N/A – N/A</sScriptDaysRange>
<sScriptYearRange>14.87 Rs – 29.35 Rs</sScriptYearRange>
<sScriptToolTipText>
Registration Code : MSFT
Company Name : MSFT – Microsoft Corpora
Exchange : NasdaqNM ( 2009-10-29 )
Last Price : 28.22 Rs
Previous Close : 28.22 Rs
Change (INR) : 0.00 Rs
Change (%) : 0.00 %
Trading Volume : 0 Shares
Price Range (Day) : N/A – N/A
Price Range (52 Weeks) : 14.87 Rs – 29.35 Rs
Query Time : 2009-10-30 17:02:57
</sScriptToolTipText>
<sScriptWebLink>http://in.finance.yahoo.com/q?s=MSFT</sScriptWebLink>
<sScriptLastTradeDate>2009-10-29</sScriptLastTradeDate>
<sScriptLastTradeTime>4:00pm</sScriptLastTradeTime>
<sScriptQueryDateTime>2009-10-30 17:02:57</sScriptQueryDateTime>
<bScriptDataFound>true</bScriptDataFound>
<lScriptRowId>0</lScriptRowId>
</MyScript>
</GetQuoteResponse>
</soap:Body>
</soap:Envelope>
[/xml]

Interesująca nas cena jest w polu ‘sScriptLastPrice’. Niektórzy pewnie się zastanawiają co oznacza ‘Rs’ obok ceny – otóż jest to indyjski web service podający ceny w Rupiach. Bardzo przepraszam ale innego nie znalazłem.

Skoro już wiemy jak wyciągnąć cenę akcji Microsoftu w Rupiach to zbudujmy proces NGinn który dokładnie to robi – tj na wejściu bierze symbol spółki giełdowej i odpytuje się o cenę akcji tej spółki. Proces ten będzie miał jedną zmienną wejściową – symbol spółki, i jedną zmienną wyjściową – kurs akcji wyrażony w rupiach.

[xml]
<?xml version="1.0" encoding="utf-8"?>
<process version="1" name="RupeeStockQuote" xmlns="http://www.nginn.org/WorkflowDefinition.1_0.xsd">
<variables>
<variable name="symbol" type="string" required="true" dir="In" />
<variable name="price" type="string" required="true" dir="Out" />
</variables>
<body>
<places>
<place id="start" type="Start" />
<place id="end" type="End" />
</places>
<tasks>
<task id="T1" type="XmlHttp">
<variables>
<variable name="symbol" type="string" dir="In" required="true"/>
<variable name="price" type="string" required="true" dir="Out" />
</variables>
<parameters>
<param variable="Url"><value>http://aspspider.info/trutech/MarketWatch.asmx</value></param>
<param variable="RequestType"><value>SOAP</value></param>
<param variable="ResponseType"><value>SOAP</value></param>
<param variable="HttpMethod"><value>POST</value></param>
<param variable="RequestBodyXsl"><value>GetQuote_Request.xsl</value></param>
<param variable="ResponseBodyXsl"><value>GetQuote_Response.xsl</value></param>
<param variable="RequestHeaders"><expr>{"Content-Type": "application/soap+xml; charset=utf-8"}</expr></param>
</parameters>
</task>
</tasks>
<flows>
<flow from="start" to="T1" />
<flow from="T1" to="end" />
</flows>
</body>
</process>
[/xml]

Proces ten jest bardzo uproszczony – ma tylko jedno zadanie. W normalnej sytuacji miałby on dalszy ciąg, czyli logikę która w jakiś sposób działa w oparciu o cenę akcji i np. wysyła do kogoś powiadomienie. Ale to tylko przykład więc musi być prosty.

Widać tu że zadanie ‘T1’ posługuje się dwiema transformacjami XSL – pierwsza z nich jest zapisana w pliku ‘GetQuote_Request.xsl’ i służy do wygenerowania komunikatu SOAP wywołującego web service, druga, w pliku ‘GetQuote_Response.xsl’ służy do przetworzenia odpowiedzi web service na postać zrozumiałą dla NGinn. Oto te transformacje:

GetQuote_Request.xsl
[xml]
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="utf-8" indent="yes" />
<xsl:template match="/data">
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<GetQuote xmlns="http://trutech.com/marketwatch/">
<credValidate>
<Email>trutech.shared@gmail.com</Email>
<Password>trutech.shared@gmail.com</Password>
</credValidate>
<MyScript>
<sScriptCode><xsl:value-of select="symbol" /></sScriptCode>
</MyScript>
</GetQuote>
</soap12:Body>
</soap12:Envelope>
</xsl:template>
</xsl:stylesheet>
[/xml]

Na wejściu dostaje ona XML zawierający dane zadania ‘T1’, czyli coś w rodzaju

[xml]<data><symbol>MSFT</symbol></data>[/xml]

Na wyjściu natomiast powstaje komunikat SOAP o strukturze opisanej wcześniej – nasza transformacja po prostu wstawia zmienną ‘symbol’ w polu ‘sScriptCode’. Po wygenerowaniu komunikatu SOAP zadanie T1 wysyła go w treści requestu  ‘POST’ pod adres web service. Następnie odbiera odpowiedź, czyli opisany wyżej komunikat ‘GetQuoteResponse’, który z kolei poddaje transformacji ‘GetQuote_Response.xsl’:

[xml]
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:quot="http://trutech.com/marketwatch/">
<xsl:output method="xml" encoding="utf-8" indent="yes" />

<xsl:template match="/soap:Envelope/soap:Body/quot:GetQuoteResponse">
<data>
<price><xsl:value-of select="quot:MyScript/quot:sScriptLastPrice" /></price>
</data>
</xsl:template>
</xsl:stylesheet>
[/xml]

Widać że na wyjściu otrzymamy bardzo prosty XML w rodzaju

[xml]<data><price>28.08 Rs</price></data>[/xml]

NGinn na podstawie tego XML zaktualizuje zmienne zadania T1, czyli przypisze otrzymaną od web service wartość kursu akcji do zmiennej ‘price’. Na tym zadanie ‘T1’ się kończy,  a razem z nim cały przykładowy proces.

Jest to bardzo elastyczna metoda wywoływania web services, mamy praktycznie całkowitą kontrolę nad strukturą przekazywanych komunikatów. Niestety, dzieje się to kosztem dość żmudnego przygotowywania XSL-i – nie ma żadnego narzędzia które by to zrobiło za nas (pewnie takie narzędzie korzystało by z WSDL, my nie musimy bo mamy przykłady XML-i). Warto tu wspomnieć o narzędziach które przydają się do tworzenia w/w xsl-i i w ogóle do testów web services. Pierwszym z tych narzędzi jest pakiet ‘Curl’ (http://curl.haxx.se/ ) umożliwiający wysyłanie po http dowolnych żądań (a w szczególności naszych komunikatów SOAP). Drugim  jest pakiet ‘nxslt’ (http://www.xmllab.net/downloads/nxslt/) umożliwiający testowanie działania transformacji XSLT. Posługując się tymi dwoma narzędziami jesteśmy w stanie szybko ustalić jaka powinna być struktura komunikatów SOAP i wygenerować odpowiednie arkusze XSL. W ten sam sposób możemy obsłużyć nie tylko wywołania web services, ale np protokół XML-RPC czy inne, oparte na XML/HTTP protokoły komunikacji co czyni zadanie XmlHttp dość uniwersalnym narzędziem.

Taki sposób używania web services nie jest oczywiście zarezerwowany dla NGinn – można to robić w każdej innej sytuacji, nawet gdy mamy do dyspozycji Visual Studio z automatycznym generowaniem kodu klienta web service. Po co? Otóż są pewne typy danych których standardowa serializacja SOAP oferowana przez Microsoft.Net nie potrafi obsłużyć – na przykład typy nullable, nieprawidłowo też są obsługiwane pola typu ‘DateTime’ w innej niż UTC strefie czasowej. Innym przykładem są niestandardowe nagłówki http lub rozszerzenia SOAP – za pomocą opisanego tu sposobu możemy generować wywołania z dowolnymi nagłówkami i z dowolną treścią, podczas gdy w ‘normalny’ sposób musieli byśmy pisać w C#/VB dodatkowy kod obsługujący przypadki niestandardowe. No a poza tym jest to świetny sposób testowania web services bez pisania kodu.

Ponieważ opisany tu mechanizm wywoływania web services jest bardzo prosty, można wręcz powiedzieć że prymitywny, rodzi się pytanie czy jest to rozwiązanie docelowe czy tylko prowizoryczne? Na pewno brakuje mu kilku rzeczy – między innymi automatycznej kontroli zgodności schematów danych z definicją web service, można by się też pokusić o jakiś graficzny edytor pozwalający mapować zmienne zadania na parametry we/wy usługi lub też automatycznie generować XSL-e na podstawie jakiegoś prostszego mapowania. Ale mimo to uważam że jest to elastyczne i proste w użyciu narzędzie które da się zastosować w wielu sytuacjach.

Jakie powinno być oprogramowanie dla dużych organizacji?

Ponieważ od wielu lat zajmuję się realizacją i wdrożeniami oprogramowania wspierającego procesy biznesowe w dużych organizacjach zebrało mi się sporo przemyśleń na temat specyfiki tej pracy, trudności które najczęściej wdrożeniowców czekają oraz sposobów radzenia sobie z typowymi przy takich projektach problemami. Dziś zatem będzie o tym w jaki sposób dobrze zaprojektowane oprogramowanie może pomagać nam w realizacji skomplikowanych projektów. Artykuł ten jest wyjaśnia też dlaczego projektując NGinn starałem się uwzględnić  pewne cechy oprogramowania które uważam za pomocne przy realizacji potrzeb naszych klientów.

Wdrożenie

Mówiąc o wdrożeniu systemu w dużej organizacji mam na myśli firmy liczące kilka tysięcy pracowników gdzie wdrażany system ma spory ‘zasięg rażenia’, czyli obejmuje istotną część personelu firmy i w jakiś sposób dotyczy istotnych dla biznesu tej firmy procesów. Przykładem takiego oprogramowania są systemy CRM, wsparcia klientów, obsługi zamówień/zleceń lub też systemy wspierające wewnętrzne procesy w dużych firmach – HR, zakupy wewnętrzne, helpdesk IT etc. W takim środowisku wymagania najczęściej nie pochodzą od jednej osoby ale są jakąś wypadkową potrzeb i planów rozwojowych wielu osób z różnych działów danej firmy. Oznacza to że prowadząc wdrożenie musimy współpracować z wieloma ‘właścicielami biznesowymi’ jednocześnie, najczęściej będą to managerowie jednostek organizacyjnych w których wprowadzamy nasz system. Wiele źródeł wymagań oznacza potencjalne rozbieżności w specyfikacji potrzeb no i różne sposoby interpretacji tego co zostało ustalone  - prawdopodobnie nie da się wszystkiego uzgodnić tak żeby specyfikacja była spójna, kompletna, niesprzeczna i na dodatek tak samo rozumiana przez wszystkie strony. Oczywiście należy do tego dążyć, ale wg mnie trzeba na jakimś etapie odpuścić i uznać że tak po prostu jest że każdy chce czego innego i na co innego zwraca uwagę. Problem jest taki że system musi być oparty na niesprzecznej wewnętrznie logice no i zapewnienie logiczności tej logiki to właśnie nasze zadanie, ale myślę że aby pracę w ogóle dało się zacząć to wystarczy ustalić kwestie podstawowe, złapać jaki-taki obraz całości i pilnować żeby każdy właściciel biznesowy na końcu dostał to na czym mu zależy. Przy takim podejściu musimy dopuścić możliwość robienia wyjątków od ustalonych przez wszystkich reguł, na przykład ustalając że system zaoferuje inną niż dla całej reszty firmy procedurę pracy lub zawartość formatki dla pracowników jakiegoś działu X bo akurat manager tego działu chce inaczej zarządzać u siebie pracą. Zamiast zmuszać wszystkich właścicieli biznesowych do uzgodnienia jednego wspólnego stanowiska w tej sprawie po prostu uwzględniamy potrzeby X w budowanym systemie tak aby nie miało to wpływu na pozostałych.

A co ze stroną techniczną przy takim podejściu? W sumie to każdy system może uwzględnić rozbieżne wymagania i wyjątki (oraz wyjątki od wyjątków) w logice biznesowej, różnice są tylko w sposobie implementacji takich wymagań, stopniu komplikacji wynikowego systemu oraz nakładzie pracy koniecznym do implementacji i późniejszego utrzymania systemu. Jest natomiast kilka cech oprogramowania które takie zadania ułatwiają:

  • oddzielenie logiki biznesowej od infrastruktury. Oprogramowanie powinno w przejrzysty sposób dzielić się na infrastrukturę (czyli komponenty odpowiedzialne za podstawowe funkcje systemu które się nie zmieniają z wdrożenia na wdrożenie) oraz na ‘logikę biznesową’ czyli specyficzne dla klienta struktury danych, GUI, formularze, szablony dokumentów i reguły według których system realizuje funkcje uzgodnione z klientem. Najlepiej żeby podział na infrastrukturę i logikę był silny, tzn każdy komponent powinien być jednoznacznie przypisany do jednej z tych grup, a dodatkowo infrastruktura powinna być na tyle dobrze zaprojektowana żeby implementacja kolejnych funkcji biznesowych nie wymagała omijania ani modyfikacji infrastruktury.
  • możliwość wprowadzania lokalnych zmian w logice. To szeroki temat, ale chodzi o to aby uwzględniając czyjeś specjalne potrzeby można było działać ‘lokalnie’ nie wpływając na pozostałe funkcje systemu, na przykład zaoferować dla wybranych użytkowników specjalną wersję formatki bez modyfikowania formatki ‘ogólnej’, tj tej dostępnej dla wszystkich, albo zmienić reguły rządzące przepływem określonego typu dokumentów bez modyfikowania reguł ogólnych którym podlegają wszystkie inne dokumenty.  I odwrotnie  - system w którym cała logika jest zapisana w jednym pliku z kodem źródłowym w postaci gigantycznego i wielokrotnie zagnieżdżonego if-else na pewno nie spełnia tej reguły i jest bardzo trudny do modyfikacji.
  • zwięzły, przejrzysty i najlepiej samo-dokumentujący się sposób zapisu logiki biznesowej. Struktura musi być łatwa do zrozumienia i zobrazowania, dzięki temu będziemy w stanie zapanować nad wielością reguł i wyjątków od nich. Można taki cel osiągnąć np przez wprowadzenie specjalizowanego języka ‘wyższego poziomu’ do zapisywania logiki biznesowej, np w postaci reguł if-then-else albo procesów w określonej notacji, tudzież opartego o XML języka opisu wyglądu formatek

Zmiana

Wracając do samego wdrożenia, chyba najważniejszym problemem z którym sobie musimy poradzić jest zmiana. Zmiany w dużych organizacjach to norma, możemy być pewni że w momencie uruchomienia systemu po raz pierwszy zacznie płynąć strumyczek wniosków racjonalizatorskich, żądań zmian i pomysłów na kolejne zastosowania systemu. Dla nas powinna to być okazja do robienia biznesu a nie rzecz z którą należy walczyć – należy klienta zachęcać do zmian i je realizować, problem tylko w tym jak to zrobić żeby nie wpaść przy okazji po szyję w bagno. A bagno pojawia się kiedy na skutek radosnej twórczości klienta  (przy naszym współudziale) system robi się bardzo skomplikowany, reguły niejasne, niespójne i czasami się wykluczające i pojawiają się dziwne ‘błędy’ za które jesteśmy obarczani odpowiedzialnością. A wina nie zawsze jest po naszej stronie – no bo po kilku latach rozwoju systemu w firmie sytuacja zwykle przedstawia się tak:

  • nikt po stronie klienta nie ma pełnego obrazu tego jakie funkcje i w jaki sposób system realizuje. Po prostu nie ma takiej osoby bo różne wymagania pochodziły od różnych ludzi, w różnym czasie i nie ma nikogo kto by to potrafił albo chciał uzgodnić
  • ludzie którzy definiowali jakieś funkcje i ustalali z nami sposób ich działania albo już nie pracują, albo nie pamiętają, albo nie chcą pamiętać, albo dostali inne zadania i już nie biorą udziału w projekcie
  • nowi managerowie nie biorą odpowiedzialności za nic bo ‘to nie oni ustalali’. Mówią tylko że w systemie jest błąd bo nie pasuje do ich koncepcji.
  • Dokumentacja nic nam nie pomoże, po prostu jest zbyt ogólna i fragmentaryczna. Nie sposób spisać każdego szczegółu.
  • Jeśli się pilnowaliśmy to mamy rejestr wprowadzanych zmian. Świetnie, ale to nic nam nie pomoże bo jest tego tyle że trzeba nadludzkich możliwości żeby analizując poszczególne zmiany ustalić jaki powinien być aktualny stan systemu. Poza tym wiele drobiazgów było załatwiane na gębę/telefon/maila i nie ma już po tym śladu
  • Może nawet mamy specyfikację systemu i wiemy co i dlaczego w nim działa tak jak działa (o, super!) ale system i tak ‘źle działa’ bo w międzyczasie u klienta była reorganizacja, np cześć procesu została ‘outsorcowana’, wprowadzono nowe grupy, inne zasady rozliczeń itp itd. Oczywiście to nasz problem a nie klienta, no bo przecież wymagania mówią że system ma realizować jakiś tam cel a nagle przestał.

Właściwie wszystkie powyższe punkty przerabiałem, myślę że każdy informatyk który pracuje z klientami również byłby gotów się z większością zgodzić. I myślę też że nie ma skutecznego sposobu który by pozwalał sobie z tym poradzić. Ale trzeba zacisnąć zęby i z tym żyć (albo zmienić pracę na jakąś mniej stresującą), dobrze też jest mieć jakieś oparcie w oprogramowaniu – a oto cechy które uważam za bardzo pożądane w takiej sytuacji:

  • Wszystkie 3 punkty o których pisałem wyżej przy okazji specyfikacji wymagań, oraz:
  • Możliwość współistnienia kilku wersji logiki biznesowej. Na przykład jednoczesnego działania w oparciu o starą logikę i nową logikę – po prostu, stare sprawy biegną wg starych reguł, nowo zakładane – wg nowych. To bardzo ułatwia migracje. To pozwala też na robienie zmian lokalnych o których pisałem wcześniej – czyli takich co obejmują tylko niektórych użytkowników nie wprowadzając zamieszania u pozostałych. No i ułatwia ewentualny rollback do poprzednich wersji jeśli nowa nie okaże się takim sukcesem jak się wydawało.
  • Samo-dokumentacja (również wspomniany wcześniej). Logika biznesowa powinna być zapisana tak, żeby łatwo można było ustalić aktualny sposób działania systemu. Na przykład strukturalny zapis w postaci reguł if-then-else pozwala nam narysować drzewo decyzyjne wg którego system podejmuje jakieś działania, a np zapis procesów biznesowych w jakiejś strukturalnej notacji pozwala uzyskać diagram aktualnego przepływu dokumentów w firmie.
  • Możliwość wprowadzania zmian w locie. To jest istotne bo duże organizacje często pracują 24×7 i system jest tak samo potrzebny zarówno w dzień jak i w nocy. Nie można sobie pozwolić na długie przestoje, zwłaszcza kiedy zmian jest dużo i są wprowadzane bardzo często, nawet ad-hoc gdy ktoś zaobserwuje że system działa niezgodnie z oczekiwaniami i trzeba to szybko naprawić.
  • Możliwość wprowadzania zmian lokalnie, bez zatrzymywania pozostałych komponentów systemu – trochę związane z poprzednim, ale chodzi o to aby do wprowadzenia zmian w jakiejś funkcji systemu nie trzeba było  wyłączać całego systemu a zasięg naszych działań był tak mały jak to tylko możliwe.
  • Cała logika biznesowa powinna być przechowywana w postaci plików tekstowych. Dlatego że w ten sposób łatwo śledzić zmiany np za pomocą systemu kontroli wersji. Wszelkie formaty binarne (np z graficznych designerów), bloby w bazie danych,  bardzo utrudniają pracę.
  • Strukturalny podział logiki biznesowej według funkcji realizowanych przez system a nie czegoś innego (chociażby np obiektów w bazie relacyjnej). Biznesowo widziane funkcje systemu mają charakter przekrojowy – obejmują wiele fragmentów aplikacji, takich jak struktura danych w bazie, GUI, uprawnienia, specyficzną dla danej funkcji logikę, powiadomienia email, szablony dokumentów, raporty itp itd. Wszystko to może składać się na jedną logiczną funkcję, o nazwie np ‘obsługa płatności’ albo ‘obsługa komunikacji z dostawcami’. Dobrze by było żeby struktura systemu grupowała te elementy w pakiety według logicznych funkcji, wtedy łatwo ustalić które elementy systemu biorą udział w realizacji określonych wymagań i jaki będzie zakre zmian w systemie przy modyfikacji określonej funkcji. Istotna jest też separacja – dwie różne funkcje raczej nie powinny wykorzystywać tych samych fragmentów logiki biznesowej czy GUI, nawet jeśli są one bardzo podobne. A to dlatego że wprowadzając zmiany dla jednej grupy użytkowników nie chcemy przy okazji psuć czegoś pozostałym. ‘Code reuse’ to dobra rzecz ale nie tutaj.
  • Preferuję systemy ‘miękkie’ – tzn skryptowe, interpretowane, dynamiczne – w przeciwieństwie do statycznie typowanych, kompilowanych, buildowanych, package’owanych i deployowanych. Chodzi o modyfikacje logiki biznesowej, konfiguracji, uprawnień czy GUI. Infrastruktura może być kompilowana i niezmienialna po wdrożeniu, ważne aby logika była modyfikowalna. Dzięki temu możemy dokonywać szybkich poprawek, mini-wdrożeń, szybkich testów nowej funkcjonalności itp. Generalnie uważam że logika biznesowa powinna być zapisywana w językach skryptowych (przy czym wybieramy takie języki które najlepiej się nadają do określonego celu), zaś infrastruktura w językach niższego poziomu – C#, C++, Java itp.
  • Cechy powyższe pozwalają nam też zmienić podejście do testowania i prototypowania nowych funkcji. Mając możliwość wrzucenia nowej wersji logiki bez wyłączania starej możemy przeprowadzać testy nowych czy zmodyfikowanych funkcji na produkcji – po prostu udostępniając je wybranej grupie testerów. Po potwierdzeniu poprawnego działania włączamy funkcję dla wszystkich użytkowników. Bardzo często testy obejmują nie tylko zmiany w systemie, ale również zmiany w organizacji – klient po prostu chce realizować nowy proces posługując się naszym narzędziem i chce go sobie przetestować na jakiejś małej grupie ludzi. W takim przypadku testowanie na produkcji jest czymś naturalnym i bardzo ułatwia późniejsze uruchomienie ‘oficjalne’. Jest to też zachęta dla klienta do eksperymentowania – zwykle sprawdzenie czegoś ‘na żywo’ jest lepsze, szybsze i tańsze niż pisanie dokumentów z wymaganiami i analizowanie tematu ‘na sucho’.

Wydajność

Sprawa wydajności jest bardzo ważna – możemy dowolnie rozwijać i modyfikować nasz system, ale musi on zapewniać wydajność wystarczającą do obsłużenia wszystkich nowych pomysłów. A jeśli odnieśliśmy sukces to klient będzie chciał objąć wdrożeniem jakieś nowe obszary organizacji co oznacza więcej danych, więcej użytkowników no i więcej funkcji w aplikacji. W niektórych przypadkach rozwój systemu jest tak szybki że przerasta to nasze oczekiwania oraz możliwości systemu który nie był projektowany do tak dużego obciążenia. W tym miejscu kilka uwag na temat potencjalnych ‘min’ wydajnościowych:

Uwaga na relacyjne bazy danych. Mimo że relacyjna baza danych to podstawa bez której nikt nie wyobraża sobie aplikacji biznesowej to jednak jest to narzędzie które ma swoje ograniczenia. Po pierwsze jest ’sztywna’ – wymaga uzgodnienia struktur danych i trzymania się ich. Tabela w bazie jest taka sama dla wszystkich przechowywanych w niej obiektów, nawet jeśli różnią się one od siebie na poziomie aplikacji. Bazy danych nie obsługują dziedziczenia. Nie można wprowadzać zmian lokalnie – albo zmieniasz całą tabelę albo nic, zmiana struktury danych dotyczy wszystkich miejsc systemu które się do niej odwołują. Druga sprawa to wielkość bazy danych – jeśli chcesz wprowadzić zmiany w tabeli która liczy sobie miliony rekordów licz się z tym że może to być bardzo długotrwały proces albo nawet i niewykonalny (!). Na przykład dodanie nowej kolumny do rekordu aktualizuje nam wszystkie stare rekordy które już są w bazie – rozmiar rekordu się zwiększa i następuje reorganizacja całej tabeli która może trwać nawet kilka godzin. A nasz czas na serwis to 30 minut… niby prosta zmiana a nie do zrobienia. Trzecia sprawa to wydajność samej bazy – rozwój systemu to coraz większa jego komplikacja i coraz bardziej złożone transakcje w bazie danych. Transakcyjność to bardzo fajna rzecz ale potrafi również mocno ograniczyć wydajność aplikacji poprzez nakładanie blokad wykluczających współbieżne wykonywanie operacji. Na dodatek nie bardzo można zwiększać wydajność systemu przez dołożenie kolejnej bazy danych – najczęściej baza musi być jedna no bo chcemy działać na jednej, aktualnej wersji danych a nie na kilku niezsynchronizowanych kopiach. Dobrze jest przemyśleć te rzeczy na jakimś wczesnym etapie bo im więcej danych się nagromadzi tym mniejsze mamy możliwości manewru.

Kolejna sprawa to komunikacja między komponentami systemu oraz między systemem a innymi aplikacjami z którymi jest zintegrowany. Należy uważać na komunikację synchroniczną – mimo że wydaje się fajnym pomysłem na początku to może nam mocno obciąć wydajność całej aplikacji. Kluczowe operacje w systemie należy rozbić na jak najmniejsze kawałki, tak aby transakcje miały niewielki zasięg i wykonywały się szybko. Resztę operacji można dokończyć asynchronicznie. Przykład: podczas rejestracji nowego dokumentu przez użytkownika pozyskujemy wymagane dane od użytkownika i rejestrujemy dokument w bazie. Wszelkie operacje dodatkowe, takie jak powiadomienie o nowym dokumencie innych systemów, wysłanie emaili, indeksowanie dokumentu itp robimy później, offline, po zakończeniu interakcji z użytkownikiem i zamknięciu tej pierwszej transakcji w bazie danych.  Komunikację z systemami zewnętrznymi robimy w tle bo nigdy nie wiadomo jak długo przyjdzie nam czekać na odpowiedź. Tak samo jeśli oferujemy jakąś usługę (np web service) dla systemów zewnętrznych – gdy zrobimy to w wersji synchronicznej może się okazać że zewnętrzne aplikacje po prostu ‘zarzynają’ nasz system. Bardzo dobrym pomysłem jest wykorzystanie architektury opartej o ‘message bus’ tj mechanizm asynchronicznego przekazywania komunikatów między komponentami systemu – w takiej architekturze naturalne jest podejście asynchroniczne. Temat jest godny jakiegoś rozwinięcia, ale to kiedy indziej.

NGinn

Oczywiście nie mogę tutaj pominąć NGinn – jest to narzędzie które projektowałem biorąc pod uwagę wszystko co napisałem wyżej (i pewnie jeszcze trochę spraw o których nie napisałem). NGinn jest moją odpowiedzią na problem realizacji skomplikowanych i ciągle zmiennych wymagań w rozbudowanych systemach. Oczywiście jeden NGinn nie załatwi wszystkiego, jest to tylko workflow engine  i nie obejmuje na przykład GUI systemu, ale skutecznie stosuje opisane tu reguły do realizacji logiki biznesowej systemu. A to dlatego  że:

  • Proces w NGinn opisuje nam logikę systemu potrzebną do realizacji określonego procesu biznesowego, tj zawiera opis konkretnej funkcji biznesowej systemu, od początku do końca. Procesy grupowane są w pakiety które oprócz samej definicji procesu mogą przenosić dodatkowe elementy potrzebne do realizacji danej funkcji biznesowej – np szablony powiadomień, logikę integracyjną, etc
  • NGinn obsługuje wersjonowanie procesów – procesy mogą współistnieć w wielu wersjach jednocześnie, wprowadzanie nowej wersji procesu nie wymaga wyłączenia starej ani migracji
  • Można modyfikować istniejące procesy bez zatrzymywania systemu.
  • Struktury danych w NGinn są dynamiczne – modyfikując proces można zmieniać definicję jego danych, nie pociąga to za sobą konieczności modyfikacji struktury bazy danych lub aktualizacji innych procesów które już się toczą.
  • Asynchroniczny w założeniach – NGinn polega głównie na asynchronicznej komunikacji między komponentami oraz między NGinn a światem zewnętrznym. Wewnętrznie NGinn używa kolejki komunikatów do przesyłania informacji pomiędzy procesami.
  • Definicja procesu jest jednocześnie dokumentacją logiki systemu – mając definicję można wygenerować graficzny schemat procesu.
  • NGinn pozwala używać ‘rules engine’ do zapisu drzew decyzyjnych w postaci reguł if-then-else. Może się to przydać tam gdzie istnieje potrzeba podejmowania różnych działań w oparciu o rozmaite atrybuty danych wejściowych.
  • W jasny sposób oddzielona jest infrastruktura NGinn od logiki biznesowej. Logika biznesowa jest przechowywana w definicji procesu w oparciu o którą infrastruktura ten proces realizuje.

Na koniec…

Podsumowując to wszystko, myślę że najefektywniejszą techniką tworzenia oprogramowania na zamówienie dla wymagających klientów (hm, czy są klienci niewymagający?) jest szybkie prototypowanie wsparte odpowiednio dostosowanym narzędziem. Pomaga to zarówno przy ustalaniu wymagań jak i przy późniejszej ich realizacji. Klientom bardzo przydaje się możliwość zobaczenia tego co chcą zamówić i skonfrontowania swoich wyobrażeń z rzeczywistością, a my dzięki temu mamy wymagania lepszej jakości i bardziej odpowiadające możliwościom systemu. No i szybciej możemy wystawić fakturę.

Oczywiście nie jest to jakieś rewolucyjne podejście – w świecie informatyki funkcjonują rozmaite metodyki prowadzenia projektów mające w założeniach szybkie prototypowanie – na przykład Extreme Programming czy rozmaite Agile – ale zostawmy ten temat, w końcu nie zajmujemy się tu teorią tylko robieniem softu. Reprezentuję punkt widzenia gościa który własnymi (lub zespołu) rękami tworzy oprogramowanie  a własną głową odpowiada za dostarczenie go klientowi zgodnie z zamówieniem i w terminie i mam nadzieję że takie spojrzenie od strony warsztatu jest dobrym uzupełnieniem ‘best practices’ prowadzenia projektów informatycznych.

Wyszukiwanie danych w procesach

Dziś zajmę się sprawą wyszukiwania danych procesu w NGinn. Problemem w tym przypadku jest sposób przechowywania danych procesu przez NGinn – otóż dane są przechowywane jako fragment dokumentu JSON zawierającego stan procesu. Taki sposób przechowywania danych nie ułatwia ich przeszukiwania – nawet jeśli dokumenty są zapisywane w relacyjnej bazie danych to i tak nie da się za pomocą SQL zadawać zapytań o poszczególne pola w dokumencie.
Rozwiązaniem jest odpowiednie indeksowanie dokumentów, przy czym nie mam tu na myśli indeksów znanych z relacyjnych baz danych a zewnętrzny indeks pełnotekstowy pozwalający na umieszczanie w nim dowolnych danych. Jednym z narzędzi tego typu jest Apache Lucene oraz jego port do środowiska .Net pod nazwą Lucene.Net. Lucene pozwala na budowę indeksów o dowolnej zawartości (każdy wpis w indeksie może zawierać dowolny zestaw pól) które potem mogą być przeszukiwane za pomocą prostych zapytań.

Poświęciłem ostatnio trochę czasu na próbę połączenia NGinn i Lucene i wyniki są bardzo zachęcające. Przede wszystkim wrażenie robi wydajność Lucene.Net – wyniki są zwracane w mgnieniu oka a mechanizm wyszukiwania jest bardzo sprytny i ma wiele interesujących możliwości. Wystarczy w aplikacji umieścić jedno pole wyszukiwania i możemy szukać po dowolnych danych w procesie a nawet konstruować bardziej zaawansowane zapytania zbudowane z kilku predykatów połączonych operatorami logicznymi. Dodatkowo Lucene w naturalny sposób obsługuje paging co znacznie ułatwia budowę aplikacji. Nie będę tu wchodził w techniczne szczegóły, ale mechanizm indeksowania danych NGinn za pomocą Lucene działa w trybie offline, tzn indeksuje dane z pewnym niewielkim opóźnieniem dzięki czemu indeksowanie nie ma negatywnego wpływu na szybkość działania NGinn. Komponent indeksujący uaktywnia się tylko po zmianie stanu jakiegoś procesu i wtedy wpis w indeksie dla tego procesu jest aktualizowany. Nie robiłem testów na milionach rekordów, ale przy właściwym poindeksowaniu danych wyszukiwanie za pomocą Lucene powinno być o wiele szybsze niż jakiekolwiek zapytania SQLowe. Jeśli chodzi o wielkość indeksu to jest ona zaskakująco mała biorąc pod uwagę ilość danych którą można w nim umieścić.

Jeśli chodzi o plany rozwojowe to w najbliższym czasie nie zamierzam umieszczać w NGinn komponentu indeksującego – jest to fajny dodatek ale ponieważ nie jest bezpośrednio związany z funkcjami workflow engine to na razie nie będę go rozwijał w ramach projektu Open Source – no chyba że pojawi się perspektywa jakiegoś interesującego wdrożenia.

Poniżej zamieszczam przykładowy ekran wyszukiwania z prostej aplikacji którą posiłkuję się przy pracach nad NGinn. Łącząc framework Ext JS oraz Lucene udało się bardzo szybko rozbudować tę aplikację o funkcję wyszukiwania procesów na podstawie ich wewnętrznych danych. Tak więc szukającym dobrego search engine dla swojego systemu bardzo polecam Lucene.

screen

O dokumentach, procesach, bazach danych i JSON. No i o przyszłości.

cf_schema_large

Mówiąc o jakichkolwiek procesach biznesowych wcześniej czy później trafiamy na pojęcie dokumentu. To wokół dokumentu buduje się procesy – dokumentem może być jakiś papier wędrujący przez organizację, faktura, sprawa zarejestrowana w jakimś systemie, nadesłany przez klienta email z zażaleniami i tak dalej. Jeśli chcemy taki proces ‘zinformatyzować’ to również dokumenty muszą zostać w jakiś sposób przetworzone na postać cyfrową i zarejestrowane w jakimś systemie.

W przypadku NGinn nastawiłem się na maksymalną elastyczność w temacie dokumentów do których odnoszą się procesy – mianowicie elastyczność wynika z całkowitego pominięcia tego tematu. Procesy w NGinn operują na danych (zostało to opisane we wcześniejszych artykułach) i te dane zawierają wszystkie informacje istotne z punktu widzenia działania procesu. W szczególności założyłem że będą mogły zawierać odnośnik do odpowiedniego dokumentu  w systemie zewnętrznym – na przykład numer faktury w systemie księgowym albo numer zgłoszenia w systemie wsparcia klientów. Sam system NGinn nie wnika w to w jakim systemie i w jaki sposób są te dokumenty przechowywane, natomiast oferuje mechanizmy integracyjne pozwalające na wymianę danych z takimi systemami i pobieranie odpowiednich informacji o dokumencie jeśli to konieczne. To pozwala twierdzić że NGinn znakomicie nadaje się do rozszerzania możliwości istniejących w firmie systemów oraz do ich integrowania, ale zupełnie pomija przypadek gdy odpowiedniego systemu nie ma w firmie a dokumenty dopiero czekają na bazę danych która je przechowa. No właśnie, co wtedy?

Pierwszy pomysł to wykorzystanie danych procesu. NGinn pozwala na definiowanie własnych struktur danych dla procesów, więc nic nie stoi na przeszkodzie żeby danymi procesu był cały dokument. Projektujemy strukturę danych tak żeby uwzględnić wszystkie pola dokumentu i wtedy zawartość naszego ‘dokumentu’ jest przechowywana przez NGinn razem z innymi informacjami o procesie. Zaletą jest to że nie musimy się martwić o przechowywanie tych danych, wad za to jest kilka – przede wszystkim dokument istnieje tylko razem z procesem i ‘zamiera’ gdy proces się kończy, dodatkowo NGinn bardzo ogranicza to co można zrobić z danymi procesu. Tak więc ten sposób jest dobry tylko dla prostych struktur danych, nieistotnych poza procesem który ich dotyczy.

Druga możliwość wiąże się z pewną rozbudową NGinn. Otóż w tej chwili NGinn przechowuje stan procesów i zadań w postaci JSON. Każde zadanie zapisywane jest w postaci pojedynczego dokumentu JSON zawieracjącego wszystkie jego dane i informacje potrzebne do działania procesu. Dzięki temu mamy elastyczność i dowolność w definiowaniu struktury procesu i jego danych. No i jeśli byśmy chcieli rozszerzyć NGinn o bazę dokumentów to możemy po prostu wykorzystać ten sam mechanizm i przechowywać dowolne dokumenty w postaci JSON – wtedy zarówno dokument jak i proces toczący się wokół niego były by obsługiwane w ten sam sposób, jako dokumenty JSON. Pomysł interesujący według mnie, ale czy są z nim jakieś problemy?

Są, ale to raczej ‘wyzwania’ niż problemy. Chodzi o to że w tej chwili do przechowywania dokumentów JSON ze stanem procesu jest wykorzystywana baza danych (MSSQL) – jest tam jedna tabela z polem tekstowym (ntext) do której wrzucany jest cały dokument JSON. Dla NGinn to jest wystarczające, ale gdybyśmy w ten sposób chcieli przechowywać dokumenty z danymi które chcemy przeszukiwać to szybko się okaże że szukanie odpada, nie da się efektywnie takiej postaci danych przeszukiwać a i aktualizacja jest mocno ograniczona. Rozwiązaniem według mnie jest przejście na nierelacyjną bazę danych, na przykład taką przeznaczoną do obsługi dokumentów JSON. Takich baz danych jest bardzo wiele, komercyjnych i darmowych. Powstały jako ‘back-end’ dla serwisów internetowych obsługujących bardzo duży ruch i wymagacjących ogromnej skalowalności bazy danych – np bardzo popularnych serwisów społecznościwych i sprawdzają się w takich zastosowaniach o wiele lepiej niż bazy relacyjne. Dodatkowo są bardzo elastyczne jeśli chodzi o struktury danych,  a to jest wg mnie kluczowa sprawa przy obsłudze dużych (raczej bardzo dużych) ilości dokumentów. Wystarczy pomyśleć co się dzieje gdy chcemy np zmienić strukturę istniejącej bazy relacyjnej:

- czas: jeśli tabela jest bardzo duża (miliony rekordów) to dodanie nowej kolumny może trwać bardzo długo gdyż może wywołać zwiększenie rozmiaru rekordu i reorganizację całego pliku bazy danych. Długo to znaczy np 5 godzin, gdy nasze ‘okienko serwisowe’ to 30 minut.
- dostępność: na czas aktualizacji bazy relacyjnej musimy wyłączyć aplikację gdyż operacje modyfikacji bazy wykluczają jej odczyt (blokada dostępu do tabel na czas aktualizacji)
- inne problemy: np konieczność zdejmowania i ponownego zakładania constraintów przy operacjach na danych, czasami prosta aktualizacja wymaga niezmiernie skomplikowanych operacji na więzach integralności i indeksach
- no właśnie, indeksowanie, nie można z nim przesadzić gdyż nadmierne poindeksowanie danych spowalnia ich aktualizację

W przypadku bazy dokumentów JSON nie musimy w żaden sposób zmieniać jej struktury gdy modyfikujemy strukturę naszych dokumentów, zaś indeksowanie jest w trybie ‘offline’ czyli nie wpływa na wydajność aktualizacji. Dodając do tego fakt że JSON jest podstawowym formatem dokumentu w NGinn użycie tekstowej bazy dokumentów wydaje się naturalnym pomysłem. A dobrym kandydatem wydaje mi się baza ‘Persevere’  (http://www.persvr.org/) – jest to kompletna baza danych dokumentów JSON z dostępem poprzez HTTP/REST i z rozbudowanymi funkcjami ‘bazodanowymi’ takimi jak indeksowanie, przeszukiwanie czy zarządzanie zawartością bazy. Polecam zapoznanie się choć pobieżnie z jego możliwościami oraz potencjalnymi zastosowaniami, wg mnie to bardzo otwiera oczy na możliwości tych ‘nowych’ baz danych oraz ich potencjalne zastosowania. Jak by wyglądał NGinn po przejściu na bazę Persevere (lub inną tego typu)? Przede wszystkim myślę że wtedy nie będzie używał żadnej bazy relacyjnej, a po drugie myślę że byłby wydajniejszy i bardziej elastyczny, zwłaszcza gdyby dać możliwość dowolnego indeksowania dokumentów i danych w procesach w celu ich łatwego przeszukiwania. Na pewno poświęcę temu niedługo więcej czasu.

P.S. Bazy w rodzaju Persevere zwykle nie obsługują transakcji rozproszonych ani two-phase commit. To jest duże ograniczenie dla NGinn i główny problem do przezwyciężenia. Chodzi o to że w tej chwili NGinn wykorzystuje transakcje rozproszone (transactionscope) do zapewnienia spójności stanu procesu w razie jakichś problemów.

Krótki update

W czerwcu nie było żadnego wpisu o NGinn, a to dlatego że mam ostatnio mało czasu. Z powodów różnych w tej chwili przechodzę z pracy etatowej na działalność gospodarczą i musialem odłożyć nieco aktywność poza-zawodową. Ale w związku z rozpoczęciem pracy na własny rachunek planuję z NGinn uczynić projekt ‘profesjonalny’ co powinno dobrze wpłynąć na jego rozwój. Więcej informacji niedługo.

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.