RAP mit asynchroner Verarbeitung (bgPF) - Background Processing Framework

kategorie
RAP
Veröffentlicht
autor
Johannes

Langläufige Operationen wie Massenverarbeitung, PDF-Generierung oder externe API-Aufrufe blockieren synchrone RAP-Requests und verschlechtern die User Experience. Das Background Processing Framework (bgPF) ermöglicht asynchrone Verarbeitung direkt aus RAP heraus – ohne klassische Background Jobs programmieren zu müssen.

Das Problem: Synchrone Verarbeitung

Blockierende Operations

" ❌ Synchron: Blockiert den Request für Sekunden/Minuten
METHOD generateReport FOR MODIFY
IMPORTING keys FOR ACTION Travel~generateReport RESULT result.
" PDF generieren - dauert 5-10 Sekunden
DATA(lv_pdf) = generate_pdf_for_travel( keys[ 1 ]-%tky ).
" Email versenden - dauert 2-3 Sekunden
send_email_with_attachment(
recipient = get_customer_email( keys[ 1 ]-%tky )
attachment = lv_pdf
).
" External System benachrichtigen - dauert 1-2 Sekunden
notify_external_system( keys[ 1 ]-%tky ).
" → Benutzer wartet 8-15 Sekunden!
ENDMETHOD.

Probleme bei synchroner Verarbeitung:

ProblemAuswirkung
Lange WartezeitBenutzer blockiert, schlechte UX
Timeout-RisikoHTTP-Timeout bei > 60 Sekunden
Keine SkalierungEin Request = ein Worker Process
FehleranfälligkeitExterne Systeme können fehlschlagen

Die Lösung: Background Processing Framework (bgPF)

Das bgPF ist das Cloud-native Framework für asynchrone Verarbeitung in ABAP Cloud. Es ersetzt klassische Background Jobs und bietet:

  • Deklarative Konfiguration über Behavior Definition
  • Automatische Wiederholung bei temporären Fehlern
  • Monitoring über SAP Fiori Apps
  • Integration mit RAP Business Events

Asynchrone Action in RAP

Schritt 1: Behavior Definition

managed implementation in class zbp_i_travel unique;
strict ( 2 );
define behavior for ZI_Travel alias Travel
persistent table ztravel
lock master
authorization master ( instance )
etag master LastChangedAt
{
create;
update;
delete;
" Synchrone Action - für schnelle Operationen
action approve result [1] $self;
" Asynchrone Action - für langläufige Operationen
action ( execution : background ) generateReport;
" Asynchrone Action mit Result (Callback)
action ( execution : background ) processExpenses;
" Asynchrone Factory Action
factory action ( execution : background ) createMassBookings [0..*];
}

Die Annotation ( execution : background ) markiert die Action als asynchron. Sie wird nicht im aktuellen Request ausgeführt, sondern in einen Background Worker ausgelagert.

Schritt 2: Implementation

