Kafka i OpenEdge cz. I

O integracji OpenEdge z Apache Kafka chciałem napisać od dawna. Najpierw musiałem pokonać pewne kłopoty techniczne, tak  aby pokazać pełna ścieżkę od instalacji tejże Kafki do napisania programów w ABL do wysyłania i pobierania danych, ale po kolei. Co to jest Kafka.

Na głównej stronie Apache Kafki możemy przeczytać w tłumaczeniu:

Apache Kafka to rozproszona platforma do strumieniowego przesyłania komunikatów typu open-source, z której korzystają tysiące firm w celu zapewnienia wydajnego przetwarzania danych, analizy przesyłania strumieniowego, integracji danych i aplikacji o znaczeniu krytycznym.

Komunikaty w Kafce pogrupowane są w tzw. topiki (tematy, ang. topic). Nadawca (producer), jak i odbiorca (consumer) powiązani są z jednym lub wieloma topikami.
Rzućmy okiem na uproszczony schemat systemu Kafki.

Kafka zapewnia przesyłanie danych w czasie zbliżonym do rzeczywistego ze źródeł, takich jak bazy danych, aplikacje, czujniki, urządzenia mobilne, usługi w chmurze itd.

Nadawcy i odbiorcy mogą być procesami napisanymi w różnych językach programowania, na różnych systemach operacyjnych; musi istnieć tylko mechanizm aby wpięli się do systemu. Dla klientów ABL również jest taki mechanizm ale o tym napiszę później.

Klientom Progressa ta architektura może przypominać produkty z grupy Sonic, jednakże Kafka jest technologią nieporównywalnie popularniejszą, stosowaną przez wiele wielkich i rozpoznawalnych firm jak Netflix, Twitter, Spotify, Cisco, LinkedIn i dziesiątki innych. Zresztą różnic jest więcej. Kafka jest platformą o wysokiej wydajności i skalowalności, przesyłane komunikaty nie są automatycznie kasowane z kolejki po odczytaniu (mogą być przechowywane bez ograniczenia czasowego) itd…

Komunikaty (nazywane także zdarzeniami lub rekordami) z danego topiku dopisywane są na końcu tzw. partycji. Partycja to uporządkowany rejestr komunikatów, mówiąc niskopoziomowo, jest to plik na dysku brokera, do którego zapisywane są komunikaty. Aby konsument był w stanie odebrać określony komunikat / sekwencję komunikatów – musi znać pozycję ostatnio przeczytanego komunikatu.

OpenEdge udostępnia nowe OpenEdge.Messaging API do wysyłania wiadomości do klastra Kafki poprzez Kafka producer i odbierania wiadomości z tego klastra przez Kafka consumer.

Tyle wstępnych informacji. Chcę Wam pokazać cały proces od zainstalowania Kafki na lokalnej maszynie aby każdy mógł przetestować ten system.

Kafka jest przeznaczona do uruchamiania w systemie Linux i Mac i nie jest przeznaczona do natywnego uruchamiania w systemie Windows. Dlatego w przypadku tej platformy zaleca się stosować technologię Docker lub WSL2.

WSL2 (Windows Subsystem for Linux 2) zapewnia środowisko Linux dla komputera z systemem Windows 10+, które nie wymaga maszyny wirtualnej.

Moja konfiguracja wygląda następująco: Windows 10, zainstalowany WSL2 oraz Windows Terminal, w którym mam dostęp do Linuxa Ubuntu.

Instaluję Apache Kafka korzystając z oryginalnej strony. Polega to (ach ten Linux!) jedynie na pobraniu i rozpakowaniu plików. Najpierw niezbędne jest zainstalowanie infrastruktury do utrzymywania i koordynowania brokerów. Może to być KRaft lub ZooKeeper. Ja wybieram to drugie rozwiązanie.

W Windows Terminal otwieram kilka zakładek dla Ubuntu.

W pierwszej uruchamiam ZooKeepera:

sudo bin/zookeeper-server-start.sh config/zookeeper.properties

W drugiej Kafkę:

sudo bin/kafka-server-start.sh config/server.properties

Jak widać cała operacja jest bardzo prosta. Mogę dalej podążać za instrukcją: dodać topik, okno consumera, nadawać i odbierać komunikaty. Ta testowa konfiguracja odwołuje się do parametru bootstrap-server, który zawiera listę par „hostname:port”, które adresują jednego lub więcej brokerów. W tym prostym przypadku dodanie topicu wygląda następująco:

kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092

Zastopujmy teraz wszystkie procesy Kafki poprzez zamknięcie zakładek.

Ponieważ będę potrzebował mieć dostęp z zewnętrznej aplikacji ABL, więc zamiast wartości localhost muszę wstawić wartość ip “maszyny” z Linuxem. Jeśli ip wynosi np. 172.22.13.101, to tę wartość wstawiam w następujących miejscach.
plik: config/server.properties
listeners=PLAINTEXT://172.22.13.101:9092
jak poniżej:

plik: config/producer.properties
bootstrap-servers=172.22.13.101:9092

plik: config/consumer.properties
bootstrap-servers=172.22.13.101:9092

OK, uruchamiam ponownie procesy ZooKeepera i Kafki. W trzeciej zakładce dodaję topic mytopic,

bin/kafka-topics.sh --create --topic mytopic --bootstrap-server 172.22.13.101:9092

a następnie uruchamiam proces producera:

bin/kafka-console-producer.sh --topic mytopic --bootstrap-server 172.22.13.101:9092

a w oknie czwartym consumera:

bin/kafka-console-consumer.sh --topic mytopic --bootstrap-server 172.22.13.101:9092

Wysyłam przykładowe wiadomości w oknie producera.

Pojawiają się one natychmiast w oknie consumera.

OK, mamy zatem skonfigurowany prosty system Kafki i w następnym wpisie pokażę jak przyłączyć się do niego z poziomu aplikacji ABL.

Podpisane biblioteki procedur ABL

Dziś napiszę wstęp o zabezpieczeniach r-kodów w tzw. podpisanych bibliotekach archiwum procedur. Temat ten wypłynął podczas kwietniowego spotkania World Tour i byłem poproszone o napisanie trochę więcej informacji niż było to w prezentacji.

