Amazon SWF – JAVA ma flow

Cześć, miało być o serverless ale nie będzie, jeszcze nie tym razem. Nie chcę pisać na siłę dlaczego akurat programista JAVA powinien implementować coś jako serverless – jeżeli znajdę dobry autentyczny przykład to będzie o tym. Tym razem sprawdzę w akcji framework Amazon SWF czyli Amazon Simple Workflow Service.

Wiec czym jest Amazon SWF? Jest to jedna z usług AWS, która pozwala skoordynować pracę miedzy rozproszonymi komponentami/aplikacjami w chmurze. W SWF FAQ możemy przeczyć o przykładowych use case’ach jakie jest w stanie obsłużyć ten serwis:

  • procesowanie strumienia mediów
  • modelowanie procesów biznesowych dla aplikacji webowych
  • analiza strumieni danych

Każda ścieżka procesu to zestaw zadań do wykonania. Ścieżki mogą się rozłączać, a później ponownie złączać. Zadania mogą być automatyczne np. wykonanie skryptu, zawołanie serwisu, wysłanie danych do kolejki MQ lub manualne czyli wykonanie czynności przez użytkownika systemu.

Korelowanie zadań może sprowadzać się do zarządzania zależnościami między krokami (execution dependencies), odraczanie ich uruchomienia w czasie (scheduling) lub współbieżność równoległych ścieżek (concurrency). Z Amazon SWF programista dostaje narzędzie, które pozwala mu na pełną kontrolę nad implementacją zadań, synchronizowanie ich i uruchamianie bez martwienia się jak te mechanizmy zostały zaimplementowane pod spodem. AWS Flow Framework otwiera również dla programisty możliwość budowania procesów asynchronicznych.

Taks reprezentuje wykonanie kilku logicznych kroków, a krok to mini zadanie. Łatwiej operować na małych zadaniach i grupować je w większe taski. Taski wykonywane są przez tzw. workerów. Worker to program, który poprzez API SWF pobiera zadanie, procesuje, a następnie zwraca wynik. Skoro worker to program to musi on być napisany w jakimś języku programowania – w tym wypadku dostajemy wsparcie dla JAVA, .NET, Ruby i PHP.

Aby umożliwić sterowanie przepływem zadań między workerami potrzebny jest kolejny program tzw. decider, który może być napisany w dowolnym wspieranym języku tak jak worker. To, że Amazon SWF w swojej abstrakcji wprowadza separację tasków oraz interakcję między nimi, pozwala nam na zdeployowanie aplikacji w wybrany sposób. Może to być chmura (EC2, Lambda) albo po prostu blaszak za firmowym firewallem.

Co mogę zrobić przy użyciu Simple Workflow Service?

SWF został stworzony aby obsłużyć część wymagań jakie pojawiają się przy tworzeniu systemów rozproszonych, w których mamy do czynienia z modelowaniem różnego rodzaju procesów. SWF pozwala na:

  • pisanie aplikacji w sposób asynchroniczny tak aby zadania były uruchamiane zdalnie, a ich postęp też mógł być śledzony w sposób zdalny
  • zarządzanie stanem wykonania procesów biznesowych (które kroki zostały wykonane w całości, które kroki są wciąż w statusie running) – nie musimy używać baz danych ani żadnych dedykowanych systemów aby śledzić stan wykonania
  • komunikację i zarządzania flow of work pomiędzy komponentami aplikacji – dzięki SWF nie musimy myśleć o projektowaniu komunikacji z użyciem kolejek albo martwić się o utratę lub duplikaty zadań
  • w sposób scentralizowany zarządzanie krokami procesach aplikacji – zarządzanie logiką aplikacji nie mysi być rozsiane miedzy różnymi kompontentami, a może być zamknięte w jednym programie
  • integrację komponentów i systemów w tym systemów legacy i 3rd party serwisów w chmurze publicznej dzięki elastyczności w sposobie w jaki aplikacja jest deployowana i uruchamiana – SWF pozwala na stopniową migrację komponentów aplikacji z prywatnych data center do chmury publicznej bez naruszania dostępności i wydajności aplikacji
  • automatyzację długoterminowych zadań gdzie rolę odgrywa czynnik ludzki, gdzie część kroków musi wykonać użytkownika („apruwale”, „rewiusy”, „”inwestigejszyny” :)) – SWF skutecznie śledzi status procesowanych kroków, których czas wykonania może nawet trwać po kilka dni lub miesięcy
  • zbudowanie warstwy aplikacji, która udostępni DSL (Domain Specific Language) dla użytkowników końcowych  – odkąd Amazon SWF pozwala na pełną dowolność w wyborze języka programowania to umożliwia nam to na budowanie interpreterów dla wyspecjalizowanych języków (np. XPDL)
  • szczegółowy audyt wykonanych procesów i wgląd w stan działających aplikacji