CLASS lhc_travel DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS generateReport FOR MODIFY
IMPORTING keys FOR ACTION Travel~generateReport.
METHODS processExpenses FOR MODIFY
IMPORTING keys FOR ACTION Travel~processExpenses.
ENDCLASS.
CLASS lhc_travel IMPLEMENTATION.
METHOD generateReport.
" Diese Methode läuft im Background!
" 1. Travel-Daten laden
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_travels).
LOOP AT lt_travels ASSIGNING FIELD-SYMBOL(<travel>).
" 2. PDF generieren (zeitintensiv)
DATA(lv_pdf) = NEW zcl_travel_pdf_generator( )->generate(
travel_id = <travel>-TravelId
).
" 3. PDF in Attachment Service speichern
zcl_attachment_service=>store(
object_type = 'TRAVEL'
object_id = <travel>-TravelId
content = lv_pdf
filename = |travel_{ <travel>-TravelId }.pdf|
).
" 4. Status aktualisieren
MODIFY ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( ReportStatus ReportGeneratedAt )
WITH VALUE #( (
%tky = <travel>-%tky
ReportStatus = 'G' " Generated
ReportGeneratedAt = cl_abap_context_info=>get_system_date( )
) ).
" 5. Event auslösen für weitere Verarbeitung
RAISE ENTITY EVENT zi_travel~ReportGenerated
FROM VALUE #( (
%key = <travel>-%key
%param-PdfUrl = |/attachments/travel_{ <travel>-TravelId }.pdf|
) ).
ENDLOOP.
ENDMETHOD.
METHOD processExpenses.
" Expenses verarbeiten und Summen berechnen
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_travels).
LOOP AT lt_travels ASSIGNING FIELD-SYMBOL(<travel>).
" Externe API aufrufen für Wechselkurse
DATA(lo_http) = cl_web_http_client_manager=>create_by_http_destination(
cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = 'Z_CURRENCY_API'
service_id = 'Z_CURRENCY_SRV'
)
).
" Expenses aus Child Entity laden
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel BY \_Expenses
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_expenses).
" Summe berechnen und konvertieren
DATA(lv_total) = REDUCE abap_dec15_2(
INIT sum = 0
FOR expense IN lt_expenses
NEXT sum = sum + convert_currency(
amount = expense-Amount
from = expense-Currency
to = <travel>-Currency
)
).
" Travel aktualisieren
MODIFY ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( TotalExpenses ExpensesProcessedAt )
WITH VALUE #( (
%tky = <travel>-%tky
TotalExpenses = lv_total
ExpensesProcessedAt = cl_abap_context_info=>get_system_date( )
) ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Schritt 3: UI-Integration

Die asynchrone Action wird in der UI wie eine normale Action angezeigt:

define root view entity ZC_Travel
provider contract transactional_query
as projection on ZI_Travel
{
@UI.lineItem: [
{ position: 10 },
{ type: #FOR_ACTION, dataAction: 'approve', label: 'Genehmigen' },
{ type: #FOR_ACTION, dataAction: 'generateReport', label: 'Report erstellen' }
]
key TravelUUID,
@UI.fieldGroup: [{ qualifier: 'Status', position: 10 }]
ReportStatus,
@UI.fieldGroup: [{ qualifier: 'Status', position: 20 }]
ReportGeneratedAt
}

Verhalten der asynchronen Action

Request-Flow

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │ │ RAP/OData │ │ bgPF │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ POST /action │ │
│──────────────────>│ │
│ │ │
│ │ Schedule Task │
│ │──────────────────>│
│ │ │
│ HTTP 202 │ │
│<──────────────────│ │
│ (Accepted) │ │
│ │ │
│ │ ┌─────────┴─────────┐
│ │ │ Background Worker │
│ │ │ │
│ │ │ generateReport() │
│ │ │ ...läuft... │
│ │ │ ...fertig! │
│ │ └───────────────────┘

Wichtige Punkte:

  1. HTTP 202 Accepted: Der Client erhält sofort eine Bestätigung, dass die Action angenommen wurde
  2. Keine Blockierung: Der Request ist in Millisekunden beendet
  3. Asynchrone Ausführung: Die eigentliche Arbeit passiert im Background Worker
  4. Automatic Retry: Bei temporären Fehlern wird automatisch wiederholt

Status-Feedback an den Benutzer

Da die Action asynchron läuft, muss der Status über andere Wege kommuniziert werden:

" Option 1: Status-Feld aktualisieren
MODIFY ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( ReportStatus )
WITH VALUE #( (
%tky = <travel>-%tky
ReportStatus = 'P' " Processing
) ).
" Nach Abschluss
MODIFY ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( ReportStatus ReportGeneratedAt )
WITH VALUE #( (
%tky = <travel>-%tky
ReportStatus = 'C' " Completed
ReportGeneratedAt = cl_abap_context_info=>get_system_date( )
) ).
" Option 2: Business Event auslösen
RAISE ENTITY EVENT zi_travel~ReportGenerated
FROM VALUE #( ( %key = <travel>-%key ) ).

Integration mit RAP Business Events

Das bgPF arbeitet optimal mit Business Events zusammen. Events ermöglichen eine lose Kopplung zwischen der asynchronen Action und nachfolgenden Verarbeitungsschritten.

Event Definition in Behavior

define behavior for ZI_Travel alias Travel
{
" Asynchrone Action
action ( execution : background ) generateReport;
" Events für Statusänderungen
event ReportGenerated parameter ZA_ReportEvent;
event ReportFailed parameter ZA_ErrorEvent;
}