Biblioteki archiwum ABL to zbiór r-kodów (rozszerzenie .apl), podobnie jak dobrze znane biblioteki .pl ale obsługują także podpisywanie i weryfikację r-kodów.
Podpisany plik archiwum gwarantuje, że żaden r-kod nie został uszkodzony ani naruszony. Jest to bardzo istotne np. podczas przeprowadzanych zewnętrznych audytów aby mieć pewność, że aplikacje przejdą kontrolę bezpieczeństwa.

Deweloperzy dostają dwa narzędzia uruchamiane z wiersza poleceń PROPACK i PROSIGN. Tworzą one cyfrowo podpisane archiwum oparte na standardzie Java JAR.

Zacznijmy od spakowania dwóch plików procedur findcustomer.r i updatecustomer.r wykorzystując komendę PROPACK. Nasz plik wynikowy bedzie miał nazwę cust.apl. Pozostaje do określenia ważny atrybut sygnatury. Może on mieć wartość open (pliki archiwum nie muszą być podpisane) lub required (podpis jest wymagany). Użyję tutaj domyślnej wartości open. Przykład komendy będzie wyglądał następująco:

propack --create --file=cust.apl --vendor "PSC" --signature=open .

Żeby sprawdzić jakie pliki znajdują się w archiwum uruchamiamy komendę z opcją –list:

propack --list --file=cust.apl

Widać, że w bibliotece znajduje się plik MANIFEST.MF
Zawiera on informacje na temat archiwum, a w przypadku podpisanej biblioteki także sygnatury r-kodów (o czym za chwilę).


Zobaczmy teraz co się stanie jeśli utworzę taką bibliotekę z parametrem signature=required.

Dodaję nazwę nowej biblioteki w Protools do PROPATH i uruchamiam prostą komendę.

Dostajemy błąd ponieważ plik musi być podpisany.

Wróćmy do pliku z sygnaturą typu open i zajmijmy się podpisaniem biblioteki. Przed uzyskaniem certyfikatu cyfrowego należy utworzyć magazyn kluczy do przechowywania certyfikatów własnych i z urzędu certyfikacji (CA). Utworzenie magazynu kluczy powoduje również umieszczenie w magazynie certyfikatu z podpisem własnym (self-signed) i pary kluczy (publiczny-prywatny). Nam dla testów będzie wystarczył certyfikat self-signed.
Użyjemy poniższej komendy pamiętając, że w oknie komend musi to być ciąg znaków bez znaków końca linii.

keytool -genkey 
-dname "CN=Host, OU=Education O=PSC, L=Warsaw, S=Mazovia, C=Poland"  
-alias server 
-keystore myKeystore.jks 
-storepass mykeystorepass1 
-validity 90 
-keyalg RSA 
-keysize 4096

Magazyn klucza jest już wygenerowany (.jks). Zapisujemy wartość aliasu i hasła do magazynu.
Teraz możemy użyć komendy PROSIGN.

prosign --archive cust.apl --signedarchive signedcust.apl 
--alias server --keystore myKeystore.jks --storepass mykeystorepass1


Biblioteka jest już podpisana ale dostaliśmy ostrzeżenie, że certyfikat jest self-signed i w środowisku produkcyjnym musimy zadbać o taki, który jest wydany przez oficjalny urząd (CA).

Zobaczmy teraz zawartość pliku MANIFEST.MF. Zawiera on na końcu funkcje skrótu SHA-256 dla obu r-kodów, gwarantując ich oryginalną zawartość.

Manifest-Version: 1.0
Implementation-Title: cust.apl
Implementation-Vendor: PSC
Implementation-Version: 0.0.0.0
Component-Name: cust.apl
Package-Type: apl
Signature-Policy: open
Validation-Policy: warn
Build-OS: all
Build-Date: 2024-05-10T14:57:39.157+01:00
OpenEdge-Tool: propack v1.00 (MSWin32)
OpenEdge-Version: 12.8
Created-By: 17.0.10 (Oracle Corporation)

Name: findCustomer.r
SHA-256-Digest: 1wpcgS+b6ya5OvqTjUek29hJdnqWNsjdRBJTPzYDCyo=

Name: updateCustomer.r
SHA-256-Digest: fKk6cX6dlJsN8VxgKzdbjM1XaXQRL/N3sdlHySHb0vE=

OpenEdge 12.8

Nareszcie mamy na rynku następną wersję LTS (Long-Term Support) OE 12.8. Poprzednia OE 12.2 pojawiła się 4 lata temu. 12.8 zawiera elementy wprowadzane w kolejnych wersjach Innovation 12.3-12.7 włącznie, co z grubsza opisałem na tym blogu. Teraz zobaczymy co ciekawego jest w samej wersji 12.8.

Zacznijmy od administracji PASOE. W poprzednim artykule opisałem nową komendę OEMANAGER, stosowaną do przeglądu statystyk i podstawowych operacji. Osoby zainteresowane odsyłam bezpośrednio do tego wpisu.

Inną ciekawą i bardzo ważną funkcją jest zapowiadana technologia Dynamic Data Masking (DDM) służąca do dynamicznego zaciemniania lub maskowania wrażliwych danych w bazie przed nieautoryzowanymi użytkownikami. Przykładem może być maskowanie w danych osobowych pola “pesel” lub w danych finansowych pola “wynagrodzenie”.

Technologia ta składa się niejako z dwóch etapów. Pierwszy należy do administratora bazy i polega na włączeniu funkcji poleceniem PROUTIL [baza] -C ENABLEDDM. Komenda ta sprawdza czy baza jest licencjonowana do użycia DDM. Następne polecenie PROUTIL [baza] -C ACTIVATEDDM uaktywnia DDM w bazie (dostępne są także opcje DEACTIVATEDDM i DISABLEDDM).

DDM do kontrolowania uprawnień przyznawanych użytkownikom w zakresie maskowania danych wykorzystuje konfigurację opartą na rolach. Administrator DDM może skonfigurować maskę nad polami tabeli, która ukrywa wrażliwe dane w zestawie wynikowym zapytania, a także utworzyć i przypisać nowe tagi autoryzacyjne do ról zdefiniowanych przez użytkownika.