Przykład 1

Dobrze więc przyjdźmy do praktycznego przykładu. Aby pokazać Ci w jaki sposób zbudowana jest aplikacja wykorzystująca SWF, przykład został podzielony na dwie części. W pierwszej części zastanowimy się co byłoby nam potrzebne aby zbudować prosty workflow w czystej Javie, tak aby można było go uruchomić jako pojedynczy proces. Natomiast w drugiej części spróbujemy przenieść nasz przykład do chmury i uruchomić go jako Simple Workflow Service.

Przypomnijmy, workflow to przepływ, który modelowany jest przy użyciu zadań, które to podzielone są na mniejsze logiczne kroki (aktywności) aby łatwiej było z nim pracować. Za wykonywanie zadań odpowiadają workery, a za sterowanie przepływem decidery. Zbudujemy prosty przykład z przepływem bez deciderów aby na tą chwilę nie zaciemniać Ci obrazu i abyś dobrze poczuł co jest czym.

Na początek przygotujmy bardzo prosty przykład w JAVA’ie. Wyobraźmy sobie, że chcemy zagrać w grę na Playstation dlatego przygotujemy:

  • interfejs PlaystationActivities, który reprezentuje nasze zadanie podzielone na aktywności, które powinny zostać wykonane w odpowiedniej kolejności
  • interfejs PlaystationWorkfloWorker reprezentuje workera, którego rolą będzie wykonanie uruchamianie aktywności
  • implementacje dla interfejsów
  • klasa PlaystationMain posłuży do uruchomienia programu

Wiem, że przykład jest mega prosty i brzmi jak zadanie HelloWorld z pierwszej lekcji programowania w JAVA’ie ale specjalnie tak jest żebyś w następnym przykładzie, który będzie uruchomimy w chmurze, mógł łatwo zmapować poszczególne elementy.

PlaystationActivities

Aktywności w zadaniu reprezentowane przez interfejs PlaystationActivities będą wyświetlać na ekran informację o tym co aktualnie robią:

public interface PlaystationActivities {

    String loginPlaystationPlus(String login);

    void playGta();

    void playFifa();

    String logoutPlaystationPlus();

}

Implementacja poniżej:

public class PlaystationActivitiesImpl implements PlaystationActivities {

    private String login;

    public String loginPlaystationPlus(String login) {
        this.login = login;
        System.out.println("You are logged in as " + login);
        return "LOGGED IN";
    }

    public void playGta() {
        System.out.println("Playing GTA");
    }

    public void playFifa() {
        System.out.println("Playing FIFA 2017");
    }

    public String logoutPlaystationPlus() {
        System.out.println("Logged out. Bye " + login);
        return "LOGGED OUT";
    }
}

Jak widać pojedyncze aktywności są niezależne tj. playGta() i playFifa() mogą być wykonane niezależnie, projektując flow możemy użyć obu albo np. tylko jednej z dwóch. W naszym przykładzie jednak zrobimy tak aby wszystkie aktywności ustawić w sekwencyjny i spójny logicznie przepływ. Przepływ kolejno będzie miał start, następnie nastąpi zalogowanie do usługi plus, kolejno wykonanie playGta() i playFifa() i przepływ zakończy się wylogowaniem z usługi. Diagram dla przepływu prezentuję poniżej:

Jeszcze raz, jak widzimy wszystkie kroki ustawione są sekwencyjnie, przepływ następuje kolejno z jednego kroku do następnego – reprezentuje tzw. topologię przepływu liniowego.

PlaystationWorkflowWorker

Aby nasz przepływ został wykonany w takiej kolejności jak na diagramie, odpowiada worker. W naszym wypadku jest to PlaystationWorkflowWorker, który posiada tylko jedną metodę odpowiedzialną za uruchomienie akcji play()

public interface PlaystationWorkflow {

    void play();

}

