Les opérations de longue durée comme le traitement de masse, la génération de PDF ou les appels API externes bloquent les requêtes RAP synchrones et dégradent l’expérience utilisateur. Le Background Processing Framework (bgPF) permet le traitement asynchrone directement depuis RAP – sans avoir besoin de programmer des jobs en arrière-plan classiques.
Le problème : Traitement synchrone
Opérations bloquantes
" ❌ Synchrone : Bloque la requête pendant des secondes/minutesMETHOD generateReport FOR MODIFY IMPORTING keys FOR ACTION Travel~generateReport RESULT result.
" Générer un PDF - prend 5-10 secondes DATA(lv_pdf) = generate_pdf_for_travel( keys[ 1 ]-%tky ).
" Envoyer un email - prend 2-3 secondes send_email_with_attachment( recipient = get_customer_email( keys[ 1 ]-%tky ) attachment = lv_pdf ).
" Notifier le système externe - prend 1-2 secondes notify_external_system( keys[ 1 ]-%tky ).
" → L'utilisateur attend 8-15 secondes !ENDMETHOD.Problèmes du traitement synchrone :
| Problème | Impact |
|---|---|
| Temps d’attente long | Utilisateur bloqué, mauvaise UX |
| Risque de timeout | Timeout HTTP à > 60 secondes |
| Pas d’évolutivité | Une requête = un processus worker |
| Sensible aux erreurs | Les systèmes externes peuvent échouer |
La solution : Background Processing Framework (bgPF)
Le bgPF est le framework cloud-native pour le traitement asynchrone dans ABAP Cloud. Il remplace les jobs en arrière-plan classiques et offre :
- Configuration déclarative via Behavior Definition
- Nouvelle tentative automatique en cas d’erreurs temporaires
- Monitoring via les applications SAP Fiori
- Intégration avec RAP Business Events
Action asynchrone dans RAP
Étape 1 : Behavior Definition
managed implementation in class zbp_i_travel unique;strict ( 2 );
define behavior for ZI_Travel alias Travelpersistent table ztravellock masterauthorization master ( instance )etag master LastChangedAt{ create; update; delete;
" Action synchrone - pour les opérations rapides action approve result [1] $self;
" Action asynchrone - pour les opérations de longue durée action ( execution : background ) generateReport;
" Action asynchrone avec résultat (Callback) action ( execution : background ) processExpenses;
" Factory Action asynchrone factory action ( execution : background ) createMassBookings [0..*];}L’annotation ( execution : background ) marque l’action comme asynchrone. Elle n’est pas exécutée dans la requête actuelle, mais déléguée à un worker en arrière-plan.
Étape 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. " Cette méthode s'exécute en arrière-plan !
" 1. Charger les données Travel 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. Générer le PDF (chronophage) DATA(lv_pdf) = NEW zcl_travel_pdf_generator( )->generate( travel_id = <travel>-TravelId ).
" 3. Enregistrer le PDF dans le service d'attachement zcl_attachment_service=>store( object_type = 'TRAVEL" object_id = <travel>-TravelId content = lv_pdf filename = |travel_{ <travel>-TravelId }.pdf| ).
" 4. Mettre à jour le statut 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. Déclencher l'événement pour un traitement ultérieur RAISE ENTITY EVENT zi_travel~ReportGenerated FROM VALUE #( ( %key = <travel>-%key %param-PdfUrl = |/attachments/travel_{ <travel>-TravelId }.pdf| ) ). ENDLOOP. ENDMETHOD.
METHOD processExpenses. " Traiter les dépenses et calculer les totaux 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>). " Appeler l'API externe pour les taux de change 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" ) ).
" Charger les dépenses depuis l'entité enfant READ ENTITIES OF zi_travel IN LOCAL MODE ENTITY Travel BY \_Expenses ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_expenses).
" Calculer la somme et convertir 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 ) ).
" Mettre à jour Travel 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.Étape 3 : Intégration UI
L’action asynchrone s’affiche dans l’UI comme une action normale :
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: 'Approuver' }, { type: #FOR_ACTION, dataAction: 'generateReport', label: 'Créer le rapport' } ] key TravelUUID,
@UI.fieldGroup: [{ qualifier: 'Status', position: 10 }] ReportStatus,
@UI.fieldGroup: [{ qualifier: 'Status', position: 20 }] ReportGeneratedAt}Comportement de l’action asynchrone
Flux de requête
┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ Client │ │ RAP/OData │ │ bgPF │└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ POST /action │ │ │──────────────────>│ │ │ │ │ │ │ Schedule Task │ │ │──────────────────>│ │ │ │ │ HTTP 202 │ │ │<──────────────────│ │ │ (Accepted) │ │ │ │ │ │ │ ┌─────────┴─────────┐ │ │ │ Background Worker │ │ │ │ │ │ │ │ generateReport() │ │ │ │ ...en cours... │ │ │ │ ...terminé! │ │ │ └───────────────────┘Points importants :
- HTTP 202 Accepted : Le client reçoit immédiatement une confirmation que l’action a été acceptée
- Pas de blocage : La requête se termine en millisecondes
- Exécution asynchrone : Le travail réel s’effectue dans le worker en arrière-plan
- Automatic Retry : En cas d’erreurs temporaires, une nouvelle tentative est automatique
Retour d’état à l’utilisateur
Comme l’action s’exécute de manière asynchrone, le statut doit être communiqué par d’autres moyens :
" Option 1 : Mettre à jour le champ de statutMODIFY ENTITIES OF zi_travel IN LOCAL MODE ENTITY Travel UPDATE FIELDS ( ReportStatus ) WITH VALUE #( ( %tky = <travel>-%tky ReportStatus = 'P' " Processing ) ).
" Après achèvementMODIFY 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 : Déclencher un Business EventRAISE ENTITY EVENT zi_travel~ReportGenerated FROM VALUE #( ( %key = <travel>-%key ) ).Intégration avec RAP Business Events
Le bgPF fonctionne de manière optimale avec Business Events. Les événements permettent un couplage lâche entre l’action asynchrone et les étapes de traitement suivantes.
Définition d’événement dans Behavior
define behavior for ZI_Travel alias Travel{ " Action asynchrone action ( execution : background ) generateReport;
" Événements pour les changements de statut 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 pour notification
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. " Vérifier le type d'événement CASE io_event->get_event_name( ). WHEN 'REPORTGENERATED'. " Lire le payload DATA(ls_payload) = io_event->get_payload( ). DATA(lv_travel_id) = ls_payload-TravelId. DATA(lv_pdf_url) = ls_payload-PdfUrl.
" Notifier le client par e-mail 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 = |Votre rapport de voyage est disponible| body = |Le rapport de votre voyage est prêt : { lv_pdf_url }| ).
WHEN 'REPORTFAILED'. " Notifier l'administrateur DATA(ls_error) = io_event->get_payload( ).
NEW zcl_notification_service( )->notify_admin( subject = |Échec de génération de rapport| message = |Travel { ls_error-TravelId }: { ls_error-ErrorMessage }| ). ENDCASE. ENDMETHOD.ENDCLASS.Enregistrement du Event Handler
Le Event Handler est enregistré via le Service Binding :
" Dans le Service Binding ou via le Event Handler RegistryCLASS zcl_event_handler_registry DEFINITION PUBLIC FINAL.
PUBLIC SECTION. CLASS-METHODS register_handlers.ENDCLASS.
CLASS zcl_event_handler_registry IMPLEMENTATION. METHOD register_handlers. " L'enregistrement se fait de manière déclarative via annotation " @EventHandler pour les événements correspondants ENDMETHOD.ENDCLASS.Gestion des erreurs et Retry
Erreurs transitoires vs. permanentes
METHOD generateReport. TRY. " Appeler l'API externe 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. " Succès - traiter le PDF process_pdf_response( lo_response ).
WHEN 429 OR 503. " Erreur transitoire - Retry par le framework " Lever une exception retryable RAISE EXCEPTION TYPE cx_bgpf_retryable EXPORTING textid = cx_bgpf_retryable=>service_temporarily_unavailable.
WHEN OTHERS. " Erreur permanente - Pas de 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). " Erreur réseau - normalement transitoire RAISE EXCEPTION TYPE cx_bgpf_retryable EXPORTING previous = lx_http. ENDTRY.ENDMETHOD.Configuration du Retry
Le bgPF offre des mécanismes de retry automatiques :
| Paramètre | Défaut | Description |
|---|---|---|
| max_retries | 3 | Nombre maximum de tentatives |
| retry_delay | 60s | Temps d’attente entre les tentatives |
| backoff_factor | 2 | Multiplicateur de backoff exponentiel |
| max_delay | 3600s | Temps d’attente maximum |
" Le comportement de retry peut être contrôlé via des annotationsdefine behavior for ZI_Travel alias Travel{ " Action avec configuration de retry spécifique action ( execution : background, retries : 5, delay : 120 ) processLargeDataset;}Traitement de masse avec bgPF
Traitement parallèle
METHOD processAllOpenTravels. " Charger tous les voyages ouverts SELECT * FROM ztravel WHERE status = 'O" INTO TABLE @DATA(lt_travels).
" Traiter en parallèle par batch scheduling DATA(lv_batch_size) = 100. DATA(lv_offset) = 0.
WHILE lv_offset < lines( lt_travels ). " Extraire le batch DATA(lt_batch) = VALUE ty_travel_tab( FOR i = lv_offset WHILE i < lv_offset + lv_batch_size ( lt_travels[ i ] ) ).
" Tâche en arrière-plan pour chaque 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.Implémentation du processus par batch
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. " Lire les paramètres DATA(lv_batch_data) = it_parameters[ name = 'BATCH_DATA' ]-value. DATA lt_travels TYPE TABLE OF ztravel.
" Désérialiser les données cl_abap_conv_in_ce=>create( input = lv_batch_data )->read( IMPORTING data = lt_travels ).
" Traiter le batch LOOP AT lt_travels ASSIGNING FIELD-SYMBOL(<travel>). TRY. process_single_travel( <travel> ). CATCH cx_root INTO DATA(lx_error). " Enregistrer l'erreur, mais continuer log_error( travel = <travel> error = lx_error ). ENDTRY. ENDLOOP.
" Résultat rv_result = if_bgpf_process=>c_result-success. ENDMETHOD.ENDCLASS.Monitoring et surveillance
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 = |Tâche en arrière-plan démarrée : { 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 = |Terminé avec succès : { 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.Application Fiori de monitoring
L’application Fiori standard Manage Background Tasks (App ID : F3840) affiche :
- Tâches en arrière-plan en cours
- Tâches ayant échoué avec détails
- Historique des tentatives
- Métriques de performance
Requête de statut programmatique
" Interroger le statut des tâchesDATA(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.
" Tâches ayant échouéDATA(lt_failed) = lo_monitor->get_failed_tasks( iv_process_name = 'Z_GENERATE_REPORT").
LOOP AT lt_failed INTO DATA(ls_failed). WRITE: / |Tâche { ls_failed-task_id } a échoué : { ls_failed-error_message }|.ENDLOOP.Bonnes pratiques
1. Assurer l’idempotence
Les tâches en arrière-plan peuvent être exécutées plusieurs fois (Retry). La logique doit être idempotente :
" ✅ Idempotent : Vérifie si déjà traité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>). " Déjà généré ? Ignorer ! IF <travel>-ReportStatus = 'C'. CONTINUE. ENDIF.
" Générer le rapport... ENDLOOP.ENDMETHOD.2. Conscience du timeout
" Traiter les grandes quantités de données par morceauxMETHOD 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 pour les opérations longues 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. Communiquer les erreurs
" En cas d'erreur : définir le statut et l'événementTRY. " Traitement... CATCH cx_root INTO DATA(lx_error). " Définir le statut sur Error 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( ) ) ).
" Événement d'erreur pour le monitoring RAISE ENTITY EVENT zi_travel~ProcessingFailed FROM VALUE #( ( %key = keys[ 1 ]-%key %param-ErrorMessage = lx_error->get_text( ) ) ).ENDTRY.4. Respecter les limites de transaction
" ❌ Faux : COMMIT dans le handler RAPMETHOD processData. COMMIT WORK. " Non autorisé !ENDMETHOD.
" ✅ Correct : Laisser le framework gérer les transactions" bgPF fait automatiquement COMMIT après exécution réussiebgPF vs. Jobs en arrière-plan classiques
| Aspect | bgPF | JOB_OPEN/SUBMIT |
|---|---|---|
| Déclaration | Behavior Definition | Code ABAP |
| Intégration | Intégration RAP native | Programmation séparée |
| Retry | Automatique | Implémentation manuelle |
| Monitoring | Application Fiori standard | SM37 |
| Cloud-ready | Oui | On-Premise uniquement |
| Intégration d’événements | Support natif | Manuel |
| Contexte transactionnel | RAP-managed | Géré soi-même |
Sujets avancés
- RAP Actions et Functions - Logique métier synchrone
- RAP Business Events - Architecture événementielle
- Background Jobs - Traitement en arrière-plan classique
- Application Logging - Logs dans ABAP Cloud