Drugim etapem jest zatem zdefiniowanie własnych ról oraz znaczników autoryzacji i przypisanie ich do zdefiniowanych ról, przydzieleniu ról uwierzytelnionym użytkownikom, konfiguracja masek i znaczników autoryzacji dla pól tabeli. Etap ten realizujemy wykorzystując interfejs IDataAdminService, który udostępnia zestaw właściwości i metod obsługujących operacje CRUD związane z DDM. Poniżej zamieszczam wzięty z dokumentacji przykład utworzenia własnej roli TestUser.

USING OpenEdge.DataAdmin.*.

VAR DataAdminService oDAS. VAR IRole oRole.
VAR LOGICAL lResult.

ASSIGN oDAS = NEW DataAdminService(LDBNAME("DICTDB")).

oRole = oDAS:NewRole("TestUser"). 
oRole:Description = "A Test User".
// This role will be used for DDM
oRole:IsDDM = true.
lResult = oDAS:CreateRole(oRole). 

DELETE OBJECT oDAS.

W OpenEdge jest systematycznie rozwijana technologia wizualizacji danych poprzez standard OpenTelemetry i wykorzystanie zewnętrznych aplikacji typu APM (Application Performance Monitoring).
W 12.8 dodano możliwość monitorowania procesów agentów PASOE oraz klientów ABL. W tym drugim przypadku należy uruchomić sesję ABL z parametrem -otelconfig myotelConfig.json, wskazującym na plik konfiguracyjny w standardzie JSON. Przykładowy taki plik zamieszczam poniżej:

{
"OpenTelemetryConfiguration": {
        "exporters": {
            "otlp": {
                "grpc": [
                    {
                        "endpoint": "http://localhost:4317",
                        "span_processor": "batch",              
                     }
            ]
        }
      }
    },
    "OpenEdgeTelemetryConfiguration": {
        "trace_procedures": "Customer*.p",
		"trace_classes": "*",
        "trace_abl_transactions": true,
        "trace_requires_parent": true,
        "trace_request_start": true
    }
}

Plik ten określa m.in. proces OTel Collectora (port:4317), a także maskę monitorowanych plików np. każda procedura zawierająca w nazwie słowo Customer i wszystkie klasy (*). O pełnej konfiguracji monitorowania przy pomocy Open Telemetry napiszę innym razem.

A teraz kilka słów na temat administrowania bazą. Pojawiła się komenda PROUTIL [baza] -C PROBE [state] która sprawdza aktualny operacyjny stan bazy. Sprawdzane są trzy stany (parametr state):

  • startup – czy baza danych zakończyła uruchamianie
  • liveness – czy broker bazy danych działa i odpowiada
  • readiness – czy baza danych może wykonywać operacje (np. CRUD)

Jeśli wykonanie komendy z parametrem –verbose nie wyświetli żadnych informacji, będzie to oznaczać sukces.

Administratorzy wykorzystujący możliwość zwiększania wartości niektórych parametrów poleceniem PROUTIL… INCREASETO mogą teraz wygenerować plik .pf z aktualnymi ustawieniami podczas runtime’u. Daje to możliwość zapamiętywania i porównywania różnych konfiguracji. Plik możemy wygenerować wykorzystując tablice VST lub z poziomu narzędzia PROMON -> R&D Advanced Options -> 4. Administrative Functions -> 17. Generate parameter file

Dużym zainteresowaniem cieszyła się nowa komenda PROUTIL… TABLEREORG wprowadzona w OE 12.7, która pozwala na przeładowanie tablicy i polepszenie stopnia fragmentacji i rozrzucenia rekordów przy pracującej bazie. Podczas ładowania wykorzystuje się sortowanie rekordów wg indeksu. W OE 12.7 musiał to być indeks unikalny, jednakże czasami lepszą wydajność można uzyskać dla indeksu nieunikalnego ale często używanego w aplikacji. W OE 12.8 indeksy mogą już być nieunikalne.

Inne ciekawe i ważne nowości to np. podniesie wersji Spring Security 6.1.4 oraz Tomcata 10.1.15. Ma to swoje implikacje podczas migracji z wcześniejszych wersji PASOE.
W Developer’s Studio dodano typ projektu .NET z narzędziami: Class Browser, Content Assist, Outline View, co ma pomóc w programowaniu przy użyciu .NET 6.

Jest jeszcze sporo innych nowinek po które zapraszam do dokumentacji.

Strukturalna obsługa błędów w ABL cz. II

W poprzednim odcinku obiecałem napisać o tym jak utworzyć własną klasę obsługi błędów oraz pokazać, że strukturalną obsługę błędów można stosować także w przypadku procedur uruchamianych na serwerze aplikacji. Obie te kwestie pokażę na jednym przykładzie.

W Developer’s Studio klasę definiujemy najwygodniej przy pomocy wizarda, tak jak każda inną klasę. Musimy pamiętać, że dziedziczy ona z klasy Progress.Lang.AppError oraz że trzeba zdefiniować ją jako Serializable, co oznacza, że instancja klasy może być przekazywane przez wartość pomiędzy sesjami AVM (ABL Virtual Machine).

Poniżej widzimy gotową klasę Errors.myError zawierającą właściwości: Errormessage, ErrorNum oraz ProcName oraz dwie metody GetErrorMessage() i GetVerboseErrorMessage(). Posłużą one do własnej obsługi błędów.
Pierwsza metoda zwraca jedynie komunikat wraz z numerem błędu. GetVerboseErrorMessage() zawiera także nazwę procedury, w której ten błąd wystąpił. Gdybyśmy tworzyli program złożony z obiektów moglibyśmy użyć nazwy klasy i metody aby mieć więcej informacji o lokalizacji wystąpienia błędu itp.