Implementacja powyższego interfejsu workera wygląda następująco:

public class PlaystationWorkflowImpl implements PlaystationWorkflow {

    private PlaystationActivities activities = new PlaystationActivitiesImpl();

    public void play() {
        String loginStatus =
                activities.loginPlaystationPlus("jbogacz");

        activities.playGta();
        activities.playFifa();

        String logoutStatus =
                activities.logoutPlaystationPlus();
    }
}

Playstation Workflow Starter

Punktem wejścia do naszego programu będzie klasa PlaystationMain, która odpowiada za zainicjowanie obiektu reprezentującego workera oraz uruchomienie przepływu poprzez metodę play(). Nic nie stoi oczywiście na przeszkodzie aby w tym miejscu przekazać jakieś parametry do workera ale na tą chwilę pozostaniemy przy najprostszym przykładzie z możliwych. Implementacja klasy main poniżej:

public class PlaystationMain {

    public static void main(String[] args) {
        PlaystationWorkflow workflow = new PlaystationWorkflowImpl();
        workflow.play();
    }

}

Przykład 2

Kod do tego przykładu dostępny jest na moim github’ie

Poprzedni przykład można powiedzieć, że był zwykłym HelloWorld i tak go nazywajmy. W tej chwili skupimy się na takiej rozbudowie kodu z poprzedniego zadania aby powstał prawdziwy HelloWorldWorkflow uruchamiany w chmurze. HelloWorld był uruchamiany jako pojedynczy proces, a interakcja między jego elementami odbywa się poprzez wołanie kolejno kawałków kodu. Aktualny idzie już poziom wyżej i przedstawiać będzie prawdziwe rozwiązanie gdzie komponenty są rozproszone w chmurze a komunikacja między nimi odbywa się przez SWF SDK i żądania HTTP.

W tej chwili chciałbym następującą rzecz, wziąć poprzedni przykład i na bazie niego pokazać Ci jak rozbudować go aby mógł być uruchomiony w AWS. W tym celu wykorzystam już wspomniany wcześniej AWS Flow Framework dla JAVA’y. AWS chwali się, że jest on prosty w użyciu, ponieważ ukrywa pod spodem sporo skomplikowanej logiki, co ma uprościć proces dewelopmentu. Osobiście wolałbym mieć świadomość co dzieje się tam w środku i czy nie za dużo bez mojej wiedzy. Jednakże, aby przejść do przykładu i uruchomienia kodu musimy przygotować środowisko (zakładam, że masz konto w AWS):

  • dodać użytkownika do AWS i wygenerować dla niego accessKey i secretId (link)
  • wyeksportować accessKey i secretId jako zmienne środowiskowe (AWS_ACCESS_KEY i AWS_SECRET_ID) – nie powinniśmy operować tymi informacjami w jawny sposób, osobiście jestem zwolennikiem aby przy użyciu AWS CLI stworzyć lokalną konfigurację na maszynie i trzymać te informacje w pliku .aws/credentials
  • zalogować się do konsoli Amazon Simple Workflow (link w Irlandii) i zarejestrować domenę dla playstation workflow – domena to logiczny kontener komponentów aplikacji takich jak workflow, aktywności i wykonania aktywności

PlaystationWorkflow Activities Worker

Nasz activity worker zaimplementowaliśmy bardzo prosto, jako pojedynczą klasę z poprzedniego przykładu – implementacja nie zmieniła się. Tak jak wspomniałem wcześniej, to co widzimy i jesteśmy tego świadomi to nasza konfiguracja i konkretna implementacja ale SWF framework upraszcza i ukrywa przed nami część rzeczy dlatego z perspektywy AWS Flow Framework dla Javy na activity worker składają się trzy rzeczy:

  • activity methods – odpowiadają za implementację kroków – zdefiniowane są jako interfejs i implementacja PlaystationActivities
  • obiekt ActivityWorker, który odpowiada za interakcje między implementacją kroków a  Amazon AWS
  •  komponent activities host, który rejestruje i uruchamia implementacje naszego workera  aktywności PlaystationActivities oraz odpowiada za posprzątanie po zakończeniu wykonywanych zadań

Po dodaniu adnotacji z AWS SDK interfejs prezentuje się jak poniżej:

@ActivityRegistrationOptions(defaultTaskScheduleToStartTimeoutSeconds = 300,
        defaultTaskStartToCloseTimeoutSeconds = 10)
