RAP avec traitement asynchrone (bgPF) - Background Processing Framework

Catégorie
RAP
Publié
Auteur
Johannes

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/minutes
METHOD 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èmeImpact
Temps d’attente longUtilisateur bloqué, mauvaise UX
Risque de timeoutTimeout HTTP à > 60 secondes
Pas d’évolutivitéUne requête = un processus worker
Sensible aux erreursLes 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 Travel
persistent table ztravel
lock master
authorization 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 :

  1. HTTP 202 Accepted : Le client reçoit immédiatement une confirmation que l’action a été acceptée
  2. Pas de blocage : La requête se termine en millisecondes
  3. Exécution asynchrone : Le travail réel s’effectue dans le worker en arrière-plan
  4. 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 statut
MODIFY ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
UPDATE FIELDS ( ReportStatus )
WITH VALUE #( (
%tky = <travel>-%tky
ReportStatus = 'P' " Processing
) ).
" Après achèvement
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 : Déclencher un Business Event
RAISE 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 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.
" 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ètreDéfautDescription
max_retries3Nombre maximum de tentatives
retry_delay60sTemps d’attente entre les tentatives
backoff_factor2Multiplicateur de backoff exponentiel
max_delay3600sTemps d’attente maximum
" Le comportement de retry peut être contrôlé via des annotations
define 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âches
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.
" 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 morceaux
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 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énement
TRY.
" 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 RAP
METHOD processData.
COMMIT WORK. " Non autorisé !
ENDMETHOD.
" ✅ Correct : Laisser le framework gérer les transactions
" bgPF fait automatiquement COMMIT après exécution réussie

bgPF vs. Jobs en arrière-plan classiques

AspectbgPFJOB_OPEN/SUBMIT
DéclarationBehavior DefinitionCode ABAP
IntégrationIntégration RAP nativeProgrammation séparée
RetryAutomatiqueImplémentation manuelle
MonitoringApplication Fiori standardSM37
Cloud-readyOuiOn-Premise uniquement
Intégration d’événementsSupport natifManuel
Contexte transactionnelRAP-managedGéré soi-même

Sujets avancés