USING Progress.Lang.*.
USING Progress.Lang.AppError.

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS Errors.myError INHERITS AppError SERIALIZABLE: 

    DEFINE PUBLIC PROPERTY ErrorMessage AS CHARACTER NO-UNDO 
    GET.
    SET. 

    DEFINE PUBLIC PROPERTY ErrorNum AS INTEGER NO-UNDO 
    GET.
    SET. 
    
    DEFINE PUBLIC PROPERTY ProcName AS CHARACTER NO-UNDO 
    GET.
    SET. 

    CONSTRUCTOR PUBLIC myError (
        INPUT pErrorNum AS INTEGER,
        INPUT pErrorMessage AS CHARACTER,
        INPUT pProcName AS CHARACTER):            
        SUPER ().
        ErrorNum = pErrorNum.            
        ErrorMessage = pErrorMessage.
        ProcName = pProcName.
    END CONSTRUCTOR.

    METHOD PUBLIC CHARACTER GetErrorMessage(  ):
        DEFINE VARIABLE res AS CHARACTER NO-UNDO.
        res =  ErrorMessage + ". " + STRING(ErrorNum).
        RETURN res.
    END METHOD.

    METHOD PUBLIC CHARACTER GetVerboseErrorMessage(  ):
        DEFINE VARIABLE res AS CHARACTER NO-UNDO.
        res =  ErrorMessage + ". " + STRING(ErrorNum) + ". Proc: " + ProcName.
        RETURN res.
    END METHOD.    
    
END CLASS.  

Poniższy przykład jest analogiczny do tego z poprzedniego odcinka – został zmodyfikowany do uruchomienia na serwerze aplikacji, oczywiście z własną obsługą błędów.

USING Errors.myError.

VAR INT i = 1.
DEFINE VARIABLE hServer AS HANDLE NO-UNDO.
DEFINE VARIABLE lReturn AS LOGICAL NO-UNDO.