@Activities(version="1.0")
public interface PlaystationActivities {

    String loginPlaystationPlus(String login);

    void playGta();

    void playFifa();

    String logoutPlaystationPlus();

}

Jak widać dodaliśmy kilka adnotacji, które sprawiają, że AWS Simple Workflow będzie rozumiał co do niego mówimy. Adnotacje, po pierwsze zapewniają informacje dla konfiguracji, a po drugie informują bezpośrednio AWS Flow Framework o tym, aby kompilator wygenerował dodatkową klasę dla aktywności tzw. activities client class, o której napiszę w dalszej części.

Omówmy sobie adnotację @ActivityRegistrationOptions i jej składowe, które definiują wartości timeotów:

  • defaultTaskScheduleToStartTimeoutSeconds – parametryzuje jak długo zadanie może oczekiwać w kolejce zadań zanim zostanie uruchomione – my ustawiamy na 5 minut
  • defaultTaskStartToCloseTimeoutSeconds – ustawia maksymalny czas na wykonanie zadania tzn. w jakim maksymalnym czasie zaimplementowana aktywność powinna się wykonać – my ustawiamy ten czas na 10 sekund

Z tego co zauważyłem adnotacja @Activities może przyjąć kilka parametrów ale zazwyczaj używa jej się do wskazania interfejsu odpowiedzialnego za definicję aktywności oraz wersjonowania co zobaczmy później w konsoli SWF (możemy utrzymywać wiele wersji danej konfiguracji).

PlaystationWorkflow Worker

Na Amazon SWF workflow worker składają się trzy komponenty:

  • klasa, która implementuje workflow i odpowiada za uruchomienie go w odpowiedniej kolejności i przepływ danych między krokami
  • klasa activities client, która jest wygenerowana automatycznie podczas kompilacji – jest ona swojego rodzaju proxy i jest wykorzystywana przez implementację workflow aby aktywności mogły być wykonywane asynchronicznie
  • klasa WorkflowWorker z Amazon SWF SDK, która odpowiada za interakcję między workflow i Amazon SWF

Weźmy nasz interfejs z poprzedniego przykładu i wzbogaćmy go o adnotacje, które będą zrozumiałe dla AWS Flow Framework

@Workflow
@WorkflowRegistrationOptions(defaultExecutionStartToCloseTimeoutSeconds = 3600)
public interface PlaystationWorkflow {

    @Execute(version = "1.0")
    void play();

}

Jak widać użyliśmy dwóch nowych adnotacji zapewniających konfigurację oraz bezpośrednio informuje AWS Flow Framework aby podczas kompilacji zostały wygenerowane klasy dla proxy.

Adnotacja @Workflow oprócz tego, że ze zwykłego interfejsu robi nam workflow to dodatkowo posiada opcjonalny parametr dataConverter, który jest używany do serializacji/deserializacji parametrów metod i zwracanych wartości dla metod w workflow

Adnotacja @WorkflowRegistrationOptions już została przez nas użyta w poprzednim interfejsie i tym razem również ustawiliśmy maksymalny czas na wykonanie całego workflow – 1 godzinę. Zachęcam do zapoznania się z adnotacjami AWS Flow Framework i ich parametrami, które możemy ustawić.

Użyliśmy jeszcze jednej bardzo ważnej adnotacji @Execute, którą wskazaliśmy w interfejsie metodę, która może zostać wywołana przez Amazon SWF do wystartowania procesu workflow. Adnotacja @Execute ma dwa główne zadania:

  • identyfikuje metodę play() jako wejście do workflow – jest to metoda, którą workflow starter woła aby rozpocząć proces – metoda może mieć parametry wejściowe, które pozwalają na zainicjalizować workflow zadanymi parametrami wejściowymi ale my w naszym przykładzie pomijamy to
  • definiuje numer wersji dla przepływu co pozwala na utrzymywanie wielu wersji dla workflow – jeżeli raz zarejestrowany proces w Amazon SWF chcemy zmodyfikować wtedy musimy podbić pole wersji

Implementacja PlaystationWorkflowImpl w tej chwili przedstawia się w następujący sposób:

public class PlaystationWorkflowImpl implements PlaystationWorkflow {

    private com.example.swf.playstationplay.PlaystationActivitiesClient activities =
            new com.example.swf.playstationplay.PlaystationActivitiesClientImpl();

