ABAP Cloud - Hintergrundverarbeitung
Wie lagert man in ABAP Cloud eigentlich speicherintensive Prozesse in den Hintergrund aus und monitort diese? In diesem Artikel schauen wir uns das Framework einmal genauer an.
Inhaltsverzeichnis
In diesem Artikel werden wir uns bgPF anschauen und wie du es in ABAP Cloud verwenden kannst, um Prozesse in den Hintergrund zu verschieben. Dabei werden wir uns an einem Beispiel anschauen, wie es im Detail funktioniert und die beiden Varianten, die für Prozesse angeboten werden.
Einleitung
In Prozessen kann es vorkommen, dass der eigentliche Prozess bereits abgeschlossen ist (Anlage eines Beleges oder einer Buchung) und dann gewisse Zusatzprozesse laufen sollen. Solche Prozesse können zum Beispiel sein: Sammeln von Statistiken, Erstellung von Mails, Auswertungen oder Nachfolgeprozesse anstoßen. Solche Zusatzprozesse kosten meistens Laufzeit und hindern den User die UI zu bedienen, um normal weiterzuarbeiten (Ladebalken, Sanduhr, etc.).
Für solche Fälle gibt es Hintergrundprozesse, die angestoßen werden und dann selbstständig laufen. SAP hatte deshalb das Background Processing Framework, kurz bgPF, vorgestellt. Die Verarbeitung in Prozessen im Hintergrund ist nicht neu, allerdings eine Verarbeitung ohne eigenen Funktionsbaustein war bisher nicht üblich. Auch das man sich nicht um die Verwaltung des Prozesses kümmern muss.
Möchtest du mehr zur Funktionsweise von bgPF erfahren, empfehlen wir dir den unten verlinkten SAP Community Artikel, dort wird der Prozess im System noch einmal schematisch dargestellt.
Vorbereitung
In unseren Beispielen werden wir eine Tabelle für das Logging verwenden, umso besser die Aktionen im System nachvollziehen zu können. Die Tabelle ist wie folgt definiert:
define table zbs_dmo_bgpf {
key client : abap.clnt not null;
key identifier : sysuuid_x16 not null;
description : abap.char(60);
inumber : abap.int4;
@Semantics.amount.currencyCode : 'zbs_dmo_bgpf.currency'
amount : abap.curr(15,2);
currency : abap.cuky;
created_at : abap.utclong;
created_from : abap.char(12);
}
Dazu setzen wir eine Datenverarbeitungsklasse darüber, die sich um das Befüllen der Sätze kümmert, damit wir in den Beispielen uns auf das eigentliche Framework fokussieren können.
CLASS zcl_bs_demo_bgpf_data DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES ts_data TYPE zbs_dmo_bgpf.
TYPES tt_data TYPE STANDARD TABLE OF ts_data WITH EMPTY KEY.
METHODS add
IMPORTING is_data TYPE ts_data.
METHODS save
IMPORTING id_commit TYPE abap_bool.
PRIVATE SECTION.
DATA mt_data TYPE tt_data.
ENDCLASS.
CLASS zcl_bs_demo_bgpf_data IMPLEMENTATION.
METHOD add.
WAIT UP TO 1 SECONDS.
DATA(ls_data) = is_data.
ls_data-identifier = cl_system_uuid=>create_uuid_x16_static( ).
ls_data-created_from = cl_abap_context_info=>get_user_alias( ).
ls_data-created_at = utclong_current( ).
INSERT ls_data INTO TABLE mt_data.
ENDMETHOD.
METHOD save.
INSERT zbs_dmo_bgpf FROM TABLE @mt_data.
IF id_commit = abap_true.
COMMIT WORK.
ENDIF.
ENDMETHOD.
ENDCLASS.
Die Klasse ist recht simpel gehalten, über die ADD Methode können wir neue Datensätze in den internen Puffer schreiben, über die SAVE Methode werden die Datensätze dann auf die Datenbank geschrieben. Damit wir später besser die Veränderungen sehen können, gibt es in der ADD Methode einen WAIT, um den Prozess zu verlangsamen.
Varianten
Das Framework bietet aktuell zwei unterschiedliche Varianten von Prozessen an, die zur Ausführung genutzt werden. Bist du im ABAP RESTful Programming Model unterwegs, solltest du die Variante "Controlled" verwenden, für alle anderen Anwendungsfälle bleibst du am besten bei "Uncontrolled".
Variante - Uncontrolled
Die unkontrollierte Variante arbeitet ohne zusätzliche Einschränkungen und wir können hier tun und lassen, was wir für unsere Arbeit benötigen. Dafür implementieren wir in der Klasse das Interface IF_BGMC_OP_SINGLE_TX_UNCONTR, welches die EXECUTE Methode zur Verfügung stellt. Diese wird später bei der Verarbeitung ausgeführt.
Prozess
Die Klasse mit dem Interface IF_BGMC_OP_SINGLE_TX_UNCONTR ist später unser Prozess der im Hintergrund läuft. Wir sollten also alle Daten dem Objekt zur Verfügung stellen, die aus unserem aktuell laufenden Prozess benötigt werden.
CLASS zcl_bs_demo_bgpf_process_uncon DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_bgmc_op_single_tx_uncontr.
METHODS constructor
IMPORTING it_data TYPE zcl_bs_demo_bgpf_data=>tt_data.
PRIVATE SECTION.
DATA mt_data TYPE zcl_bs_demo_bgpf_data=>tt_data.
ENDCLASS.
CLASS zcl_bs_demo_bgpf_process_uncon IMPLEMENTATION.
METHOD constructor.
mt_data = it_data.
ENDMETHOD.
METHOD if_bgmc_op_single_tx_uncontr~execute.
DATA(lo_table) = NEW zcl_bs_demo_bgpf_data( ).
LOOP AT mt_data INTO DATA(ls_data).
lo_table->add( ls_data ).
ENDLOOP.
lo_table->save( abap_true ).
ENDMETHOD.
ENDCLASS.
In diesem Beispiel besteht die Verarbeitung daraus, dass wir die übergebenen Datensätze übernehmen und über die SAVE Methode mit COMMIT WORK auf die Datenbank persistieren.
Ausführung
Wollen wir nun unseren Prozess ausführen, dann können wir dies wie folgt tun:
- Erzeugung der Operation (unser Prozessobjekt)
- Erzeugen eines Prozesses über die Factory
- Setzen des Prozessnamens
- Übergabe der Operation über die Methode SET_OPERATION_TX_UNCONTROLLED
- Speichern zur Ausführung
- COMMIT zur Ausführung des Prozesses
DATA lo_operation TYPE REF TO if_bgmc_op_single_tx_uncontr.
DATA(lt_data) = VALUE zcl_bs_demo_bgpf_data=>tt_data(
( description = 'Test 1' inumber = 12 amount = '12.54' currency = 'EUR' )
( description = 'Test 2' inumber = 95 amount = '0.21' currency = 'USD' )
( description = 'Test 3' inumber = 547 amount = '145.50' currency = 'EUR' ) ).
lo_operation = NEW zcl_bs_demo_bgpf_process_uncon( lt_data ).
DATA(lo_process) = cl_bgmc_process_factory=>get_default( )->create( ).
lo_process->set_name( 'Uncontrolled Process' )->set_operation_tx_uncontrolled( lo_operation ).
lo_process->save_for_execution( ).
COMMIT WORK.
Die Ausführung beginnt nur, wenn der COMMIT WORK gesetzt wird. Wird ein ROLLBACK wegen eines Fehlers durchgeführt, startet auch unser Prozess nicht. Dies soll sicherstellen, dass der Prozess nur im Erfolgsfall ausgeführt wird.
Variante - Controlled
Der Unterschied zur unkontrollierten Variante ist, dass hier die RAP Phasen durchlaufen werden, die über die Klasse CL_ABAP_TX angesteuert werden können. Das heißt wir haben eine Interaktionsphase und die Speichersequenz, die wir in unserem Prozess abbilden können. Die Klasse wird in diesem Fall mit dem Interface IF_BGMC_OP_SINGLE implementiert, es steht uns aber auch hier die EXECUTE Methode zur Verfügung.
Prozess
Wieder übernehmen wir die Daten im Prozess und speichern sie für die Ausführung. Bei der Verarbeitung können wir Optional in die Interaktionsphase wechseln (MODIFY) und die Daten übernehmen. Da wir hier eigentlich auch mit RAP arbeiten würden, würden mir unser EML Statement absetzen. Zum Abschluss wechseln wir in die Speichersequenz und können auch Statements wir INSERT oder UPDATE auf die Datenbank absetzen.
CLASS zcl_bs_demo_bgpf_process_contr DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_bgmc_op_single.
METHODS constructor
IMPORTING it_data TYPE zcl_bs_demo_bgpf_data=>tt_data.
PRIVATE SECTION.
DATA mt_data TYPE zcl_bs_demo_bgpf_data=>tt_data.
ENDCLASS.
CLASS zcl_bs_demo_bgpf_process_contr IMPLEMENTATION.
METHOD constructor.
mt_data = it_data.
ENDMETHOD.
METHOD if_bgmc_op_single~execute.
DATA(lo_table) = NEW zcl_bs_demo_bgpf_data( ).
" Optional
cl_abap_tx=>modify( ).
LOOP AT mt_data INTO DATA(ls_data).
lo_table->add( ls_data ).
ENDLOOP.
cl_abap_tx=>save( ).
lo_table->save( abap_false ).
ENDMETHOD.
ENDCLASS.
Hinweis: In dieser Variante dürfen wir keinen COMMIT WORK absetzen, dieser wird durch das Framework gesetzt und führt nur zu einem Fehler, wenn wir es versuchen sollten.
Ausführung
Die Ausführung ist sehr ähnlich zur ersten Variante aufgebaut, der einzige Unterschied ist die Methode SET_OPERATION die wir aufrufen, um unsere Operation ausführen zu lassen. Auch hier gilt, dass der Prozess erst nach dem COMMIT WORK ausgelöst wird.
DATA lo_operation TYPE REF TO if_bgmc_op_single.
DATA(lt_data) = VALUE zcl_bs_demo_bgpf_data=>tt_data(
( description = 'Control 1' inumber = 12 amount = '12.54' currency = 'EUR' )
( description = 'Control 2' inumber = 95 amount = '0.21' currency = 'USD' )
( description = 'Control 3' inumber = 547 amount = '145.50' currency = 'EUR' ) ).
lo_operation = NEW zcl_bs_demo_bgpf_process_contr( lt_data ).
DATA(lo_process) = cl_bgmc_process_factory=>get_default( )->create( ).
lo_process->set_name( 'Controlled Process' )->set_operation( lo_operation ).
lo_process->save_for_execution( ).
Monitoring
Wie geht es eigentlich weiter, wenn wir den Prozess gestartet haben? SAP bietet hier eine recht einfache Möglichkeit eines Monitorings an.
ID ableiten
Wenn wir uns die Methode SAVE_FOR_EXECUTION anschauen, finden wir dort ein Monitoring Objekt, welches wir bei Ausführung erhalten.
Wir können das Objekt direkt verwenden oder wir lassen uns die ID zurückgeben, um sie zu sichern und später in unterschiedlichen Sessions auszuwerten.
DATA(lo_process_monitor) = lo_process->save_for_execution( ).
DATA(ld_id) = lo_process_monitor->to_string( ).
Die Methode TO_STRING liefert uns dann ein XML Format mit den entsprechenden Informationen zum Prozess zurück. Hier einmal das aufbereitete XML, welches wir aus der Methode erhalten:
<?xml version="1.0" encoding="utf-16"?>
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
<asx:values>
<MONITOR href="#o146"/>
</asx:values>
<asx:heap xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:abap="http://www.sap.com/abapxml/types/built-in" xmlns:cls="http://www.sap.com/abapxml/classes/global" xmlns:dic="http://www.sap.com/abapxml/types/dictionary">
<cls:CL_BGMC_PROCESS_MONITOR id="o146">
<CL_BGMC_PROCESS_MONITOR>
<MV_BGRFC_UNIT_ID>Ghuf92k+Hu+I55PryC/zBw==</MV_BGRFC_UNIT_ID>
<MV_BGRFC_QUEUE/>
</CL_BGMC_PROCESS_MONITOR>
</cls:CL_BGMC_PROCESS_MONITOR>
</asx:heap>
</asx:abap>
Monitor erzeugen
Die ID können wir auf der Datenbank speichern oder für die weitere Verarbeitung nutzen. Über die ID können wir uns dann wieder das Monitoring Objekt erzeugen, um verschiedene Aktionen auszuführen. Im folgenden Quellcode erzeugen wir ein neuen Monitoring Objekt und lassen uns den Status des Prozess zurück geben.
DATA(lo_process_monitor) = cl_bgmc_process_factory=>create_monitor_from_string( ld_id ).
DATA(ld_state) = lo_process_monitor->get_state( ).
Im Interface IF_BGMC_PROCESS_MONITOR finden wir ein Enum (GCS_STATE) mit allen Konstanten, die den Status betreffen.
Ablauf
Testen wir nun einmal den Ablauf der Ausführung, unser Test ist wie folgt geplant:
- Erzeugung der Operation und Start des Prozesses über COMMIT WORK
- Erzeugung der ID über das Monitoring Objekt
- Prüfung des Status alle eine Sekunde und schreiben des UTC Timestamp, sowie des Status, in die Konsole
Ergebnisse
Die folgenden Ergebnisse erhalten wir nun als Ausgabe. Einmal die Ausgabe aus der Konsole:
Und als zweites die Ausgabe auf der Datenbank:
Auswertung
Nachdem der Prozess über den Commit gestartet wurde, prüfen wir das erste Mal den Status. Da dieser sich noch in der Queue befindet, erhalten wir den Status NEW zurückgeliefert. Nach ca. einer Sekunde befindet sich der Prozess in der Ausführung, die Timestamps der Datenbankeinträge werden bei Übernahme des Datensatzes gesetzt. Alle eine Sekunde, wird ein neuer Datensatz übernommen. So lange sieht unser Monitoring Prozess, dass der Prozess am Laufen ist. Nachdem die Datensätze abgearbeitet wurden, meldet der Prozess ein SUCCESSFUL zurück und unsere Logik beendet sich.
Daraus erkennen wir, dass die Verarbeitung der Queue nicht unmittelbar nach dem Hinzufügen startet, sondern der Prozess mit einer kurzen Verzögerung ausgeführt wird, er damit in einer Queue steht. Über das Monitoring können wir den Status des Prozesses verfolgen und dem Anwender eine Information dazu geben.
Vollständiges Beispiel
Zum Abschluss noch das vollständige Beispiel der ausführenden Klasse. Dabei werden beide Varianten durchlaufen und das Ergebnis in die Konsole geschrieben. Zu Beginn der Verarbeitung wird die Tabelle gelöscht, damit nur ein neues Ergebnis aufgenommen wird.
CLASS zcl_bs_demo_bgpf_start DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PRIVATE SECTION.
METHODS run_uncontrolled_process
RETURNING VALUE(rd_result) TYPE string.
METHODS run_controlled_process
RETURNING VALUE(rd_result) TYPE string.
METHODS wait_and_log
IMPORTING io_out TYPE REF TO if_oo_adt_classrun_out
id_string TYPE string
RAISING cx_bgmc.
ENDCLASS.
CLASS zcl_bs_demo_bgpf_start IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DELETE FROM zbs_dmo_bgpf.
COMMIT WORK.
out->write( `Start uncontrolled` ).
DATA(ld_string) = run_uncontrolled_process( ).
wait_and_log( io_out = out
id_string = ld_string ).
out->write( `Start controlled` ).
ld_string = run_controlled_process( ).
wait_and_log( io_out = out
id_string = ld_string ).
ENDMETHOD.
METHOD run_controlled_process.
DATA lo_operation TYPE REF TO if_bgmc_op_single.
DATA(lt_data) = VALUE zcl_bs_demo_bgpf_data=>tt_data(
( description = 'Control 1' inumber = 12 amount = '12.54' currency = 'EUR' )
( description = 'Control 2' inumber = 95 amount = '0.21' currency = 'USD' )
( description = 'Control 3' inumber = 547 amount = '145.50' currency = 'EUR' ) ).
lo_operation = NEW zcl_bs_demo_bgpf_process_contr( lt_data ).
TRY.
DATA(lo_process) = cl_bgmc_process_factory=>get_default( )->create( ).
lo_process->set_name( 'Controlled Process' )->set_operation( lo_operation ).
DATA(lo_process_monitor) = lo_process->save_for_execution( ).
COMMIT WORK.
RETURN lo_process_monitor->to_string( ).
CATCH cx_bgmc.
ROLLBACK WORK.
ENDTRY.
ENDMETHOD.
METHOD run_uncontrolled_process.
DATA lo_operation TYPE REF TO if_bgmc_op_single_tx_uncontr.
DATA(lt_data) = VALUE zcl_bs_demo_bgpf_data=>tt_data(
( description = 'Test 1' inumber = 12 amount = '12.54' currency = 'EUR' )
( description = 'Test 2' inumber = 95 amount = '0.21' currency = 'USD' )
( description = 'Test 3' inumber = 547 amount = '145.50' currency = 'EUR' ) ).
lo_operation = NEW zcl_bs_demo_bgpf_process_uncon( lt_data ).
TRY.
DATA(lo_process) = cl_bgmc_process_factory=>get_default( )->create( ).
lo_process->set_name( 'Uncontrolled Process' )->set_operation_tx_uncontrolled( lo_operation ).
DATA(lo_process_monitor) = lo_process->save_for_execution( ).
COMMIT WORK.
RETURN lo_process_monitor->to_string( ).
CATCH cx_bgmc.
ROLLBACK WORK.
ENDTRY.
ENDMETHOD.
METHOD wait_and_log.
DATA(lo_process_monitor) = cl_bgmc_process_factory=>create_monitor_from_string( id_string ).
DO.
IF sy-index = 60.
EXIT.
ENDIF.
DATA(ld_state) = lo_process_monitor->get_state( ).
io_out->write( ld_state ).
io_out->write( utclong_current( ) ).
IF ld_state = if_bgmc_process_monitor=>gcs_state-successful
OR ld_state = if_bgmc_process_monitor=>gcs_state-erroneous.
EXIT.
ENDIF.
WAIT UP TO 1 SECONDS.
ENDDO.
ENDMETHOD.
ENDCLASS.
Verfügbarkeit
Im ABAP Environment steht die Funktion bereits eine Weile zur Verfügung, hier musst du nicht auf das Release achten. On-Premise wird das Framework mit S/4 HANA 2023 ausgeliefert und kann nach einer kurzen Konfiguration genutzt werden. Mehr Details zur Konfiguration findest du unten im verlinkten Blog der SAP.
Hinweis: Die Monitoring Funktionalität zwischen Cloud und On-Premise Version kann abweichen, daher empfiehlt sich ein Blick in die offizielle Dokumentation der SAP.
Fazit
Mit bgPF und dem neuen Weg der Hintergrundverarbeitung steht dir eine einfache Alternative in ABAP Cloud zur Verfügung, um Prozesse auszulagern. Selbst ein Funktionsbaustein benötigt man damit nicht und kann komplett im OO-Kontext bleiben.
Weitere Informationen:
SAP Blog - Introducing the Background Processing Framework
YouTube - ABAP Cloud: Background Processing Framework