Event Payload

define abstract entity ZA_ReportEvent
{
TravelId : abap.numc( 8 );
PdfUrl : abap.string( 255 );
Timestamp : timestampl;
}
define abstract entity ZA_ErrorEvent
{
TravelId : abap.numc( 8 );
ErrorCode : abap.char( 10 );
ErrorMessage : abap.string( 255 );
}

Event Consumer für Benachrichtigung

CLASS zcl_travel_event_handler DEFINITION
PUBLIC
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_event_handler.
ENDCLASS.
CLASS zcl_travel_event_handler IMPLEMENTATION.
METHOD if_rap_event_handler~handle_event.
" Event-Typ prüfen
CASE io_event->get_event_name( ).
WHEN 'REPORTGENERATED'.
" Payload lesen
DATA(ls_payload) = io_event->get_payload( ).
DATA(lv_travel_id) = ls_payload-TravelId.
DATA(lv_pdf_url) = ls_payload-PdfUrl.
" Kunden per Email benachrichtigen
DATA(lo_travel) = NEW zcl_travel_service( ).
DATA(ls_travel) = lo_travel->get_by_id( lv_travel_id ).
NEW zcl_email_service( )->send(
to = ls_travel-CustomerEmail
subject = |Ihr Reisebericht ist verfügbar|
body = |Der Report für Ihre Reise ist fertig: { lv_pdf_url }|
).
WHEN 'REPORTFAILED'.
" Administrator benachrichtigen
DATA(ls_error) = io_event->get_payload( ).
NEW zcl_notification_service( )->notify_admin(
subject = |Report-Generierung fehlgeschlagen|
message = |Travel { ls_error-TravelId }: { ls_error-ErrorMessage }|
).
ENDCASE.
ENDMETHOD.
ENDCLASS.

Event Handler Registrierung

Der Event Handler wird über die Service Binding registriert:

" In der Service Binding oder über die Event Handler Registry
CLASS zcl_event_handler_registry DEFINITION
PUBLIC
FINAL.
PUBLIC SECTION.
CLASS-METHODS register_handlers.
ENDCLASS.
CLASS zcl_event_handler_registry IMPLEMENTATION.
METHOD register_handlers.
" Registrierung erfolgt deklarativ über Annotation
" @EventHandler für entsprechende Events
ENDMETHOD.
ENDCLASS.

Fehlerbehandlung und Retry

Transiente vs. Permanente Fehler

METHOD generateReport.
TRY.
" Externe API aufrufen
DATA(lo_http) = cl_web_http_client_manager=>create_by_http_destination(
cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = 'Z_PDF_SERVICE'
)
).
DATA(lo_request) = lo_http->get_http_request( ).
lo_request->set_uri_path( '/generate' ).
DATA(lo_response) = lo_http->execute( if_web_http_client=>post ).
DATA(lv_status) = lo_response->get_status( )-code.
CASE lv_status.
WHEN 200 TO 299.
" Erfolg - PDF verarbeiten
process_pdf_response( lo_response ).
WHEN 429 OR 503.
" Transient Error - Retry durch Framework
" Werfe retryable Exception
RAISE EXCEPTION TYPE cx_bgpf_retryable
EXPORTING
textid = cx_bgpf_retryable=>service_temporarily_unavailable.
WHEN OTHERS.
" Permanenter Fehler - Kein Retry
RAISE ENTITY EVENT zi_travel~ReportFailed
FROM VALUE #( (
%key = keys[ 1 ]-%key
%param-ErrorCode = |HTTP_{ lv_status }|
%param-ErrorMessage = lo_response->get_text( )
) ).
ENDCASE.
CATCH cx_http_dest_provider_error
cx_web_http_client_error INTO DATA(lx_http).
" Netzwerkfehler - normalerweise transient
RAISE EXCEPTION TYPE cx_bgpf_retryable
EXPORTING
previous = lx_http.
ENDTRY.
ENDMETHOD.

Retry-Konfiguration

Das bgPF bietet automatische Retry-Mechanismen:

ParameterDefaultBeschreibung
max_retries3Maximale Anzahl Wiederholungen
retry_delay60sWartezeit zwischen Versuchen
backoff_factor2Exponentieller Backoff-Multiplikator
max_delay3600sMaximale Wartezeit
" Retry-Verhalten kann über Annotations gesteuert werden
define behavior for ZI_Travel alias Travel
{
" Action mit spezifischer Retry-Konfiguration
action ( execution : background, retries : 5, delay : 120 )
processLargeDataset;
}

Massenverarbeitung mit bgPF

Parallele Verarbeitung

METHOD processAllOpenTravels.
" Alle offenen Reisen laden
SELECT * FROM ztravel
WHERE status = 'O'
INTO TABLE @DATA(lt_travels).
" Parallel verarbeiten durch Batch-Scheduling
DATA(lv_batch_size) = 100.
DATA(lv_offset) = 0.
WHILE lv_offset < lines( lt_travels ).
" Batch extrahieren
DATA(lt_batch) = VALUE ty_travel_tab(
FOR i = lv_offset WHILE i < lv_offset + lv_batch_size
( lt_travels[ i ] )
).
" Background Task für jeden Batch
cl_bgpf_process_api=>schedule(
EXPORTING
iv_process_name = 'Z_PROCESS_TRAVEL_BATCH'
it_parameters = VALUE #(
( name = 'BATCH_DATA' value = cl_abap_conv_out_ce=>create( )->write( lt_batch ) )
)
).
lv_offset = lv_offset + lv_batch_size.
ENDWHILE.
ENDMETHOD.

Batch-Prozess Implementation

CLASS zcl_travel_batch_processor DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_bgpf_process.
ENDCLASS.
CLASS zcl_travel_batch_processor IMPLEMENTATION.
METHOD if_bgpf_process~execute.
" Parameter auslesen
DATA(lv_batch_data) = it_parameters[ name = 'BATCH_DATA' ]-value.
DATA lt_travels TYPE TABLE OF ztravel.
" Daten deserialisieren
cl_abap_conv_in_ce=>create( input = lv_batch_data )->read(
IMPORTING data = lt_travels
).
" Batch verarbeiten
LOOP AT lt_travels ASSIGNING FIELD-SYMBOL(<travel>).
TRY.
process_single_travel( <travel> ).
CATCH cx_root INTO DATA(lx_error).
" Fehler loggen, aber fortfahren
log_error( travel = <travel> error = lx_error ).
ENDTRY.
ENDLOOP.
" Ergebnis
rv_result = if_bgpf_process=>c_result-success.
ENDMETHOD.
ENDCLASS.

Monitoring und Überwachung

Application Logging

CLASS zcl_bgpf_logger DEFINITION.
PUBLIC SECTION.
METHODS log_start
IMPORTING iv_action TYPE string
iv_object_id TYPE string.
METHODS log_success
IMPORTING iv_action TYPE string
iv_object_id TYPE string
iv_details TYPE string OPTIONAL.
METHODS log_error
IMPORTING iv_action TYPE string
iv_object_id TYPE string
ix_error TYPE REF TO cx_root.
PRIVATE SECTION.
DATA mo_log TYPE REF TO if_bali_log.
ENDCLASS.
CLASS zcl_bgpf_logger IMPLEMENTATION.
METHOD log_start.
mo_log = cl_bali_log=>create( ).
mo_log->set_header(
header = cl_bali_header_setter=>create(
object = 'Z_BGPF'
subobject = iv_action
external_id = iv_object_id
)
).
mo_log->add_item(
item = cl_bali_free_text_setter=>create(
severity = if_bali_constants=>c_severity_information
text = |Background Task gestartet: { iv_action }|
)
).
cl_bali_log_db=>get_instance( )->save_log( log = mo_log ).
ENDMETHOD.
METHOD log_success.
mo_log->add_item(
item = cl_bali_free_text_setter=>create(
severity = if_bali_constants=>c_severity_status
text = |Erfolgreich abgeschlossen: { iv_details }|
)
).
cl_bali_log_db=>get_instance( )->save_log( log = mo_log ).
ENDMETHOD.
METHOD log_error.
mo_log->add_item(
item = cl_bali_exception_setter=>create(
severity = if_bali_constants=>c_severity_error
exception = ix_error
)
).
cl_bali_log_db=>get_instance( )->save_log( log = mo_log ).
ENDMETHOD.
ENDCLASS.