    public void play() {
        Promise<String> loginStatus = activities.loginPlaystationPlus("jbogacz");
        activities.playGta(loginStatus);
        activities.playFifa(loginStatus);
        Promise<String> logoutStatus = activities.logoutPlaystationPlus(loginStatus);
    }
}

Porównując kod z poprzednim przykładem od razu widać dwie różnice:

  • PlaystationWorkflowImpl korzysta z instancje wygenerowanej klasy  PlaystationActivitiesClientImpl zamiast PlaystationActivitiesImpl
  • metody loginPlaystationPlus() i logoutPlaystationPlus zwracają obiekt typu Promise<String> zamiast String
  • metody playGta() i playFifa() przyjmują argument typu Premise<String> chociaż interfejs nie przewidywał, żadnych argumentów wejściowych – wygenerowana klasa PlaystationActivitiesClient przewiduję parametry wejściowe co pozwala na wskazanie Amazon SWF w jakiej kolejności powinny wykonać się metody

Poprzedni przykład był prostym programem w Javie uruchamianym lokalnie jako pojedynczy proces dlatego PlaystationWorkflowImpl wykorzystywał instancję napisanej przez nas klasy PlaystationActivitiesImpl, a następnie wołał kolejno jej metody i obsługiwał zwracane wartości. Jednak korzystając z Amazon SWF musimy zmienić podejście. Co prawda metody reprezentujące aktywności wciąż są wołane w tej samej kolejności ale tym razem na rzecz obiektu PlaystationActivitiesClientImpl. Dzięki temu metoda nie zostanie uruchomiana w tym samym procesie, który obsługuje przepływ – nawet nie musi być uruchomiona w tym samym systemie – metody zostaną uruchomione asynchronicznie, a użycie typu Premise<T> pozwoli na to aby wartości z metod również zostały zwrócone w sposób asynchroniczny.

Gdybyśmy uruchomili kod bez wskazania argumentów wejściowych Premise<String> nie mielibyśmy pewności w jakiej kolejności wykona dla nas kroki SWF.

Activities Client

PlaystationActivitiesClientImpl jest niczym innym jak proxy dla PlaystationActivitiesImpl, które pozwala aby Amazon SWF poszczególne metody wykonywał asynchronicznie. Interfejs PlaystationActivitiesClient oraz klasa PlaystationActivitiesClientImpl wygenerowane są automatycznie używając adnotacji z interfeju. Workflow worker wykonuję aktywności wołając odpowiednie metody asynchronicznie, które natychmiast zwracają obiekt Promise<T> gdzie T jest typem zwracanym pierwotnie przez PlaystationActivitiesClient. Obiekt Promise<T> jest ogólnie placeholder’em dla wartości metody reprezentującej aktywność, która może ewentualnie zostać zwrócona.

  • gdy metoda zwraca obiekt typu Promise<T>, na początku obiekt jest w stanie unread, co oznacza, że obiekt jeszcze nie reprezentuje poprawnej wartości
  • jeżeli odpowiednia metoda zakończy wykonywanie zadania i zwróci prawdziwy i poprawny wynik, wtedy framework przypisuje odpowiednia wartość do obiektu Promise<T> i ustawi go w stan ready

PlaystationWorkflow Starter

Ostatnim kawałkiem układanki, który pozostał nam do zrobienia jest napisanie klasy, która uruchomi dla nas workflow w chmurze. Stan wykonania każdego przepływu zapisywany jest przez Amazon SWF dlatego mamy możliwość podejrzenia historii wykonania oraz statusów poprzez konsolę AWS – co pokażę za chwilę. Przejdźmy do kodu, który zarejestruje nasz przepływ w chmurze i uruchomi go:

public class PlaystationMain {