CREATE SERVER hServer.
lReturn = hServer:CONNECT("-URL http://localhost:8810/apsv
  -sessionModel Session-managed"). 

IF NOT lReturn THEN DO:
  DELETE OBJECT hServer NO-ERROR.
  RETURN ERROR "Failed to connect to the ABL web application: " + RETURN-VALUE.
END.

RUN catcherror-pasoe-myerror.p ON hServer (INPUT 3000).

CATCH myerr AS MyError:
    MESSAGE myerr:GetVerboseErrorMessage()
        VIEW-AS ALERT-BOX INFO BUTTONS OK.
END.
/************************************************************/

/* catcherror-pasoe-myerror.p */
BLOCK-LEVEL ON ERROR UNDO, THROW.

USING Errors.myError.

DEFINE INPUT PARAMETER icustNum AS integer.
DEFINE VARIABLE ProcName AS CHARACTER.


FIND customer WHERE customer.custnum = icustNum.
if country NE "Poland" THEN DO:
    ProcName = ENTRY(1, PROGRAM-NAME(1),' ').
    UNDO, THROW NEW myError(555, "Bad country", ProcName).

END. 

Ponieważ użyłem metody GetVerboseErrorMessage() otrzymuję cały komunikat z nazwą procedury w której wystąpił błąd.

Strukturalna obsługa błędów w ABL cz. I

Zanim przejdziemy do omówienia strukturalnej obsługi błędów w języku ABL, trzeba przypomnieć pewne (oczywiste) fakty dotyczące tego języka.

ABL (wcześniej 4GL) był zawsze proceduralnym językiem bazodanowym o strukturze blokowej. Umożliwia on łączenie logiki biznesowej i interfejsu użytkownika w jednym pliku procedury zawierającym instrukcje, blok główny i bloki zagnieżdżone. Blokowa struktura jest tu niezwykle istotna, ponieważ z nią związane są zakresy transakcji, blokady rekordów a także obsługa błędów.

Od samego początku obsługa błędów (nazywana tradycyjną) była zaimplementowana w języku 4GL. Związane są z nią takie słowa kluczowe jak NO-ERROR, STATUS-ERROR, RETURN ERROR, bloki typu ON ERROR itd. Programiści dobrze wiedzą o co chodzi.

W wersji OpenEdge 10 język 4GL wyewoluował do ABL, zawierając oprócz poprzednich instrukcji także technologię programowania obiektowego i razem z tymi zmianami pojawiła się strukturalna obsługa błędów. Jest to model obsługi błędów spotykany w wielu językach, ale zazwyczaj jest kojarzony z językami obiektowymi i składnią typu: try i catch.

Instrukcja try umożliwia zdefiniowanie bloku kodu, z którym można powiązać zachowanie związane z obsługą błędów. Z kolei instrukcja catch służy do zdefiniowania obsługi błędu i powiązanie jej z typem błędu. Gdy system wykryje błąd w bloku try, wykona kod w bloku catch, który pasuje do błędu. Jeśli takiego bloku nie ma, to obsługa błędu jest przekazywana najczęściej w dół stosu wywołań, dopóki system nie znajdzie odpowiedniego bloku catch.

Strukturalna obsługa błędów posiada następujące cechy:

  • Wszystkie błędy są reprezentowane w postaci obiektów
  • Można zdefiniować własne typy błędów (klasy)
  • Można wywołać (throw) błąd explicite
  • Można obsłużyć (catch) określone typów błędów w określonym bloku ABL.
  • Możliwość propagacji błędu (ponowny throw).
  • Jeśli chodzi o implementację tej technologii w języku ABL, to należy przypomnieć, że jest to język o strukturze blokowej, gdzie prawie wszystkie bloki są blokami transakcyjnymi UNDO (poza zwykłym blokiem grupującym DO:…END.) i nie ma potrzeby budowania specjalnych bloków try, co ułatwia programowanie.

    OK, napiszmy pierwszy prosty przykład własnej obsługi błędu, korzystając z faktu, że w tablicy Customer nie ma rekordu o polu CustNum = 1000.

    DO ON ERROR UNDO, LEAVE:
        FIND FIRST Customer WHERE CustNum = 1000.
        MESSAGE Customer.Name VIEW-AS ALERT-BOX.
        CATCH mySysError AS Progress.Lang.SysError:
            MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
        END CATCH.
    END.
    

    Wykorzystaliśmy tutaj blok transakcyjny DO ON ERROR UNDO,… W przypadku gdy rekord nie będzie odnaleziony zostanie “wyłapany” błąd systemowy i obsłużony przez nas w bloku CATCH:…END. Blok ten musi być na końcu bloku transakcyjnego. Taki “złapany” błąd przestaje istnieć w sesji ale co jeśli chcemy go przekazać po obsłużeniu do zewnętrznego bloku (np. bloku procedury)? Wystarczy następująca modyfikacja:

    DO ON ERROR UNDO, LEAVE:
        FIND FIRST Customer WHERE CustNum = 1000.
        MESSAGE Customer.Name VIEW-AS ALERT-BOX.
        CATCH mySysError AS Progress.Lang.SysError:
            MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
            UNDO, THROW mySysError.
        END CATCH.
    END.
    
    CATCH mySysError AS Progress.Lang.SysError:
        MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX.
    END CATCH.
    

    Teraz możemy wychwycić go i obsłużyć ponownie.
    Należy dodać, że jeśli użyjemy tradycyjnej składni obsługi błędu NO-ERROR, to ma ona pierwszeństwo nad obsługą strukturalną. W poniższym przykładzie blok CATCH nie będzie więc wywołany.

    DO ON ERROR UNDO, LEAVE:
        FIND FIRST Customer WHERE CustNum = 1000 NO-ERROR.
        IF AVAILABLE (Customer) THEN 
           MESSAGE Customer.Name VIEW-AS ALERT-BOX.
        CATCH mySysError AS Progress.Lang.SysError:
            MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
            UNDO, THROW mySysError.
        END CATCH. 
    END.
    

    Poniżej przedstawiłem uproszczony schemat dziedziczenia klas. Progress.Lang.Error służy do wychwycenia dowolnego błędu. Progress.Lang.ProError służy do wychwycenia dowolnego rodzaju błędu Progress: systemu, aplikacji, SOAP. Progress.Lang.SysError służy do wychwycenia błędów języka ABL, np.: niepowodzenie instrukcji FIND, błąd indeksu; wszystko, co obsługuje NO-ERROR. Progress.Lang.AppError służy do wychwycenia błędów aplikacji zgłaszanych (throw) zgodnie z regułami biznesowymi ABL. Z tej klasy może dziedziczyć nasza własna klasa do obsługi błędów.

    Zobaczmy teraz jak wychwytywać błędy w procedurach i przesyłać stosowne komunikaty do programu głównego.
    W tradycyjnej obsłudze błędów mogliśmy skorzystać ze składni w programie wywołującym: IF ERROR-STATUS:ERROR THEN MESSAGE RETURN-VALUE VIEW-AS ALERT-BOX ERROR.
    oraz w wywoływanym RETURN ERROR “Brak rekordu w bazie”.

    W obsłudze strukturalnej mamy szersze pole do popisu. Przyjrzyjmy się poniższemu przykładowi.

    /* main.p */
    VAR INT i = 1.
    
    RUN FindCustomer.p (1000).
    
    CATCH syserr AS Progress.Lang.SysError:
        MESSAGE "Customer: " syserr:GetMessage(1)
           VIEW-AS ALERT-BOX INFO BUTTONS OK.
    END.
      
    CATCH apperr AS Progress.Lang.AppError:
      DO WHILE i <= apperr:NumMessages:
          MESSAGE "Country: " apperr:GetMessage(i)  
                              apperr:GetMessageNum(i)
             VIEW-AS ALERT-BOX INFO BUTTONS OK.
          i = i + 1.    
      END.      
    END.
    
    /************************************************************/
    
    /* FindCustomer.p */
    BLOCK-LEVEL ON ERROR UNDO, THROW.
    
    DEFINE INPUT PARAMETER icustNum AS integer.
    
    FIND customer WHERE customer.custnum = icustNum.
    IF country NE "Poland" THEN
        UNDO, THROW NEW Progress.Lang.AppError("Niewłaściwy kraj", 555).
        
    /* Można dodac więcej komunikatów własnej obsługi błędów */    
    CATCH e AS Progress.Lang.AppError :
        e:AddMessage("Klient nie jest z Polski", 777).
        undo, throw e.     
    END CATCH.
    

    Na początku wywoływanej procedury FindCustomer.p znajduje się instrukcja BLOCK-LEVEL ON ERROR UNDO, THROW. która gwarantuje, że wszystkie nieobsługiwane błędy w blokach transakcyjnych, zostaną propagowane do obiektu wywołującego. Jeśli wywołamy tę procedurę z parametrem 1000 (a więc będzie brak rekordu Customer) błąd zostanie obsłużony w procedurze main.p w bloku CATCH syserr AS Progress.Lang.SysError (ponieważ będzie to błąd systemowy).
    Załużmy, że jeśli rekord zostanie znaleziony, będziemy chcieli sprawdzić czy na pewno klient jest z Polski a jeśli nie, to ustawić własny błąd. Definiujemy więc błąd typu AppError: ...THROW NEW Progress.Lang.AppError("Niewłaściwy kraj", 555).
    Po pewnym czasie chcielibyśmy dodać jeszcze jeden komunikat dla tego błędu. Możemy to zrobić bez problemu gdyż ta klasa posiada metodę AddMessage jak widać poniżej w Class Browser w Developer's Studio (dla klasy SysError nie ma takiej możliwości).

    Możliwości obsługi błędów w aplikacjach ABL jest więc całkiem sporo. Wiem, że niektórzy chcieliby wiedzieć jak poradzić sobie z serwerem aplikacji i czy można pisać własne klasy obsługi, ale o tym następnym razem.

    Testy ABLUnit

    Testowanie jest nieodłącznym elementem budowania aplikacji. Istnieją różne metody, które mają dać pewność deweloperom, że aplikacja poradzi sobie w każdej sytuacji, bez względu na podawane dane a interfejs będzie tzw. “idiotoodporny”. My skupimy się tutaj tylko na pierwszej części czyli logice aplikacji. I znowu, ten element można testować na różne sposoby np. wykorzystując debugger, kompilację kodu z opcją XREF, czy komunikaty zrzucające dane do plików na dysku. Jednakże te metody mają wadę, polegającą na testowaniu jednej sytuacji na raz, a my chcielibyśmy zautomatyzować testowanie dla różnych sytuacji i różnych wprowadzanych danych.
    Do tego celu bardzo dobrze nadaje się testowanie tzw. ABLUnit, dla którego w Developer’s Studio jest zdefiniowany specjalny typ projektu. Testy te zostały wprowadzone od wersji OpenEdge 11.4 i mają zastosowanie dla kodu ABL zarówno do procedur jak i klas.

    Na początku musimy przyswoić sobie pewne pojęcia związane z testowaniem.

    Przypadek testowy (test case) odnosi się do pliku procedury ABL lub pliku klasy. Przypadek testowy może zawierać wiele testów (zestaw testów).

    Test to wewnętrzna procedura w pliku procedury lub metoda w pliku klasy, oznaczona adnotacją @Test.

    Asercje (assertions) – służą do określenia, czy dany test powiódł się czy nie. ABLUnit zapewnia metody asercji dla wszystkich typów danych ABL. Sukces lub niepowodzenie tych twierdzeń jest śledzone przez platformę i można je wyświetlić po zakończeniu przebiegu testu.

    Zestaw testów (test suite) to zbiór przypadków testowych. Służy on do organizowania i uruchamiania przypadków testowych w grupach logicznych. Zestaw testów może zawierać dowolną liczbę przypadków testowych.

    Poniższy obrazek przedstawia cykl wykonania ABLUnit.

    Jak wspomniałem wcześniej, każdy test powinien być opatrzony adnotacją @Test. Testowana procedura lub klasa może mieć dowolną liczbę testów, a każdy test jeden zestaw metod/procedur, które są opatrzone adnotacjami @Setup i @TearDown. Przypadek testowy (test case) może mieć po jednej metodzie/procedurze oznaczonej adnotacjami @Before i @After. @Before jest wykonywana raz przed rozpoczęciem testów i może być wykorzystana np. do podłączenia się do bazy. @After jest natomiast wykonywana po ukończeniu testów i może być wykorzystana np. do odłączenia bazy.
    Proces testowania rozpoczniemy od stworzenia projektu typu OpenEdge ABLUnit.

    Projekt ten jest podłączony do bazy sports2000. Dodajemy do niej tabelę login, która ma dwa pola: username i password (patrz słownik baz danych poniżej).

    W tabeli login wstawiamy jeden rekord o polach jak poniżej. Będzie on reprezentować prawidłowe dane logowania do aplikacji.

    create login.
    username = "test".
    password = "test".
    

    Plik Main.p to nasz testowany moduł aplikacji. Zawiera on prosty kod sprawdzający czy osoba logująca się podała prawidłowe dane, dane nieprawidłowe lub nie podała żadnych danych.
    W drugim i trzecim przypadku generowany jest błąd z odpowiednim komunikatem.

    /* Main.p */
    USING Progress.Lang.AppError FROM PROPATH.
    DEFINE VARIABLE mye AS Progress.Lang.AppError NO-UNDO.
    
    PROCEDURE loginProc :
      DEFINE INPUT PARAMETER username AS CHARACTER.
      DEFINE INPUT PARAMETER password AS CHARACTER.
      DEFINE OUTPUT PARAMETER loginResult AS CHARACTER INIT "Fail".
      IF username EQ ? THEN
        DO:
          mye = NEW Progress.Lang.AppError("Username is empty", 200).
          mye:ReturnValue = "Username cannot be empty".
          RETURN ERROR mye.
        END.
    
      FOR EACH login WHERE login.username = username:
        IF login.password = password THEN
        loginResult = "Success".
        ELSE DO:
          mye = NEW Progress.Lang.AppError("wrong password", 200).
          mye:ReturnValue = "Password is wrong". 
          RETURN ERROR mye.
        END.    
      END.
    END PROCEDURE.
    

    Aby uruchomić powyższą procedurę z naszych testów, należy:

  • Połączyć się z bazą danych (@Before).
  • Uruchomić główną procedurę (main.p), używając wskaźnika w trybie PERSISTENT (@Setup).
  • Podać wartości wejściowe (@Test).
  • Uruchomić wewnętrzną procedurę loginProc z parametrami wejściowymi i wyjściowymi (@Test).
  • Sprawdzić wynik testu poprzez asercję (@Test).
  • Usunąć procedurę z pamięci (@TearDown).
  • Odłączyć się od bazy danych (@After).
  • Poniżej znajduje się procedura do przeprowadzenia testów (TestCase.p).

    /* TestCase.p */
    USING OpenEdge.Core.Assert FROM PROPATH.
    
    BLOCK-LEVEL ON ERROR UNDO, THROW.
    
    DEFINE VARIABLE procHandle AS HANDLE NO-UNDO.
    DEFINE VARIABLE uname AS CHARACTER NO-UNDO.
    DEFINE VARIABLE pwd AS CHARACTER NO-UNDO.
    DEFINE VARIABLE res AS CHARACTER NO-UNDO.
    
    @Before.
    PROCEDURE Before:
    CONNECT C:\WrkOpenEdge126\db\sports2000.db -H localhost -S 10005 NO-ERROR.
    END PROCEDURE.
    
    @Setup.
    PROCEDURE setUp:
    RUN main.p PERSISTENT SET prochandle.
    END PROCEDURE.
    
    @Test.
    PROCEDURE validusr: /* Test1 */
    uname = "test".
    pwd = "test".
    RUN loginProc IN procHandle (INPUT uname,INPUT pwd,OUTPUT res) .
    Assert:equals("success", res).
    END PROCEDURE.
    
    @Test.
    PROCEDURE invalidusr1: /* Test2 */
    uname = "ablunit".
    pwd = "test".
    RUN loginProc IN procHandle (INPUT uname,INPUT pwd,OUTPUT res) .
    Assert:equals("success", res).
    END PROCEDURE.
    
    @Test.
    PROCEDURE emptyUsr: /* Test3 */
    uname = ?.
    pwd = "invalid".
    RUN loginProc IN procHandle (INPUT uname,INPUT pwd,OUTPUT res) .
    Assert:equals("success", res).
    END PROCEDURE.
    
    @TearDown.
    PROCEDURE tearDown:
    DELETE PROCEDURE procHandle.
    END PROCEDURE.
    
    @After.
    PROCEDURE tearDownAfterProcedure:
    DISCONNECT sports2000.
    DELETE PROCEDURE THIS-PROCEDURE.
    END PROCEDURE.
    

    Po uruchomieniu procedury TestCase.p pojawi sie plik results.xml.

    Klikamy na niego dwukrotnie a w oknie konsoli, w zakładce ABLUnit pojawią się wyniki naszych testów.

    Zgodnie z naszymi przewidywaniami tylko pierwszy test zakończył się powodzeniem.
    Zachęcam do własnych testów!

    Debugging w aplikacjach PASOE

    W kilkunastu postach opisywałem tworzenie serwisów i aplikacji z udziałem serwera aplikacji PASOE i coraz więcej naszych klientów korzysta z tej technologii. Coraz częściej powtarza się pytanie: jak można wykorzystać debugger aby wychwytywać błędy i testować aplikacje.
    PASOE to połączenie technologii Tomcat (Java) i OpenEdge (ABL), więc kompletny proces debuggowania jest dość złożony.
    Tutaj chciałbym pokazać jak zacząć ten proces z punktu widzenia OpenEdge’a.

    Jak zwykle przed przystąpieniem do wykorzystania debuggera, trzeba włączyć tę funkcjonalność w środowisku OpenEdge. Należy uruchomić komendę prodebugenable -enable-all jako administrator.

    Drugim krokiem jest wdrożenie web aplikacji oedb.war w testowanej instancji. Pamiętajmy, że ze względów bezpieczeństwa, nigdy nie wdrażać jej w środowisku produkcyjnym!
    pasman deploy -I oepas1 %DLC%\servers\pasoe\extras\oedbg.war


    Cały proces debuggowania będzie odbywał się w Progress Developer’s Studio for OpenEdge.
    Wykorzystam do testowania plik klasy wygenerowany przy tworzeniu prostego serwisu REST i dodania zasobu (Business Entity) Customer, co opisywałem na początku 2016 roku.
    Dostajemy klasę customer.cls z metodami CRUD: CreateCustomer, ReadCustomer, UpdateCustomer i DeleteCustomer. Ja wykorzystam metodę ReadCustomer i wstawię breakpoint zaraz za instrukcją SUPER:ReadData(filter). klikając po lewej stronie dwukrotnie myszką.

    Teraz klikając prawym przyciskiem myszy na instancję PASOE wybieramy Restart in Debug.


    Następnie uruchamiamy serwis w przeglądarce poprzez link dla Custnum=1,
    http://localhost:8810/CustProj/rest/CustProjService/customer?filter=Custnum=1
    a w następnej kolejności dla Custnum < 5.
    http://localhost:8810/CustProj/rest/CustProjService/customer?filter=Custnum<5

    W PDSOE w widoku dla zmiennych zobaczymy wartość filtra odpowiednio “Custnum=1″oraz “Custnum<5”.
    Widać z tego, że zasada testowania aplikacji przy pomocy debuggera jest dość prosta.



    Dodam jeszcze jedną informację, ponieważ byłem o to kilka razy pytany: W sytuacji gdy mamy wiele projektów i instancji PASOE, jak sprawdzić która instancja obsługuje dany serwis.
    Można sprawdzić np. poleceniem tcman jakie serwisy są wdrożone w danej instancji, a w PDSOE podejrzeć właściwości danego projektu jak na widoku poniżej.

    Cały proces debuggowania można prześledzić na kanale Youtube.
    Webinar ten jest rozszerzony o testowanie aplikacji nie tylko lokalnych ale także zdalnych.

    OpenEdge 12.7

    5 maja pojawiła się wersja OpenEdge 12.7. Ma być ona ostatnią z wersji Innovation dla dwunastki ponieważ ostatnia, zapowiadana na koniec roku, ma być wersją LTS. Przyjrzyjmy się wybranym nowościom.

    PROSTRCT REMOVEONLINE – nowa komanda poprawiająca dostępność bazy danych. Umożliwia usuwanie extentów lub całych obszarów bez konieczności zamykania bazy.

    PROUTIL TABLEREORG to narzędzie służące do poprawy stopnia fragmentacji w tabelach (zastępuje operacje dump i load online) dla obszarów typu II. Wprowadzono je w OE 12.3, a teraz dodano nowe parametry np. restrict czy nosmartscan. Pierwsza opcja jest przeznaczona dla dużych tabel i pozwala na reorganizację wybranego fragmentu tabeli wg. wartości pola. Druga opcja wyłącza domyślne inteligentne skanowanie. Przykład poniżej.

    Wprowadzono nowy typ pakietu (lub biblioteki) kodu aplikacji ABL, który można podpisać i weryfikować, aby mieć pewność, że skompilowany kod aplikacji ABL nie został uszkodzony ani zmodyfikowany. Ten nowy typ biblioteki jest znany jako plik archiwum i ma rozszerzenie .apl. Do utworzenia archiwum, dodawania r-kodów służy komenda PROPACK, a do podpisywania PROSIGN.

    OpenEdge obsługuje teraz OAuth2 i SAML do uwierzytelniania i autoryzacji. Te standardowe protokoły zapewniają bezpieczny i wygodny mechanizm uwierzytelniania użytkowników bez udostępniania poufnych informacji. OpenEdge Authentication Gateway może automatycznie przekonwertować zweryfikowane tokeny na token OpenEdge Client Principal, który możemy obsługiwać w aplikacjach ABL.

    Administrowanie PASOE: do tej pory można było skonfigurować instancje tak aby w razie potrzeby agent wielosesyjny uruchamiał dodatkowe sesje, ale nie było możliwości aby te sesje były automatycznie przycinane. W obecnej wersji OpenEdge dodano nowe właściwości do pliku openedge.properties, które umożliwiają administratorowi systemu skonfigurowanie instancji PASOE tak, aby sesje ABL były przycinane gdy nie są już potrzebne czy też gdy osiągnęły limit czasu lub pamięci.

    W Developer Studio dodano ABL Type Hierarchy View, który pokazuje relacje rodzic-dziecko między klasami ABL i interfejsami, aby pomóc programiście ABL zobaczyć hierarchię i strukturę dziedziczenia klasy i przechodzenie do dowolnego elementu widocznego w widoku.

    Dla wykorzystujących AI Archiver do zapisu After Image pojawiła się ciekawa możliwość wystartowania tego procesu online bez konieczności wykonania backupu. Jest to szczególnie istotne dla dużych baz gdy wykonanie kopii powodowało wstrzymanie wykonywanie transakcji.

    W operacja przywracanie bazy PROREST dodano parametry do wykonania jej jako proces wielowątkowy.

    Jest jeszcze kilka nowinek po które odsyłam do dokumentacji.

    Profiler – narzędzie dewelopera cz. III

    Coraz więcej programistów korzysta z narzędzia Developer Studio, pora więc pokazać jak korzystać w nim z Profilera (dostępnego od wersji OE 11.6), omawianego w dwóch poprzednich artykułach.
    Środowisko tego narzędzia nadaje się o wiele bardziej do wyświetlania złożonych informacji w kilku okienkach niż tradycyjny edytor.
    Spróbujmy najpierw otworzyć wygenerowany wcześniej w profilerze plik typu .out. Najwygodniej będzie zmienić to rozszerzenie na natywne dla profilera czyli .prof.
    Importuję do nowego projektu OpenEdge plik .prof oraz dwa pliki procedur.

    Dwukrotne kliknięcie na plik .prof otwiera widok Profilera.

    Opis kolumn jest analogiczny jak opisany w poprzednim artykule, więc nie będę się powtarzać. Nas interesuje teraz jak generować plik .prof z poziomu Developer Studio po dokonaniu pewnych zmian w programie (np. chcę aby procedura orderval.p wykonała się dla wszystkich iteracji).
    Pierwszym krokiem jest dodanie katalogu profiler do ścieżki PROPATH dla projektu.
    Następnie, wybieramy z menu Run -> Run Configurations. Dodajemy nową konfigurację (np. o nazwie Profiler), wstawiamy nazwę procedury CustReport.p…

    … a w zakładce Profiler zaznaczamy pole Enable profiling.

    Klikamy przycisk Run i czekamy aż program zakończy działanie, po czym otwiera się okno Viewera z nowymi statystykami.

    Na razie kończę omawaiać to narzędzie. Niektórzy z Was chcieliby dowiedzieć się więcej o możliwości instalacji i wykorzystania OECC i temu poświęcę następne artykuły.

    Profiler – narzędzie dewelopera cz. II

    Dziś kontynuujemy wykorzystanie narzędzia Profiler Control do przetestowania kodu naszej aplikacji. Weźmy np. prosty poniższy przykład CustReport.p który wyświetla w pętli podstawowe pola z tablicy Customer,

    DEFINE VARIABLE dOrdervalue AS DECIMAL NO-UNDO.
    
    FOR EACH customer WHERE custnum < 100:
       dOrdervalue = 0.
       IF custnum < 40 THEN 
          RUN ordervalue.p (custnum, OUTPUT dOrdervalue).
       
       DISP custnum name dOrdervalue.
    
    END.
    

    a dla części iteracji uruchamia dodatkową procedurę ordervalue.p zliczającą sumę zamówień klienta.

    DEF INPUT PARAM icustnum AS INTEGER.
    DEF OUTPUT PARAM dordval AS DECIMAL.
    
    FIND customer WHERE customer.custnum = icustnum.
    
    FOR EACH order OF customer, EACH orderline OF order:
       dordval += price * qty.
    END.
    

    Po uruchomieniu Profilera zostanie wygenerowany plik typu .out. Jeśli spróbujemy go otworzyć przy pomocy przycisku VIEW z panelu narzędzia, dostaniemy komunikat o błędzie: Cannot load data. Don’t know how to load data for version 3.
    Otwórzmy plik zwykłym edytorem tekstowym, np. Notepad++. Na pierwszej pozycji znajduje się nr wersji a potem data. Zmieniamy wartość nr wersji 3 na 1.
    Zobaczmy przy okazji, że plik ten zawiera informacje generowane podczas inicjalizacji sesji oraz dane niezbędne do analizy przy pomocy Viewera.

    Po naciśnięciu VIEW zobaczymy poniższy ekran.

    Profiler udostępnia statystyki dotyczące działania programu. Dane te są podzielone według bloków i poszczególnych linii kodu. Każdy blok reprezentuje aktualną procedurę, procedurę wewnętrzną lub funkcję. W górnym pasku, po lewej stronie możemy wybrać zarejestrowaną sesję. Obszar 1 zawiera następujące informacje:

    Code Block: nazwa wewnętrznej procedury lub funkcji, tryger interfejsu użytkownika i nazwa programu
    Calls To: liczba wykonań tego bloku kodu
    Avg Time: średni czas w sekundach potrzebny do wykonania tego bloku kodu
    Tot Time: całkowity czas w sekundach potrzebny do wykonania tego bloku kodu
    %Session: procent całej sesji wykorzystany przez ten blok kodu w porównaniu z innymi blokami kodu
    Cum Time: całkowity czas potrzebny do wykonania tego bloku kodu oprócz całkowitego czasu wszystkich bloków kodu wywołanych przez ten blok.

    Obszar 3 zawiera informacje dot. linii kodu:

    Line: numer linii w wybranym programie
    Exec Count: liczba wykonań tej linii kodu
    Avg Exec: średni czas wykonania tej linii kodu w sekundach
    Tot Time: całkowity czas w sekundach potrzebny do wykonania tej linii kodu
    Cum Time: całkowity czas w sekundach, jaki zajęło wykonanie tego wiersza kodu, a także wszelkich bloków kodu, wywołanych przez tę linię. UWAGA: ta liczba będzie się różnić od Tot Time tylko wtedy, gdy linia kodu jest instrukcją uruchomienia procedury lub wywołania funkcji.
    Pozostałe pola nie są używane.

    Wreszcie obszar 2.
    Calling Code Block oraz Called Code Block umożliwiają nawigację między różnymi sekcjami kodu poprzez dwukrotne kliknięcie wybranego wiersza.
    Widzimy, że procedura ordervalue.p była wywołana 38 razy, co zajęło niecałe 4% czasu sesji.

    Jeszcze jedna sprawa – na ekranie wyjściowym Profilera (pierwszy obraz w poprzednim artykule) widzimy po prawej stronie trzy pola wyboru: Listing, Coverage, Tracking.
    Pierwsze służy do wygenerowania listingu, który będzie widoczny w Viewerze. Coverage służy do generowania dodatkowych danych dla procedur zewnętrznych, a Tracking do określenia filtra dla generowania danych tylko dla określonych procedur. Jeśli zaznaczymy Listing i ponownie uruchomimy sesję, na ekranie pojawi się listing programu, a wybór linii kodu a w browserze podświetli tę linię na listingu. Bardzo użyteczna funkcja.


    Teraz trzeba już tylko przetestować własne procedury aby zorientować się w przydatności Profilera.
    To jeszcze nie koniec. W następnym odcinku zobaczymy jak używać tego narzędzia w Developer Studio.

    1 2 3 5