Monitoring-Fiori-App

Die Standard-Fiori-App Manage Background Tasks (App ID: F3840) zeigt:

  • Laufende Background Tasks
  • Fehlgeschlagene Tasks mit Details
  • Retry-Historie
  • Performance-Metriken

Programmatische Status-Abfrage

" Task-Status abfragen
DATA(lo_monitor) = cl_bgpf_process_api=>get_monitor( ).
DATA(lt_tasks) = lo_monitor->get_tasks(
iv_process_name = 'Z_GENERATE_REPORT'
iv_from_date = sy-datum - 7
iv_to_date = sy-datum
).
LOOP AT lt_tasks INTO DATA(ls_task).
WRITE: / ls_task-task_id,
ls_task-status,
ls_task-started_at,
ls_task-completed_at.
ENDLOOP.
" Fehlgeschlagene Tasks
DATA(lt_failed) = lo_monitor->get_failed_tasks(
iv_process_name = 'Z_GENERATE_REPORT'
).
LOOP AT lt_failed INTO DATA(ls_failed).
WRITE: / |Task { ls_failed-task_id } fehlgeschlagen: { ls_failed-error_message }|.
ENDLOOP.

Best Practices

1. Idempotenz sicherstellen

Background Tasks können mehrfach ausgeführt werden (Retry). Die Logik muss idempotent sein:

" ✅ Idempotent: Prüft ob schon verarbeitet
METHOD generateReport.
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
FIELDS ( ReportStatus )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_travels).
LOOP AT lt_travels ASSIGNING FIELD-SYMBOL(<travel>).
" Bereits generiert? Skip!
IF <travel>-ReportStatus = 'C'.
CONTINUE.
ENDIF.
" Report generieren...
ENDLOOP.
ENDMETHOD.

2. Timeout-Bewusstsein

" Große Datenmengen in Chunks verarbeiten
METHOD processMassData.
DATA(lv_chunk_size) = 1000.
DATA(lv_processed) = 0.
SELECT * FROM zlarge_table
INTO TABLE @DATA(lt_chunk)
UP TO @lv_chunk_size ROWS
OFFSET @lv_processed.
WHILE lt_chunk IS NOT INITIAL.
process_chunk( lt_chunk ).
lv_processed = lv_processed + lines( lt_chunk ).
" Checkpoint für lange Operationen
IF lv_processed MOD 10000 = 0.
COMMIT WORK.
ENDIF.
SELECT * FROM zlarge_table
INTO TABLE @lt_chunk
UP TO @lv_chunk_size ROWS
OFFSET @lv_processed.
ENDWHILE.
ENDMETHOD.

3. Fehler kommunizieren

" Bei Fehler: Status und Event setzen
TRY.
" Verarbeitung...
CATCH cx_root INTO DATA(lx_error).
" Status auf Error setzen
MODIFY ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( ProcessingStatus ErrorMessage )
WITH VALUE #( (
%tky = keys[ 1 ]-%tky
ProcessingStatus = 'E' " Error
ErrorMessage = lx_error->get_text( )
) ).
" Error-Event für Monitoring
RAISE ENTITY EVENT zi_travel~ProcessingFailed
FROM VALUE #( (
%key = keys[ 1 ]-%key
%param-ErrorMessage = lx_error->get_text( )
) ).
ENDTRY.

4. Transaktionsgrenzen beachten

" ❌ Falsch: COMMIT in RAP-Handler
METHOD processData.
COMMIT WORK. " Nicht erlaubt!
ENDMETHOD.
" ✅ Richtig: Transaktionssteuerung dem Framework überlassen
" bgPF macht automatisch COMMIT nach erfolgreicher Ausführung

bgPF vs. Klassische Background Jobs

AspektbgPFJOB_OPEN/SUBMIT
DeklarationBehavior DefinitionABAP Code
IntegrationNative RAP-IntegrationSeparate Programmierung
RetryAutomatischManuell implementieren
MonitoringStandard Fiori AppSM37
Cloud-readyJaNur On-Premise
Event-IntegrationNative UnterstützungManuell
TransaktionskontextRAP-managedSelbst verwalten

Weiterführende Themen