    public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InstantiationException {

        ClientConfiguration configuration = new ClientConfiguration().withSocketTimeout(70 * 1000);

/*
        String swfAccessId = System.getenv("AWS_ACCESS_KEY_ID");
        String swfSecretKey = System.getenv("AWS_SECRET_KEY");
        AWSCredentials awsCredentials = new BasicAWSCredentials(swfAccessId, swfSecretKey);
*/

        // Retrieve credentials from ~/.aws/credentials
        AWSCredentialsProvider awsCredentials = new ProfileCredentialsProvider();

        // Verify we can fetch credentials from the provider
        awsCredentials.getCredentials();


        AmazonSimpleWorkflow service = new AmazonSimpleWorkflowClient(awsCredentials, configuration);
        service.setEndpoint("https://swf.us-west-1.amazonaws.com");
        service.setRegion(RegionUtils.getRegion(Regions.EU_WEST_1.getName()));

        String domain = "PlaystationWorkflowDemo";
        String taskListToPoll = "PlaystationTaskListDemo";

        ActivityWorker aw = new ActivityWorker(service, domain, taskListToPoll);
        aw.addActivitiesImplementation(new PlaystationActivitiesImpl());
        aw.start();

        WorkflowWorker wfw = new WorkflowWorker(service, domain, taskListToPoll);
        wfw.addWorkflowImplementationType(PlaystationWorkflowImpl.class);
        wfw.start();

        PlaystationWorkflowClientExternalFactory factory =
                new com.example.swf.playstationplay.PlaystationWorkflowClientExternalFactoryImpl(service, domain);
        com.example.swf.playstationplay.PlaystationWorkflowClientExternal client = factory.getClient();
        client.play();
    }

}

Pierwszy krok to utworzenie i konfiguracja instancji AmazonSimpleWorkflowClient, która wywołuje podstawowe metody serwisu AmazonSWF. Aby to zrobić musimy kolejno:

  • stworzyć obiekt ClientConfiguration i ustawić timeout dla otwartego socket’u – w naszym wypadku będzie to 70 sekund. Wartość ta specyfikuje jak długo dane mogą być transferowane po ustanowionym połączeniu zanim socket zostanie zamknięty
  • stworzyć obiekt, który będzie odpowiadał za identyfikację naszego konta w AWS. Użyłem czegoś co nazywa się ProfileCredentialsProvider – działa on w ten sposób, że czyta plik ~/.aws/credentials, w którym znajduje się nasz accessKey i secretKey wygenerowany dla naszego konta. Jak widać w kodzie mamy zakomentowany fragment, który może pobrać te wartości jako zmienne środowiskowe. Osobiście preferuję to pierwsze rozwiazanie ale najważniejsze jest aby tych wartości nie przechowywać i nie przesyłać jawnie – oczywiście ze względów bezpieczeństwa
  • utworzyć AmazonSImpleWorkflowClient, który reprezentuje workflow i przesłąć do niego AWSCredentialsProvider i ClientConfiguration
  • ustawić url i region dla serwisu SWF

Kolejną rzeczą jaką musimy zrobić będzie ręczne zarejestrowanie domeny PlaystationWorkflowDemo, w której uruchomimy workflow. Jest to czynność jaką należy wykonać ręcznie przez konsolę AWS. Klikamy kolejno Services -> Application Services -> SWF -> Manage Domains -> Register New i podajemy dla workflow name, retention i descirption.

Retention oznacza czas przez który informacja o wykonaniu workflow (wraz z historią) będzie przechowywana w serwisie. Po tym okresie informacja o przebiegach workflow zniknie z panelu.

Uruchamiamy workflow

Uruchamiamy workflow jak zwykły program w Javie i przechodzimy do konsoli AWS aby zobaczyć efekt. Na głównym dashboard SWF widzimy informację o ilości zakończonych wywołań dla domeny PlaystationWorkflowDemo

W zakładce Workflow Executions widać szczegóły dotyczące każdego wykonania przepływu:

W zakładce Workflow Types znajdziemy informację o zarejestrowanych workflow workerach ich wersjach:

Natomiast w zakładce Activity Types możemy znaleść informacje na temat pojedynczych aktywności, które zostały zarejestrowane w Amazon SWF i z których możemy skorzystać przy uruchamianiu workflow:

Podsumowanie

Amazon SWF jest ciekawym podejściem gdzie abstrakcję polegającą na modelowaniu procesów biznesowych przenosimy na zewnątrz naszej aplikacji do chmury. Dostajemy od razu dostęp do gotowych mechanizmów zarządzania zadaniami i śledzenia stanu wykonania procesów. Nie potrafię sobie tylko wyobrazić jak wyglądała by praca na prawdziwym produkcyjnym organizmie gdzie codziennie mamy do czynienia z tysiącem instancji procesów.. chętnie bym zobaczył takie rozwiązanie w akcji 🙂

 

 

 

Leave a Reply

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *