Memory Profiler 1

W OpenEdge od wersji 12.8.9 mamy długo oczekiwane narzędzie Memory Profiler.

Umożliwia ono analizę wykorzystania pamięci przez aplikację w funkcji czasu a także w określonych interwałach, umożliwiając wizualizację wykorzystania pamięci przez kod aplikacji.
Wycieki pamięci występują, gdy aplikacja nie zwalnia niepotrzebnej już pamięci. W konsekwencji wykorzystanie jej przez aplikację rośnie z czasem, co może prowadzić do spadku wydajności, a ostatecznie do wyczerpania zasobów pamięci i awarii aplikacji lub systemu. Wycieki pamięci są szczególnie problematyczne w długo działających aplikacjach lub systemach wymagających wysokiej wydajności i niezawodności działania.
Memory Profiler zapewnia wizualizację wykorzystania pamięci i pomaga programistom ABL w skutecznym wykrywaniu i diagnozowaniu problemów z wykorzystaniem pamięci w ich aplikacjach ABL.

Korzyści płynące z używania narzędzia:

  • Identyfikacja wycieków pamięci — poprzez śledzenie nadmiernego zużycia pamięci i identyfikację wykorzystania pamięci w celu poprawy wydajności aplikacji.
  • Zwiększenie stabilności systemu i aplikacji — śledząc wykorzystanie pamięci, które nie jest zwalniane w działających aplikacjach lub w fazach testowania, zapobiegając w ten sposób potencjalnym awariom i niestabilności systemu w środowiskach produkcyjnych.
  • Debugowanie złożonych problemów — udostępniając statystyki i wizualizację, które upraszczają proces debugowania i przyspieszają rozwiązywanie problemów związanych z pamięcią.
  • Optymalizacja wydajności — identyfikując nieefektywne wykorzystanie pamięci.
  • Zapewnienie stosowania najlepszych praktyk — identyfikując obszary wymagające odpowiednich operacji czyszczenia i zachęcając do stosowania efektywnych praktyk kodowania.

    Z technicznego punktu widzenia to narzędzie jest specjalną instancją PASOE, którą możemy utworzyć korzystając z dostarczonych plików.
    Spakowane pliki te znajdują sie w katalogu [DLC]/servers/redist/oemp-[wersja oe].zip (np. oemp-12.8.9.zip).
    Plik zip kopiujemy do jakiegoś katalogu z pełnymi uprawnieniami i rozpakowujemy, przechodzimy do podkatalogu oemp\bin, a następnie w oknie komend proenv uruchamiamy komendę oemp install.
    Należy pamiętać żeby katalogiem, w którym umieścimy pliki nie byl podkatalog oemp w lokalizacji working directory ponieważ pojawi się wtedy poniższy błąd:

    Jak wybrałem podkatalog c:\oemp i wszystko poszło jak po maśle.

    Proces instalacji jest dość długi, u mnie trwał prawie 5 minut.

    Po zakończonej instalacji mamy nową instancję PASOE oemp, która odwołuje się do bazy danych /db/reportdb. Narzędzie startujemy poleceniem: oemp startup.
    Kiedy instancja jest już uruchomiona, wpisujemy następujący URL w przeglądarce: http://localhost:8880 i dostajemy główny widok Memory Profilera.

    Niedługo napiszę jak korzystać z OE Memory Profilera.

    Promon – analiza danych 3

    Dziś napiszę znowu o promonie odpowiadając przy okazji na kilka Waszych pytań, za które bardzo dziękuję.

    Pierwsze dotyczy strojenia parametru -spin (tylko dla licencji serwera Enterprise).
    Parametr „Spin Lock Retries” (–spin N) służy do określania, ile razy proces próbuje uzyskać zatrzask (latch) do zablokowanego zasobu w pamięci współdzielonej przed “pójściem na drzemkę”. Po wstrzymaniu procesu na określoną liczbę milisekund, proces uruchamia się ponownie i ponownie próbuje uzyskać zatrzask. Proces ten bardzo obciąża procesor dlatego uzywa się go wyłącznie w maszynach wieloprocesorowych, choć w starych systemach jednoprocesorowych zalecano użycie tego parametru z wartością 1, ze względu na to, że i tak był on szybszy niż mechanizm oparty na semaforach. Liczba nieudanych prób uzyskania zatrzasku określana jest w promonie Latch timeouts. Im jest ich więcej tym częściej dochodzi do konfliktów między zatrzaskami i tym gorsza jest wydajność bazy danych.

    Domyślne obliczenie brokera bazy danych to 6.000 x liczba procesorów zgłoszonych przez system operacyjny, co prawdopodobnie było odpowiednie, gdy na serwer przypadało 2 lub 4 rdzenie/procesory, ale teraz, w przypadku maszyn wielordzeniowych z 16 lub więcej rdzeniami, automatycznie obliczona liczba jest prawdopodobnie zbyt wysoka.
    Np. w moim laptopie procesor ma 4 rdzenie i 8 wątków i parametr ten ma domyślną wartość 48.000.
    Miejcie więc na względzie aby sprawdzić tę wartość w promonie:
    R&D -> 4. Administrative Functions 4. Adjust Latch Options


    W tym samym miejscu możemy zmienić tę wartość online (podopcja 1).

    Expert z White Star Software uważa, że wartości z przedziału 5.000 do 20.000 są wystarczające dla zdecydowanej większości baz danych, więc nie ma co szaleć. Należy przede wszystkim monitorować wykorzystanie CPU i w razie gdy osiąga ono duże wartości (90% lub więcej) zmniejszyć -spin online.

    Analizę timeoutów możemy podejrzeć na poniższym ekranie promona – ostatni wiersz.
    R&D -> 3. Other Displays -> 1. Performance Indicators -> Latch timeouts

    Drugim parametrem, o którym chcę tutaj napisać jest -lruskips dostępny także tylko dla serwera Enterprise i ma związek z zatrzaskami przy próbie dostępu do zasobu w puli buforów.
    Łańcuch LRU (Least Recently Used) składa się ze wskaźników do buforów w puli i przy każdym odczycie z tej puli (dla -lruskips = 0) potrzebne jest uzyskanie zatrzasku aby uaktualnić ten łańcuch. Jeśli ustawimy -lruskips = 100 (dobra wartość stosowana obecnie domyślnie) to próba uzyskania zatrzasku LRU będzie co 100-tny dostęp do bufora, co poprawia współbieżność i wydajność.
    Wartość parametru możemy dostosować także online w tym samym miejscu promona co dla -spin (patrz poniższy obrazek). Jeśli mamy dwie pule buforów (-B i -B2) to możemy określić osobno -lruskip i -lruskip2. Mechanizm ten nie jest wykorzystywany w prywatnej puli buforów.

    Jak dobrać długość clustra BI

    W poprzednim wpisie napisałem pod koniec jak z grubsza określić długośc clustra BI. Oczywiście wydłużanie tego clustra ma sens tylko dla licencji serwera bazy Enterprise. W Workgroup nie ma procesów asynchronicznych, a więc duża liczba buforów Dirty nie będzie “rozładowana” przed checkpointemi i cały system znacznie spowolni.
    A zatem, w bazie Enterprise chcemy aby odstęp między checkpointami był np. 4 razy większy niż obecnie, więc wydłużamy 4 razy cluster BI. Operacja jest bardzo prosta i odbywa się na zamkniętej bazie. Jednakże nie możemy kierować się tylko takim podejściem ponieważ szybkość dysków ma swoje granice.

    Do określenia maksymalnego rozmiaru clustra przydatny i powszechnie stosowany jest Furgal Storage Device Test lub po prostu Furgal Test zwany tak od nazwiska autora Mike’a Furgala. Polega on na wykonaniu polecenia BIGROW do pomiaru szybkości dysku poprzez pomiar czasu potrzebnego na utworzenie pliku BI o określonym rozmiarze.

    Po obcięciu pliku BI i wystartowaniu serwera bazy tworzone są domyślnie 4 clustry BI. Jednakże w trakcie normalnej pracy systemu mogą one być niewystarczające i plik BI urośnie do np. 6 clustrów.
    Operacja proutil z opcją bigrow służy do tego aby zwiększyć liczbę clustrów zanim uruchomimy bazę, poprzez wstępne sformatowanie określonej liczby clustrów. Pamiętajmy, że jeśli chcemy mieć liczbę clustrów 6 to podajemy jako parametr 2 (bo 4 są już i tak domyślne). Furgal Test wykazuje silną korelację między czasami wykonania bigrow a wydajnością systemu bazy danych.

    Warto dodać, że mechanizm rozszerzania pliku BI o dodatkowe sformatowane klastry został przeprojektowany i przyśpieszony (buforowany). Wcześniej tylko na Solarisie i AIX, obecnie (od OE 11.7.6 i OE 12.1) jest dostępny na wszystkich platformach w tym Linux i Windows.

    Mamy poniżej dwie komendy.
    proutil [baza] –C truncate bi –bi 16384
    time proutil [baza] –C bigrow 2

    Pierwsza ustawia większy niż domyślny rozmiar clustra bi na 16 MB, a druga mierzy czas sformatowania 6 clustrów (4 + 2).
    Plik BI urośnie zatem do 96 MB. Mamy zatem dwa parametry rozmiar BI i czas sformatowania. Dzielimy je przez siebie i otrzymujemy szybkość dysku w MB na sekundę.
    Nasz maksymalny rozmiar klastra BI powinien być nie większy niż 2-krotność szybkości dysku.
    Zróbmy ten test na testowej bazie na Windows – zamiast polecenia time możemy posłużyć się poniższym skryptem (timer.bat).

    @echo off
    echo %time% < nul
    cmd /c %1
    echo %time% < nul
    

    timer "proutil [baza] –C bigrow 2"

    Czas wykonania komendy to ok. 1 sekunda, czli prędkość wynosi w przybliżeniu 100 MB/sec. Maksymalny cluster zatem to 200 MB. Oczywiście taką komendę warto wykonać kilka razy i wziąć średni pomiar. No i nie ustawiamy od razu ten maksymalny rozmiar clustra ale zwiększamy go stopniowo, obserwując jak działa system (analiza checkpointów w promonie).

    W bazie wiedzy możemy znaleźć informację, że zgodnie z powszechnie akceptowanym od lat punktem odniesienia BIGROW w wielu różnych systemach, zapisanie pliku BI o rozmiarze 100 MB nie powinno zająć dłużej niż 10 sekund. W naszym teście zajęło to tylko 1 sekundę (dysk SSD).

    Promon – analiza danych 2

    Powracając do tematyki związanej z tym najbardziej znanym narzędziem dla administratorów baz, chciałbym napisać dwa słowa o ukrytej opcji, którą, jak się okazuje, nie wszyscy znają.
    Kiedyś była to opcja R&D, niewidoczna z głównego menu ale to było dawno temu.
    Drugie menu było bardziej ukryte. Trzeba było napisać devdbh, Enter i następnie 6.
    Obecnie w nowym promonie ta opcja jest widoczna właśnie na pozycji 6 w meny R&D chociaż możemy wejść do niej także “po staremu”.

    Zawiera zaawansowane opcje dogłębnego monitorowania aktywności i wydajności systemu baz danych OpenEdge.

    Jak widać lista jest całiem długa, ale są to informacje dla zaawansowanych administratorów, lub raczej dla techsupportu PSC. Może się zdarzyć że będziemy poproszeni o przesłanie konkretnych danych z tej listy.

    Powróćmy do danych, które możemy pobrać do własnej analizy. Myślę, że jednymi z najważniejszych są te związane z checkpointami.
    Uruchamiam własny program generujący transakcje w bazie testowej (kopia sports2000) i wchodzę do promona.
    W opcji R&D -> 1. Status Displays -> 9. BI Log sprawdzam parametry zapisu BI na dysku. Blok BI ma 8kB, a cluster tylko 512 kB (domyślna wartość). Zobaczmy jaki to będzie miało efekt w działaniu.

    Przechodzę do głównego menu, a następnie 3. Other Displays -> 4. Checkpoints.
    Popatrzmy na poniższą analizę. Nie wygląda to dobrze…

    Co oznaczają niektóre kolumny (wg dokumentacji):
    Dirty – liczba zmodyfikowanych buforów zaplanowanych do zapisania.
    CPT Q – Liczba buforów zapisanych z kolejki checkpoint przez procesy APW.
    Scan – Liczba buforów zapisanych przez procesy APW podczas cyklu skanowania.
    APW Q – Liczba buforów zapisanych przez kolejkę APW i zastąpionych w łańcuchu LRU przez procesy APW.
    DB Writes – Całkowita liczba buforów bazy danych zapisanych na końcu checkpointu.

    Administratorzy baz chcą aby odstęp między checkpointami (kolumna Len) był rzędu kilku minut, a tu mamy 2 sekundy. Ponadto podczas checkpointu jest nienaturalnie duża ilość zapisów na dysk – kolumna DB Writes (ten parametr w starszych wersjach jest określany jako Flushes). Tak liczne zapisy powodują spadek wydajności.
    Nie uruchomiłem jeszcze procesu APW, więc kolejka APW jest pusta, ale i tak widać od razu, że trzeba koniecznie wydłużyć cluster. Zwiększę go zatem z 0.5 MB do 16 MB. Rozmiar bloku BI zwiększę przy okazji z 8 KB do 16 KB.
    proutil [baza] -C truncate bi -bi 16384 -biblocksize 16

    Ponieważ rozmiar clustra został zwiększony 32 razy to dostęp między checkpointami zwiększy się z 2-3 sekund do ok. 70 sekund. Po odczekaniu zobaczmy jak teraz wygląda analiza checkpointów.

    Zobaczmy, że wartości Dirty nie przekraczają wartości DB Writes, jak poprzednio. Oznacza to, że jest dość czasu podczas checkpointu żeby zapisy zostały dokonane. Jednakże duża liczba tych zapisów może powodować spowolnienie systemu podczas checkpointu. Nie mamy tu na razie procesów asynchronicznych (tylko serwer Enterprise) i kolumna CPT Q pokazuje same zera.

    A teraz co się zmieni po uruchomieniu procesu APW (niebieska ramka). Wszystkie bufory do zapisu są umieszczone w kolejce checkpoint i zapisane przez procesy APW tak, że podczas checkpointu nie ma już żadnych zapisów i system nie będzie spowalniał

    Metoda wydłużenia czasu między checkpointami jest dość prosta i możemy pokusić się o dalsze zwiększenie clustra BI żeby czas wynosił tutaj 2-3 a nawet 5 minut. Pytanie tylko czy nasze dyski są dostatecznie szybkie aby temu podołać. Clustra nie można zwiększać bez końca bez żadnych konsekwencji.

    W następnym wpisie napiszę o prostym teście jak można taką graniczną wartość określić.

    Na koniec jeszcze mała uwaga dotycząca zbierania danych z promona poprzez skrypt. Większość parametrów znajduje się w tablicach VST, zawierających tylko jeden rekord (podawana wartość to różnica wartości z danego pola w czasie) ale czasem tych rekordów jest więcej, jak w przypadku checkpointów. Nie ma dla nich w promonie opcji automatycznego odświeżania. Najłatwiej jest oczywiście napisać program w ABL ale jeśli nie mamy takiej możliwości, to obliczamy po ilu minutach zapełnią się dane wszystkich checkpointów na ekranie (tutaj ok. 15 minut) i ustawiamy uruchomienie skryptu zapisującego tylko ten ekran (np. w cronie) co ten czas.

    Promon – analiza danych

    Administratorzy baz danych stają czasem przed problemem: jak analizować parametry generowane w narzędziu promon w funkcji czasu. Wszystkie te parametry pochodzą z tablic VST i można łatwo napisać odpowiedni program w języku ABL. Problem polega jednak na tym, że administratorzy często nie mają uprawnień do kompilacji.
    Sytuacja nie jest jednak beznadziejna i można sobie z nią poradzić bez większych problemów.

    Promon to narzędzie które można obsługiwać ręcznie, przeglądając określone statystyki lub generować zrzuty ekranowe w sposób automatyczny. Zrzuty takie można potem analizować na różne sposoby. Wystarczy stworzyć wejściowy plik tekstowy, zawierający zestaw znaków sterujących, odpowiadający dokładnie takim samym klawiszom które użylibyśmy przeglądali określone statystyki ręcznie.

    Zanim stworzymy taki plik, musicie wiedzieć, że dla wersji OE12 znajdują się w katalogu [DLC]\bin skrypty dla serwera bazy Enterprise: gather-script-enterprise.bat
    oraz Workgroup: gather-script-wrkgrp.bat.
    Wystarczy uruchomić taki skrypt na naszej bazie i powstanie plik wynikowy zawierający wiele ekranów z menu podstawowego oraz R&D. We wcześniejszych wersjach OE trzeba poszukać skryptu gather. Dokładniejszy opis jest w bazie wiedzy.
    Dla naszych potrzeb stwórzmy prostszy plik sterujący promon_in.txt:

    R&D  # Opcje R&D
    5    # Adjust Monitor Options
    1    # Display page length
    9999 # Enter 9999 for length
    3    # Monitor sampling interval
    30   # Ustaw na 30 sek.
    4    # Pause between displays
    30   # Ustaw na 30 sek.
    6    # Number of auto repeats 
    10   # Ustaw 10 odświeżeń
    t    # Powrót do głównego menu R&D 
    2    # Activity Displays
    1    # Summary
    A    # Tryb automatyczny

    Plik ten zawiera określone wybory menu i ustawienia oraz komentarz. Oczywiście dla wysterowania promona komentarz musimy usunąć. Plik więc będzie wyglądał następująco:

    R&D
    5
    1
    9999
    3
    30
    4
    30
    6
    10
    t
    2
    1
    A

    W tym prostym przykładzie dostaniemy zrzut 10 ekranów z ogólnej aktywności bazy. Każdy ekran wygeneruje się co 30 sekund.
    Uruchamiamy zatem promona na bazie testowej poleceniem:
    promon [baza] < promon_in.txt > promon_out.txt
    Można użyć opcjonalnie parametru -NL, szczególnie do analizy problemów z konfliktami aktywności w pamięci współdzielonej. Tutaj można go pominąć. Nas będzie interesował parametr BI Writes.
    Pominąłem jeden krok, uruchomiłem wcześniej program, który dokonuje losowo transakcji na tablicy Customer (kopia bazy sports2000). Krok ten zostawiam czytelnikom.

    Promon wygeneruje plik wynikowy promon_out.txt, zawierający zrzuty ekranowe. Z uwagi na jego obszerność wyświetlam tylko interesujący nas fragment. Zgodnie z ustawieniami plik zawiera 10 wygenerowanych takich ekranów.

    Event                  Total  Per Sec |Event                  Total  Per Sec
    
    Commits            17245004      99.4 |DB Reads                332       0.0
    Undos               1864747      10.8 |DB Writes            904278       5.2
    Record Reads       38455612     221.7 |BI Reads              61998       0.4
    Record Updates     15614825      90.0 |BI Writes           1380881       8.0
    Record Creates      1630171       9.4 |AI Writes                 0       0.0
    Record Deletes      1630180       9.4 |Checkpoints           20601       0.1
    Record Locks       82489712     475.5 |Flushed at chkpt     883672       5.1
    Record Waits              0       0.0 |Active trans              0
    

    Dla nas jest ważne aby wyekstrahować określone informacje, np. linie zawierające określone dane, jak badany parametr BI Writes.
    W systemie Windows możemy posłużyć się komendą findstr. Poniżej ściąga jak używać tej komendy:
    findstr [szukane słowo] promon_out.txt > wynik.txt
    findstr /C:”[szukana fraza]” promon_out.txt > wynik.txt

    W systemie Linux/Unix możemy posłużyć się komendą grep.
    Uruchamiamy tutaj następująca komendę.
    findstr /C:”BI Writes” promon_out.txt > bi.txt
    Zawartość pliku bi.txt wygląda następująco.

    Record Updates        10859     362.0 |BI Writes               938      31.3
    Record Updates        10879     362.6 |BI Writes              1005      33.5
    Record Updates        10476     349.2 |BI Writes               872      29.1
    Record Updates        10930     364.3 |BI Writes              1005      33.5
    Record Updates        10933     364.4 |BI Writes               938      31.3
    Record Updates        10850     350.0 |BI Writes               938      30.3
    Record Updates        10578     352.6 |BI Writes               939      31.3
    Record Updates        10847     361.6 |BI Writes               938      31.3
    Record Updates        10855     361.8 |BI Writes              1006      33.5
    Record Updates        10828     360.9 |BI Writes               938      31.3
    

    Juz jest nieźle, bo mamy tylko 10 potrzebnych linii. Teraz trzeba zaimportować te dane do arkusza jak Excell czy LibreOffice. Tu posłużyłem się LibreOffice, w którym można wygodnie ustalić miejsca podziału kolumn.

    Niepotrzebne kolumny można usunąć. Potrzebujemy jeszcze osi czasu, ale to prosta sprawa bo odczytujemy tylko czas dla pierwszego odczytu, dodajemy 30 sekund do następnego a pozostałe czasy wygenerują się automatycznie po przeciągnięciu kursorem myszy.

    Na koniec generujemy wykres w celu analizy. Tutaj jest akurat w Excellu.

    Jak widać cały proces nie jest trudny i gdy raz go przejdziemy nie będzie kłopotu aby powtarzać go dla innych danych.
    Następnym razem napiszę o jeszcze kilku analizach w promonie.

    Dynamic Data Masking II

    Kolejnym krokiem jest zdefiniowanie masek dla wybranych pól. Maski te zasłaniają nieautoryzowanym użytkownikom część lub całą zawartość pola. Mamy cztery rodzaje takich masek: domyślna, częściowa (partial), literal, null.

    Wszystkie ustawienia DDM w ABL (tak jak w poprzednim artykule) realizujemy poprzez metody DataAdminService. Zaczynamy od maski domyślnej, w ktorej mamy małe pole do menewru. Pola znakowe są oznaczane jako XXX (liczba X zależy od maskowanych danych), pola decimal 0.00 itd. Maska jest ustawiana poprzez prefiks “D:”. Zaczniemy od pola Balance.

    USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.
    
    DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
    DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.
    
    service = NEW DataAdminService(LDBNAME("DICTDB")).
    lResult = service:setDDMConfig("Customer","Balance","D:","#DDM_SEE_ContactInfo").
    

    Ustawmy taką samą maskę także na pola znakowe SalesRep i State.

    Żeby ustawienia maskowania były widoczne trzeba aktywować usługę DDM w bazie poleceniem:
    proutil sports2020 -C activateddm.

    Do testowania użyjemy prostej procedury findCust.p.

    // findCust.p
    FIND FIRST customer.
    DISPLAY customer EXCEPT comments WITH 1 COLUMN.
    PAUSE.
    

    Uruchamiamy dwie sesje klienta z bazą sports2020 logując się raz jako Admin i raz jako User (patrz poprzedni artykuł). Poniżej widać porównanie danych dla obu sesji – różnice zaznaczyłem na czerwono.

    Maska literal służy do maskowania danych gdy chcemy zamiast originalnej wartości podać własną. Należy pamiętać, że dla każdego rodzaju maski obowiązuje zasada zgodności typów, i tak tutaj dla pola znakowego City możemy podać własną maskę np. “UKRYTE“, dla pola PostalCode 00000 itd.
    Maska literal jest ustawiana poprzez prefiks “L:”.

    USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.
    
    DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
    DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.
    
    service = NEW DataAdminService(LDBNAME("DICTDB")).
    lResult = service:setDDMConfig("Customer","City","L:UKRYTE","#DDM_SEE_ContactInfo").
    lResult = service:setDDMConfig("Customer","PostalCode","L:00000","#DDM_SEE_ContactInfo").
    


    Pora przejść do najciekawszej maski, czyli częściowej, dającej najwięcej możliwości, ale stosowanej tylko dla pól znakowych.
    Składnia maski wygląda tak: P:prefix,char[:maxchars][,suffix]

    prefix – określa liczbę niezamaskowanych znaków, które mogą być wyświetlane na początku pola.
    char[:maxchars] – określa pojedynczy znak ASCII, który ma być użyty do ukrycia wartości kolumny, maxchars (wartość opcjonalna) określa maksymalną liczbę znaków do zamaskowania przy użyciu podanego znaku ASCII, reszta ciągu jest przycinana.
    [,suffix] – (wartość opcjonalna) określa liczbę znaków na końcu pola, które mogą być niezamaskowane.

    USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.
    
    DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
    DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.
    
    service = NEW DataAdminService(LDBNAME("DICTDB")).
    lResult = service:setDDMConfig("Customer","Phone","P:5,*:2,4","#DDM_SEE_ContactInfo").
    

    Testujemy pole Phone. Działa to w następujący sposób: pierwsze 5 znaków jest niezamaskowana, podobnie jak ostatnie 4 znaki, to co w środku jest zamaskowane maksymalnie 2 znakami “*“. Jeśli długość pola będzie miała tylko 10 znaków to będzie wyświetlana tylko jedna gwiazdka itd.

    Ostatnim rodzajem maski jest null, którą można stosować dla każego typu danych. Zamaskowane wartości są wyświetlane jako “?“. Maskę null ustawiamy za pomocą prefiksu N:.

    USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.
    
    DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
    DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.
    
    service = NEW DataAdminService(LDBNAME("DICTDB")).
    lResult = service:setDDMConfig("Customer","CreditLimit","N:","#DDM_SEE_ContactInfo").
    lResult = service:setDDMConfig("Customer","Terms","N:","#DDM_SEE_ContactInfo").
    


    Jeśli trzeba usunąć maskowanie z pola musimy posłużyć się metodą unsetDDMMask. Usuńmy maskę np. z pola State.

    USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.
    
    DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
    DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.
    
    service = NEW DataAdminService(LDBNAME("DICTDB")). 
    
    lResult = service:unsetDDMMask("Customer","State").
    


    Przy pomocy metod DataAdminService można manipulować rolami, tagami autoryzacji itp. Możemy podejrzeć np. tag i maskę dla danego pola.

    USING OpenEdge.DataAdmin.*. 
    VAR DataAdminService oDAS. 
    VAR CHARACTER cMask. 
    VAR CHARACTER cAuthTag. 
    
    oDAS = new DataAdminService (ldbname("DICTDB")). 
    oDAS:GetFieldDDMConfig ("Customer","Phone", OUTPUT cMask, OUTPUT cAuthTag).
    MESSAGE cMask SKIP cAuthTag
    VIEW-AS ALERT-BOX. 
    

    Poniżej widać dane dla pola Phone.

    OK, na tym na razie kończę. Do tematu powrócę jeśli będą od Was jakieś pytania.

    Dynamic Data Masking I

    Na początku chciałbym podać nieco wiadomości wprowadzających do Dynamic Data Masking (DDM). Najprościej mówiąc jest to zabezpieczenie danych w bazie na poziomie logicznym, ale definiowane przez administratora.
    Wyobrażmy sobie sytuację gdy deweloper tworzy aplikację, w której wyświetlane są dane wraźliwe jak np. numer telefonu, adres lub nawet pesel czy miesięczne zarobki. Z drugiej strony są osoby w firmie, które powinny te dane widzieć. Możemy to rozwiązać na poziomie pisania aplikacji lub zrobić to globalnie na poziomie bazy. Takie rozwiązanie zmusza każdego użytkownika do zalogowania się w bazie i, w zależności od uprawnień, daje wgląd do wybranych pól w tabelach.
    Administrator DDM (lub administrator zabezpieczeń) może skonfigurować maskę dla pól tabeli, która ukrywa poufne dane w zestawie wyników zapytania. Kontroluje on również uprawnienia dostępu użytkowników do przeglądania niezamaskowanych wartości określonych pól.
    Żeby korzystać z DDM trzeba mieć licencję na dodatek The OpenEdge Advanced Security (Progress OEAS) i serwer bazy danych Enterprise. Minimalna wersja to 12.8.4.

    OK, tworzymy testową bazę, kopię bazy sports2020 i włączamy funkcję DDM.
    proutil sports2020 -C enableddm
    Sprawdzamy czy funkcja ta jest dodana (na razie jeszcze nieaktywna).
    proutil sports2020 -C describe

    Pierwszym krokiem jest dodanie użytkowników do bazy. W dokumentacji online jest to zrealizowane przez program w obiektowym ABL. Ja zrobię to standardowo w narzędziu Data Administration -> Admin -> Security -> Edit User List.
    Dodaję użytkowników: Admin, User. Oczywiście można wykorzystać tutaj konta użytkowników zdefiniowanych nie w bazie ale w zewnętrznych systemach uwierzytelniania.

    Loguję się jako Admin i ustawiam go jako security adminisrtator Admin -> Security -> Security Administrators.

    Warto zablokować dostęp podczas runtime’u dla niezalogowanych użytkowników oraz ustawić sprawdzanie uprawnień kont także podczas runtime’u.Admin -> Database Options

    Aby określić uprawnienia użytkownika do dostępu do zamaskowanych danych, trzeba utworzyć w kodzie ABL rolę dla użytkowników zdefiniowanych w bazie (operacja mapowania użytkownika do roli).
    Poniższy przykładowy kod tworzy rolę myRole, którą wykorzystamy do zdefiniowania uprawnień przyznawanych użytkownikom do demaskowania danych:

    USING OpenEdge.DataAdmin.*.
    
    VAR DataAdminService service. 
    VAR IRole oRole.
    VAR LOGICAL lResult.
    
    service = NEW DataAdminService(LDBNAME("DICTDB")).
    
    oRole = service:NewRole("myRole"). 
    oRole:Description = "Role for DDM Admin".
    oRole:IsDDM = true.
    lResult = service:CreateRole(oRole). 
    
    DELETE OBJECT service.
    

    Następnym krokiem jest utwórzenie tagu autoryzacji i powiązania go z rolą utworzoną w poprzednim kroku. Tag autoryzacji ustanawia połączenie między zdefiniowanymi przez użytkownika rolami DDM a polami tabeli, do których ma zostać zastosowana maska. Nazwa tagu musi zaczynać się od #DDM_SEE_.

    Poniższy kod kojarzy tag autoryzacji #DDM_SEE_ContactInfo z rolą myRole:

    USING OpenEdge.DataAdmin.*.
    
    VAR DataAdminService service.
    VAR IAuthTag oTag.
    VAR LOGICAL lReturn.
    
    service = NEW DataAdminService (LDBNAME("DICTDB")).
    
         oTag = service:NewAuthTag("#DDM_SEE_ContactInfo"). 
         oTag:RoleName = service:GetRole("myRole"):Name.
         oTag:description = "To see contact info".
         lRETURN = service:CreateAuthTag(oTag).
    
    

    No i wreszcie przydzielamy zdefiniowaną rolę użytkownikowi Admin.

    USING OpenEdge.DataAdmin.*.
     
    VAR DataAdminService service.
    VAR IGrantedRole oRole.
    VAR IUser oUser.
    VAR LOGICAL lResult = FALSE.
     
    service = NEW DataAdminService(LDBNAME("DICTDB")).
    oRole = service:NewGrantedRole().
    oRole:Role = service:GetRole("myRole").
    oUser = service:GetUser("Admin").
     
    IF VALID-OBJECT(oRole:Role) AND VALID-OBJECT(oUser) THEN DO:
      oRole:Grantee = oUser:Name.
     
       IF oUser:Domain:Name NE "" THEN
          oRole:Grantee = oRole:Grantee  + "@" + oUser:Domain:Name.
     
        oRole:CanGrant = false. // Cannot grant to others.
        // Granting Role to Admin
       lResult = service:CreateGrantedRole(oRole).
       
    END.
    

    Następnym krokiem jest zdefiniowanie maski dla wybranych pól i przetestowanie przykładów, ale tym zajmiemy się w następnym artykule. Teraz chciałbym pokazać gdzie można szukać informacji o zefiniowanych rolach i tagach.
    Elementy te zostały dodane do metaschematu bazy, a jeśli tak, to są w określonych nowych tablicach VST.
    Po pierwsze możemy zrzucić dane do plików poprzez opcję w Data Administration Admin -> Dump Data and Definitions -> Security Permissions.
    Poniżej widać pierwsze linie z dwóch plików, w których są zdefiniowane przez nas dane.

    _sec-auth-tag.d

    "myRole" "#DDM_SEE_ContactInfo" "Can see contact info"
    ...
    

    _sec-granted-role.d

    "f82629bb-b26d-6b83-d314-c5d870ea3fee" "Admin" "myRole" no "Admin" ""
    ...
    

    Informacja w plikach pochodzi z poniższych ukrytych tablic VST.

    Wykorzystując je możmy sami wygenerować potrzebne informacje jak w poniższym przykładzie.

    FIND FIRST _sec-granted-role NO-LOCK.
       DISPLAY  _grantee _role-name  WITH SIDE-LABELS.
    
    FIND FIRST _sec-auth-tag NO-LOCK.
       DISPLAY  _sec-auth-tag._role-name _auth-tag SKIP 
                _description WITH SIDE-LABELS.
    


    OK, następnym razem weźmiemy na warsztat tworzenie różnych masek i testowanie programów.

    Bezpieczeństwo PASOE Manager

    O bezpieczeństwie PASOE pisałem już niejednokrotnie i jest to związane z różnymi aspektami zabezpieczeń. Tym razem będzie krótko o Tomcat Manager i OpenEdge Manager, o czym zresztą obiecałem w poprzednim artykule.

    Służą one do zarządzania aplikacjami webowymi i wielosesyjnymi agentami i przy braku odpowiedniego zabezpieczenia mogą stać się potencjalnym celem ataków hakerskich.

    Progress zdecydowanie zaleca podjęcie następujących działań w celu zapewnienia bezpieczeństwa instancji PASOE i wdrożonych aplikacji internetowych (ten proces jest dobrze opisany w dokumentacji):

    • Ograniczenie dostępu do adresów URL dla zarządzania zdalnego poprzez Tomcat Manager i OpenEdge Manager.
    • Zmiana domyślnego hasła dla serwera Tomcat (tomcat/tomcat).

    Pierwsze z nich polega na edycji tekstowego pliku: CATALINA_BASE/webapps/[manager]/META-INF/context.xml, a konkretnie elementu: <Valve className=”org.apache.catalina.valves.RemoteAddrValve…”

    Poniżej widać fragment pliku context.xml dla wersji OE 12.8 – element Valve nie był wzięty w komentarz, a więc dostęp zdalny był możliwy tylko dla adresu localhost (127.0.0.1), co widać poniżej (we wcześniejszych wersjach element był zakomentowany i domyślny dostęp był dla wszystkich adresów).

    Dostęp do tego samego managera wpisując domyślny adres routera jest już zabroniony.

    Drugim zabezpieczeniem jest zmiana domyślnego hasła tomcata (i ew. nazwy użytkownika, tomcat:tomcat) w plikach instancji PASOE xml oraz w OpenEdge Managaer/Explorer.

    Zaczynamy od zrobienia kopii plików instancji ../conf/server.xml oraz ../conf/tomcat-users.xml.

    W pliku server.xml wyłączamy domyślną bazę z danymi użytkowników UserDatabase, tzw. Realm.

    <!-- feature:begin:UserDatabase:off
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
                resourceName="UserDatabase" />
     feature:end:UserDatabase:off -->

    Dodajemy nową bazę, która używa klasy CredentialHandler class, np.:

    <!-- feature:begin:UserDatabase-pbkdf2:on -->
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
                resourceName="UserDatabase" >
              <CredentialHandler className="org.apache.catalina.realm.SecretKeyCredentialHandler"
                   algorithm="PBKDF2WithHmacSHA512"
                   iterations="10000"
                   saltLength="16"
                   keyLength="256" />
            </Realm>
    <!--     feature:end:UserDatabase-pbkdf2:on -->

    PBKDF jest zaawansowanym i bardzo silnym algorytmem szyfrowania haseł, zgodnym z FIPS. Im więcej podamy iteracji tym lepiej ale i proces szyfrowania jest wolniejszy. Zapisujemy plik server.xml.

    Teraz trzeba wygenerować nowe hasło. W proenv, przechodzimy do katalogu: DLC/servers/pasoe/bin. Przykładowa instrukcja, która uwzględnia parametry szyfrowania z pliku server.xml wygląda następująco:
    digest -a PBKDF2WithHmacSHA512 -i 10000 -s 16 -k 256 -h “org.apache.catalina.realm.SecretKeyCredentialHandler” mycat
    Mycat to oczywiście moje nowe hasło w miejsce tomcat.

    Komenda zwraca zaszyfrowaną wartość hasła, którą oczywiście musimy skopiować i umieścić w pliku CATALINA_BASE/conf/tomcat-users.xml.
    Warto przy okazji zrobić przegląd kont użytkowników i zostawić tylko niezbędne. Nazwę admina też można zmienić. Ja zmieniam teraz tylko hasło.

    Startuję moją instancję testową i widzę, że OE Manager nie jest dostępny. Tak samo będzie jeśli będę chciał dostać się do managera: localhost:8833.

    Widoki analizy sesji i żądań są wyłączone.

    Podobnie metryki dla aplikacji.

    Wchodzę do konfiguracji i w polach Tomcat manager password oraz OpenEdge manager password wpisuję moje nowe hasło jako zwykły tekst: mycat.
    Trzeba chwilę odczekać aż zmiany będą widoczne, a następnie nacisnąć przycisk Test Connection.

    Połączenie powinno się teraz udać.

    A widoki są dostępne

    Nowe hasła są zapisane w pliku %DLC%\properties\pasmgr.properties

    Nowe hasło musimy podać także jeśli chcemy wejść z poziomu managera tomcata i przeglądać dane np. przy użyciu Swaggera.

    Modele zabezpieczeń serwera PASOE

    “Temat PASOE nie schodzi z afisza”, można powiedzieć, ale co zrobić jeśli to wciąż kluczowy produkt w architekturze aplikacji progressowych i w dodatku wciąż rozwijany. Zwykle piszę na temat, z którym związane są pewne niejasności i pytania. Tak samo jest tym razem. Chodzi o tworzenie instancji PASOE jako model deweloperski i produkcyjny. Każdy z tych modeli związany jest z inną licencją, jako że jest to nieco inny produkt. Zestawmy ich krótkie porównanie:

    Model deweloperski:

    • kompilacja procedur
    • brak zabezpieczeń w konfiguracji
    • wbudowana aplikacja web oeabl.w (ROOT). Wszystkie warstwy transportowe wdrożone i włączone
    • ograniczenie do 5 jednoczesnych żądań. 1 agent
    • dołączona instancja dla testów (oepas1)

    Model produkcyjny:

    • brak możliwości kompilacja procedur w locie
    • konfiguracja zabezpieczona
    • wbudowana aplikacja web oeabl.w (ROOT). Wszystkie warstwy transportowe wdrożone ale wyłączone
    • liczba jednoczesnych żądań jest ograniczona jedynie licencją
    • brak instancji testowej

    Jeśli mamy utworzoną już instancję i chcemy sprawdzić jej tryb to możemy zrobić to na różne sposoby np.:

    • w pliku openedge.properties:srvrAppMode=development | production
    • w pliku appserver.properties:spsc.as.security.model=development | production
    • uruchomić komendę:pasman env –I nazwa_instancji

    OK, przejdżmy teraz do kilku opcji tworzenia instancji. Po pierwsze mamy do dyspozycji parametr -f, którego użycie spowoduje wdrożenie wszystkich aplikacji webowych (pliki .war) z ${DLC}/servers/pasoe/webapps do nowej instancji. Przyda on się nam za chwilę.

    Po drugie, możemy tworzyć instancję o określonym trybie, korzystając z parametru -Z. Mamy tu trzy wartości do wyboru: dev, prod i pas.

    Większość z Was słyszała zapewne o dwóch pierwszych ale mało kto wie po co jest i jak działa trzecia wartość (pas). Napiszę o tym za chwilę.

    Pierwsza watość (dev) najmniej nas tutaj interesuje; druga jest o wiele ciekawsza ponieważ stwarza wrażenie, że jeśli jej użyjemy to mamy od razu wersję produkcyjną. Załóżmy, że mamy licencję deweloperską i tworzymy nową instancję PASOE o nazwie myProdInst (numery portów są pominięte):

    pasman create -Z prod %WRKDIR%\myProdInst

    Pytanie brzmi: czy stworzyliśmy wersję produkcyjną czy wciąż deweloperską?

    Sprawdzamy np. komendą pasman.


    Widać, że znacznik security model = production.

    Teraz zobaczmy warstwy transportowe dla domyślnej aplikacji webowej ROOT w narzędziu OEM/OEE (mozna alternatywnie sprawdzić w pliku konfiguracyjnym):


    Wszystkie warstwy są wdrożone i wyłączone, jak dla modelu production.

    Pomimo tego nasza instancja nie jest do końca produkcyjna i firma Progress zdecydowanie zaleca używanie w środowisku produkcyjnym licencji modelu produkcyjnego. Po pierwsze w naszej nazwijmy quasi-produkcyjnej instancji nie są wymagane r-kody, a więc procedury mogą być kompilowane w locie, co stanowi duże zagrożenie. Po drugie instancja nie może zmienić uprawnień w katalogu $DLC/servers/pasoe. I wreszcie nie można uruchomić więcej niż 1 agenta z 5 sesjami.

    Powróćmy teraz do trzeciej wartości dla opcji -Z pas. Jest to instancja podobna do tej -Z dev ale domyślną aplikację ROOT (oeabl.war) zastępuje aplikacja noaccess.war. W rzeczywistości nie jest to serwer PASOE, a serwer aplikacji Tomcat.

    Zróbmy test i stwórzmy przykładową instancję poleceniem (numery portów pominięte):

    pasman create -Z pas %WRKDIR%\myProdPas


    Widać, że nie ma żadnej aplikacji ABL oraz wdrożonej aplikacji webowej opartej na oeabl.war i start tekiej instancji przy pomocy komend OpenEdge np. pasman start czy tcman pasoestart nie powiedzie się. Uda się, natomiast start przy pomocy tcman start (to komenda Tomcata).

    Jakakolwiek próba zdalnego dostępu do takiej instancji kończy się komunikatem “Request refused”. Instancja taka to zabezpieczony pusty kontener. Żeby z niego skorzystać musimy wdrożyć w nim aplikacje webowe. Metoda ta jest szczególnie wygodna gdy mamy wygenerowane pliki .OEAR (OpenEdge Application Archive), przy pomocy których możemy importować całe fragmenty architektury PASOE: aplikacje ABL, aplikacje webowe, zabezpieczenia… Pisałem o tym kiedyś krótko (pasman export/import). Po więcej zapraszam do dokumentacji.

    Pozostaje jeszcze pytanie czy korzystać w instancjach produkcyjnych z managerów? Jeśli widzimy taką konieczność to oczywiście tak, ale trzeba je najpierw zabezpieczyć, o czym napiszę w następnym artykule.

    Kafka i OpenEdge cz. II

    W poprzednim artykule pokazałem jak zainstalować i skonfigurować lokalne środowisko Kafki a teraz pokażę jak zintegrować je z OpenEdgem.
    Niektórzy z Was, którzy będą potrzebować zintegrować się z istniejącą architekturą Kafki, będą zainteresowani od tego właśnie miejsca.

    Najlepiej w tym celu sprawdzić dokumentację OE dla danej wersji np. online. Ważne, że trzeba posiadać wersję OE 12.5 lub wyższą.

    Pierwszym krokiem jest pobranie biblioteki Apache Kafka C/C++ dla Windows lub Unixa (link Download package po prawej stronie). Minimalna wymagana wersja to 1.7, ale zaleca się pobranie ostatniej wersji.

    Gdy plik mamy już pobrany zmieniamy jego rozszerzenie z nupkg na zip i rozpakowujemy. W przypadku Windows będą nas interesowały pliki z podkatalogu runtimes\win-x64\native\, które umieszczamy w katalogu roboczym naszej aplikacji ABL.

    Dodajemy do PROPATH następujace biblioteki:

    • $DLC/tty/netlib/OpenEdge.Net.pl
    • $DLC/tty/messaging/OpenEdge.Messaging.pl

    Teraz możemy wziąć się za napisanie procedury w ABL, a raczej za pobranie jej z dokumentacji i małą kastomizację.

    /* producer.p */
    
    using OpenEdge.Messaging.*.
    using OpenEdge.Messaging.Kafka.*.
    
    block-level on error undo, throw.
    
    var RecordBuilder recordBuilder.
    var KafkaProducerBuilder pb.
    var IProducer producer.
    var IProducerRecord record.
    var ISendResponse response.
    
    pb = cast(ProducerBuilder:Create("progress-kafka"), KafkaProducerBuilder).
    
    pb:SetBootstrapServers("172.22.13.101:9092").
    pb:SetClientId("test client").
    pb:SetBodySerializer(new StringSerializer()).
    pb:SetKeySerializer(new StringSerializer()).
    
    pb:SetProducerOption("message.timeout.ms", "10000").
    
    producer = pb:Build().
    
    recordBuilder = producer:RecordBuilder.
    recordBuilder:SetTopicName("mytopic").   
    
    Find first customer.
    recordBuilder:SetBody(name).
    
    record = recordBuilder:Build().
    
    response = producer:Send(record).
    
    producer:Flush(1000).
        
    repeat while not response:Completed:
        pause .1 no-message.
    end.
    
    if response:Success then do:
       message "Send successful" view-as alert-box.
    end.
    else do:
        undo, throw new Progress.Lang.AppError("Failed to send the record: " +
            response:ErrorMessage, 0).
    end.
     
    
    catch err as Progress.Lang.Error :
        message err:GetMessage(1) view-as alert-box.
    end catch.
    
    finally:
        delete object producer no-error.
    end.
    

    Po pierwsze wstawiam własny adres IP w metodzie SetBootstrapServers i nazwę topicu mytopic.
    Potem ustawiam SetBodySerializer() (metoda ustawia proces serializacji dla treści wiadomości) na StringSerializer. Serializator zajmuje się konwersją danych na bajty w celu przesłania do topicu. OpenEdge udostępnia również MemptrSerializer i JsonSerializer, jak również tworzenie własnych niestandardowych serializatorów.
    I wreszcie znajduję pierwszy rekord customer i ustawiam jego nazwę (name) jako wartość do przesłania.
    Po uruchomieniu procedury sprawdzam co pojawiło się w topicu Kafki. Na czerwono zaznaczyłem wartość przesłanej wartości (Lift Tours).

    Teraz pora na odczyt wartości z topicu Kafki w procedurze ABL.

    /* consumer.p */
    
    block-level on error undo, throw.
     
    using OpenEdge.Messaging.ConsumerBuilder from propath.
    using OpenEdge.Messaging.IConsumer from propath.
    using OpenEdge.Messaging.IConsumerRecord from propath.
    using Progress.Json.ObjectModel.JsonConstruct from propath.
    using Progress.Json.ObjectModel.JsonObject from propath.
     
    var ConsumerBuilder cb.
    var IConsumer consumer.
    var IConsumerRecord record.
    //var Progress.Lang.Object messageBody.
    var char messageBody.
     
    cb = ConsumerBuilder:Create("progress-kafka").
     
    // Kafka requires at least one bootstrap server host and port.
    cb:SetConsumerOption("bootstrap.servers", "172.22.13.101:9092").
    
    // Explicitly disable auto commit so it can be controlled within the application.
    cb:SetConsumerOption("enable.auto.commit", "false").
    
    // Identify the consumer group. The consumer group allows multiple clients to
    //  coordinate the consumption of multiple topics and partitions
    //cb:SetConsumerOption("group.id", "my.consumer.group").
    cb:SetConsumerOption("group.id", "test-consumer-group").
    
    // Specify whether the consumer group should automatically be deleted when
    //  the consumer is garbage collected.
    cb:SetConsumerOption("auto.delete.group", "true").
    
    // Configure the consumer's deserializer in order to convert values from
    //  the network messages to string objects.
    cb:SetConsumerOption("value.deserializer", "OpenEdge.Messaging.StringDeserializer").
    
    // Set the consumer starting position to the most recent message
    cb:SetConsumerOption("auto.offset.reset", "latest").
         
    // identify one or more topics to consume
    cb:AddSubscription("mytopic").
         
    // build the consumer
    consumer = cb:Build().
              
    // loop forever receiving and processing records.
    repeat while true:
        // request a record, waiting up to 1 second for some records to be available
        record = consumer:Poll(1000).
             
        if valid-object(record) then do:
               
            // acknowledge the message so the client can resume where it leaves off
            // the next time it is started
            consumer:CommitOffset(record).
    
            MESSAGE record:TopicName VIEW-AS ALERT-BOX.   
    
           messageBody = record:Body:ToString().      
           message messageBody view-as alert-box.
          
        end.
             
    end.
         
    catch err as Progress.Lang.Error :
        message err:GetMessage(1) view-as alert-box.
    end catch.
    
    finally:
        delete object consumer no-error.
    end.
    

    Uruchamiamy procedurę producer.p i consumer.p. Widać, że pierwsza wyświetla komunikat o poprawnym wysłaniu, a druga prawidłową wartość pobraną z kolejki.

    Na koniec jeszcze jedna informacja związana z ustawianiem wartości parametrów. Otóż można to zrobić na dwa sposoby: poprzez użycie metod akceptujących wartości silnie typizowane (tzw. strongly-typed) lub używając par nazwa-wartość.
    Obie poniższe linie kodu są równoważne:

    pb:SetProducerOption("bootstrap.servers", "172.22.13.101:9092").
    pb:SetBootstrapServers("172.22.13.101:9092").
    
    1 2 3 5