Parallel Processing no ABAP Cloud: o guia prático do CL_ABAP_PARALLEL
Por que paralelizar no ABAP Cloud?
Um loop que processa 5.000 registros, cada um com uma chamada a uma API externa de 300ms, leva 25 minutos rodando sequencialmente. Distribuído em 10 work processes, cai para menos de 3. Essa é a promessa do parallel processing — e no ABAP Cloud ela tem nome e sobrenome: a classe liberada CL_ABAP_PARALLEL.
A ideia é simples. Num sistema ABAP, os dialog work processes ficam ociosos boa parte do tempo. O parallel processing aproveita esses processos livres para distribuir trabalho independente, em vez de empilhar tudo num único processo. Você não ganha CPU mágica — ganha throughput, fazendo várias coisas ao mesmo tempo em vez de uma de cada vez.
Mas paralelizar errado piora tudo: derruba a performance dos usuários, cria deadlocks e adiciona complexidade onde não havia ganho. Este guia mostra como usar o CL_ABAP_PARALLEL com exemplos realistas e — o que mais importa — os casos onde você não deve chegar perto dele.
O essencial em 5 linhas
CL_ABAP_PARALLELé uma classe liberada (released) no ABAP Cloud que orquestra execução paralela via aRFC em dialog work processes.Sua lógica vai numa classe de task que implementa
if_abap_parallel(métododo) eif_serializable_object; você dispara comrun_inst.A documentação fixa um piso: não vale paralelizar operações abaixo de ~200ms — abaixo disso o custo de transferir dados entre processos engole o ganho.
Cada task roda no seu próprio LUW:
COMMIT ENTITIES/COMMIT WORKsão permitidos dentro da task — mas isso também significa que não existe rollback global automático.Use para trabalho pesado e independente (APIs externas, BAPIs, EML em massa). Não use para lógica leve, set-based (vai para CDS/AMDP) ou com dependência de ordem.
O que é o CL_ABAP_PARALLEL?
O CL_ABAP_PARALLEL é uma solução orientada a objetos para paralelização baseada em aRFC (RFC assíncrona), liberada para o ABAP Cloud Development Model (SAP Help — Parallel Processing). Ela é a orquestradora: distribui as tarefas pelos work processes livres, controla quantos rodam ao mesmo tempo e coleta os resultados de volta.
A lógica que roda em paralelo não vive na classe orquestradora — vive numa classe de task que você escreve, implementando a interface if_abap_parallel. Essa interface tem um único método, do, que é o ponto de entrada: tudo que deve rodar em paralelo entra ali.
INTERFACE if_abap_parallel PUBLIC.
METHODS do.
ENDINTERFACE.Há um requisito que pega muita gente de surpresa: a classe de task também precisa implementar if_serializable_object. O motivo é arquitetural — para uma instância viajar do seu processo até um work process separado, ela precisa ser serializada e desserializada do outro lado. Sem essa interface, a serialização falha.
Não está no escopo liberado: o paralelismo "clássico" do ABAP on-premise — CALL FUNCTION ... STARTING NEW TASK com SPBT_INITIALIZE — não faz parte da linguagem liberada no ABAP Cloud. Não é questão de estar "obsoleto": ele simplesmente não compila no Clean Core. O caminho liberado é o CL_ABAP_PARALLEL.
Herança ou interface: qual cenário usar?
A classe oferece dois modelos de uso, e a diferença é grande na prática. A documentação e a comunidade convergem na mesma recomendação: prefira o cenário de interface.
Critério | Cenário 1 — Herança ( | Cenário 2 — Interface ( |
|---|---|---|
Como a classe se liga ao framework |
|
|
Como os dados entram e saem |
| Direto pela instância (constructor + getter) |
Verbosidade | Alta — você gerencia a serialização | Baixa — o framework cuida disso |
Legibilidade / manutenção | Menor | Maior |
O cenário de herança só compensa quando você precisa de controle absoluto sobre a serialização binária — um caso raro. Para 95% dos projetos, o cenário de interface é mais simples, mais legível e menos sujeito a erro. Daqui para frente, todos os exemplos usam run_inst.
Como configurar o constructor sem derrubar o sistema?
O constructor do CL_ABAP_PARALLEL é onde você decide quanto do sistema vai consumir. Configurar errado aqui é a forma número um de transformar uma otimização em um incidente de produção.
Parâmetro | Tipo | O que faz |
|---|---|---|
|
| Número fixo de processos para a paralelização. |
|
| Percentual dos dialog WP livres a usar (0–100). Default 50%. Tem precedência sobre |
|
| Timeout (segundos) por task. 0 = o sistema calcula automaticamente. |
|
|
|
|
|
|
|
| Controles finos de processos simultâneos e de retenção das instâncias após a execução. |
Duas regras da documentação oficial que você não pode ignorar:
"Transportar os dados para um dialog process livre leva algum tempo. Portanto, não é eficiente paralelizar operações muito curtas. O limite de tempo fica em torno de 200 milissegundos." Abaixo disso, o overhead supera o ganho.
"Dois dos dialog processes livres em um servidor são sempre reservados pelo sistema para requisições de diálogo." Ou seja: por mais que você peça, o sistema protege os usuários interativos.
" Conservador para ambiente produtivo: no máximo 30% dos WP livres,
" e apenas no servidor local (comportamento mais previsível).
DATA(lo_parallel) = NEW cl_abap_parallel(
p_percentage = 30
p_local_server = abap_true
p_timeout = 60
).Regra prática de dimensionamento: em produção, comece com p_num_tasks = 3 a 5 (ou p_percentage = 30) e monitore SM50/SM66 sob carga real antes de subir. Mais tasks não significam linearmente mais velocidade — significam mais disputa por work processes com os usuários finais.
Exemplo real 1: enriquecer itens com uma API externa de frete
Este é o caso de uso clássico e mais seguro: cada item é independente, e a chamada externa (300–500ms de latência de rede) cruza com folga o piso de 200ms. Imagine 5.000 itens de pedido que precisam de uma cotação de frete vinda de uma API REST externa.
Primeiro, a classe de task. Ela recebe um item no constructor, faz a chamada no método do e guarda o resultado para ser lido depois.
CLASS zcl_frete_task DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_abap_parallel. " ponto de entrada paralelo
INTERFACES if_serializable_object. " OBRIGATÓRIO p/ trafegar entre processos
METHODS constructor
IMPORTING is_item TYPE zif_frete=>ts_item.
METHODS get_result
RETURNING VALUE(rs_result) TYPE zif_frete=>ts_result.
PRIVATE SECTION.
DATA ms_item TYPE zif_frete=>ts_item.
DATA ms_result TYPE zif_frete=>ts_result.
ENDCLASS.
CLASS zcl_frete_task IMPLEMENTATION.
METHOD constructor.
ms_item = is_item.
ENDMETHOD.
METHOD if_abap_parallel~do.
" Roda num work process separado, com LUW próprio.
" Tudo que precisa de paralelismo vai aqui.
ms_result-item_id = ms_item-item_id.
TRY.
DATA(lo_dest) = cl_http_destination_provider=>create_by_url(
|https://api.frete.exemplo/v1/cotacao| ).
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination( lo_dest ).
DATA(lo_req) = lo_client->get_http_request( ).
lo_req->set_uri_query( |cep={ ms_item-cep_destino }&peso={ ms_item-peso }| ).
DATA(lo_resp) = lo_client->execute( if_web_http_client=>get ).
ms_result-valor = parse_valor( lo_resp->get_text( ) ).
ms_result-ok = abap_true.
CATCH cx_root INTO DATA(lx).
" Falha de negócio NÃO deve derrubar a task: trate e registre.
ms_result-ok = abap_false.
ms_result-message = lx->get_text( ).
ENDTRY.
ENDMETHOD.
METHOD get_result.
rs_result = ms_result.
ENDMETHOD.
ENDCLASS.Agora o chamador: monta uma instância por item, dispara run_inst (que bloqueia até tudo terminar) e lê os resultados de cada instância.
DATA lt_tasks TYPE cl_abap_parallel=>t_in_inst_tab.
" 1. Uma instância por item de entrada
LOOP AT lt_itens INTO DATA(ls_item).
INSERT NEW zcl_frete_task( ls_item ) INTO TABLE lt_tasks.
ENDLOOP.
" 2. Executa em paralelo (síncrono — espera terminar)
NEW cl_abap_parallel( p_num_tasks = 10 )->run_inst(
EXPORTING p_in_tab = lt_tasks
IMPORTING p_out_tab = DATA(lt_done)
).
" 3. Coleta resultado de cada instância
LOOP AT lt_done INTO DATA(ls_done).
IF ls_done-message IS INITIAL.
" Task rodou (sem dump/timeout) — pegue o resultado de negócio
DATA(ls_res) = CAST zcl_frete_task( ls_done-inst )->get_result( ).
IF ls_res-ok = abap_true.
INSERT ls_res INTO TABLE lt_cotacoes.
ENDIF.
ELSE.
" Task abortou na infraestrutura: ls_done-message tem o motivo
" (ex.: "Time limit exceeded" ou short text de runtime error)
INSERT VALUE #( item_id = '?' texto = ls_done-message ) INTO TABLE lt_falhas.
ENDIF.
ENDLOOP.Repare na separação de dois níveis de erro: o campo message da saída sinaliza falha de infraestrutura (dump, timeout, sem WP livre). Falha de negócio (a API respondeu 404) você trata dentro do do e expõe no seu próprio resultado (ok/message). Misturar os dois é uma fonte comum de bug.
Exemplo real 2 (avançado): EML com COMMIT dentro da task
Aqui está o caso mais poderoso — e mais perigoso se mal posicionado. Como cada task tem seu próprio LUW, você pode executar MODIFY ENTITIES seguido de COMMIT ENTITIES dentro do do — algo proibido dentro de determinations e validations de um RAP BO gerenciado.
METHOD if_abap_parallel~do.
MODIFY ENTITIES OF zmy_task_bo
ENTITY ZMyTask
CREATE FROM VALUE #( (
%cid = 'CID_001'
MaterialID = mv_material
Title = |Task para { mv_material }|
OverallStatus = 'O'
%control = VALUE #(
MaterialID = if_abap_behv=>mk-on
Title = if_abap_behv=>mk-on
OverallStatus = if_abap_behv=>mk-on ) ) )
MAPPED DATA(lt_mapped)
FAILED DATA(lt_failed)
REPORTED DATA(lt_reported).
" COMMIT no LUW DESTA task — independente do chamador
IF lt_failed IS INITIAL.
COMMIT ENTITIES RESPONSE OF zmy_task_bo
FAILED DATA(lt_commit_failed)
REPORTED DATA(lt_commit_reported).
IF lt_commit_failed IS INITIAL.
mv_result_uuid = lt_mapped-zmytask[ 1 ]-TaskUUID.
ENDIF.
ENDIF.
ENDMETHOD.Onde NÃO chamar isto: nunca dispare parallel processing de dentro da save sequence de um RAP BO gerenciado (métodos save_modified, finalize etc.). Naquele ponto, o framework RAP é o dono da transação e está prestes a fazer o save coordenado. Um COMMIT ENTITIES de um BO diferente, num LUW paralelo, comita seu próprio trabalho e quebra o contrato all-or-nothing: se o save principal falhar e der rollback, o que a task paralela já comitou permanece. Você fica com dados órfãos.
O lugar certo para esse padrão é fora do LUW transacional do RAP: um relatório/classe utilitária disparado pelo usuário, uma Application Job de carga em massa, ou uma ação RAP cujo único papel é orquestrar a criação em lote — sempre ciente de que cada task comita por conta própria e a consistência global é responsabilidade sua. Se você só precisa manipular entidades RAP em memória, veja primeiro os componentes do EML no RAP sem commit paralelo.
Dá para chamar BAPIs com isso?
Dá — e é um dos encaixes mais naturais do CL_ABAP_PARALLEL. Cada task roda no seu próprio LUW, o que resolve justamente o que trava BAPIs em contexto transacional: dentro do do você dispara a BAPI e faz o commit. Só vale a regra de ouro de BAPI — não se usa COMMIT WORK direto, e sim BAPI_TRANSACTION_COMMIT (e BAPI_TRANSACTION_ROLLBACK no erro).
METHOD if_abap_parallel~do.
" LUW próprio da task -> commit isolado é permitido.
DATA lt_return TYPE TABLE OF bapiret2.
CALL FUNCTION 'BAPI_PR_CREATE'
EXPORTING prheader = ms_input-header
IMPORTING number = ms_result-pr_number
TABLES return = lt_return
pritem = ms_input-items.
IF line_exists( lt_return[ type = 'E' ] ) OR line_exists( lt_return[ type = 'A' ] ).
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ms_result-ok = abap_false.
ms_result-message = VALUE #( lt_return[ type = 'E' ]-message OPTIONAL ).
ELSE.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = abap_true.
ms_result-ok = abap_true.
ENDIF.
ENDMETHOD.A pegadinha não está no paralelismo — está no escopo de APIs liberadas. No ABAP Cloud só compila código liberado, então uma BAPI clássica não pode ser chamada diretamente. O que muda é o ambiente:
Ambiente | É possível? | Como |
|---|---|---|
S/4HANA Public Cloud (Tier 1) | Em geral não com BAPI clássica | As |
Private Cloud / on-premise (modelo 3-tier) | Sim, via wrapper | Gera-se um wrapper com |
Quando falta uma API liberada, a própria SAP recomenda embrulhar a BAPI clássica num wrapper e liberar o wrapper para uso em cloud (SAP Community — Wrapper para BAPIs). No Public Cloud puro isso não existe; lá a melhor opção costuma ser justamente o EML do exemplo anterior.
Cuidado com enqueue locks: BAPIs costumam bloquear (enqueue) os objetos que alteram. Se você paralelizar BAPIs que tocam os mesmos registros, vai colher lock conflict ou deadlock. Paralelize apenas quando cada BAPI opera sobre um objeto de negócio independente — ex.: criar 2.000 requisições de compra distintas, nunca várias linhas do mesmo documento ao mesmo tempo.
FORK / JOIN: quando você não quer esperar
O run_inst é síncrono — bloqueia até a última task terminar. Quando você quer disparar o trabalho e continuar fazendo outra coisa enquanto ele roda, o par fork_inst / join_inst resolve. O fork_inst dispara sem bloquear; o join_inst só bloqueia no momento em que você realmente precisa dos resultados.
DATA(lo_parallel) = NEW cl_abap_parallel( p_num_tasks = 5 ).
" FORK: dispara e NÃO bloqueia
lo_parallel->fork_inst( EXPORTING p_in_tab = lt_tasks ).
" === enquanto as tasks rodam, faça trabalho local independente ===
preparar_estrutura_saida( ).
ler_dados_auxiliares( ).
" JOIN: agora sim, espera e coleta
lo_parallel->join_inst( IMPORTING p_out_tab = DATA(lt_done) ).O caso típico: disparar chamadas a APIs externas em paralelo e usar o tempo de latência para preparar localmente a estrutura que receberá os resultados.
Tratamento de erros: a tabela de saída nunca mente
A documentação garante um comportamento que simplifica muito a coleta de resultados: "a tabela de saída tem sempre o mesmo número de entradas que a tabela de entrada, independentemente de as tasks terem sido executadas ou não". Você sempre recebe uma resposta por task — sucesso ou falha.
O que vem em | Causa | Ação recomendada |
|---|---|---|
| A task passou de | Aumentar o timeout ou quebrar a task em pedaços menores |
Short text de runtime error | Dump dentro da task (ex.: acesso inválido) | Corrigir o código do |
Mensagem do framework | Sem work process livre, timeout de RFC | Reduzir |
O parâmetro p_abort_on_error = abap_true aborta toda a execução paralela assim que qualquer task falha. Use-o somente quando as tasks são interdependentes e o fracasso de uma invalida o lote inteiro — o que, por definição, é raro num cenário de trabalho independente. E nunca ignore o message em silêncio: registre as falhas (Application Log, cl_bali_*) para reprocessamento.
Como debugar tasks paralelas?
Debugar processos paralelos é difícil porque cada task roda num processo separado — o breakpoint do seu programa principal não para lá. A SAP resolve isso com o parâmetro p_debug, presente em run, run_inst, fork e fork_inst.
" p_debug = abap_true: roda as tasks SEQUENCIALMENTE, sem RFC.
" O debugger do ADT para normalmente dentro do método DO.
NEW cl_abap_parallel( p_num_tasks = 3 )->run_inst(
EXPORTING
p_in_tab = lt_tasks
p_debug = abap_true
IMPORTING
p_out_tab = DATA(lt_done)
).Fluxo de debug: ative p_debug = abap_true → ponha o breakpoint em if_abap_parallel~do → rode no ADT (o debugger para em cada task, em sequência) → validada a lógica, remova o p_debug e teste com paralelismo real. Sempre faça os dois testes: a lógica pode estar certa em modo sequencial e ainda assim ter um problema de concorrência (lock, ordem) só visível em paralelo.
Quando você DEVE paralelizar
Cenário | Por que funciona |
|---|---|
Chamadas a APIs externas (REST/SOAP) | Latência de rede (>200ms) justifica de sobra; cada chamada é isolada |
Processamento em massa de registros independentes | Cada registro é autocontido, sem depender do resultado de outro |
Criação em massa via RAP EML (fora da save sequence) | Cada task tem LUW próprio; |
Chamadas a BAPIs liberadas com COMMIT WORK interno | O commit cabe no LUW da task |
Cargas/migrações divididas em pacotes | Lotes homogêneos processados ao mesmo tempo |
O indicador resumido: o trabalho é de camada de aplicação (não set-based), cada iteração dura mais de 200ms, as iterações são independentes e o tempo sequencial total é inaceitável para o usuário ou o SLA do job.
Quando NÃO usar (a parte que mais importa)
Situação | Motivo |
|---|---|
Operações abaixo de ~200ms | O overhead de transferir dados entre processos supera o ganho |
Lógica set-based (somas, joins, agregações) | Pertence ao banco — empurre para CDS/AMDP, não para o app server |
Poucos registros (< 5–10) | Criar e coordenar processos custa mais do que rende |
Dependência de ordem ou de resultado entre iterações | Não há garantia de ordem nem de sincronização entre tasks |
Requisito transacional all-or-nothing | Cada task tem LUW próprio; rollback global não é trivial |
Locks de banco sobre os mesmos registros | Tasks concorrentes podem gerar deadlock |
Dentro da save sequence do RAP / poucos WP livres | Quebra o contrato transacional e/ou degrada os usuários interativos |
O erro de arquitetura mais comum: paralelizar no app server algo que era para ser set-based no HANA. Se a sua "lógica pesada" é somar, agrupar ou cruzar tabelas, a resposta não é distribuir o loop — é eliminar o loop, empurrando o cálculo para uma CDS View ou um AMDP. O CL_ABAP_PARALLEL brilha quando o trabalho não pode ser set-based: chamadas externas, BAPIs, EML. Para cálculo de dados, o paralelismo certo é o do próprio banco.
E os erros de implementação que mais derrubam projetos:
" ❌ ERRADO: contar com dados estáticos/globais compartilhados.
" Cada task tem contexto de memória PRÓPRIO — não enxerga isto.
CLASS-DATA gt_cache TYPE string_table.
" ❌ ERRADO: passar objeto complexo não-serializável no constructor.
" Referências a outros objetos podem não sobreviver à serialização.
METHODS constructor IMPORTING io_servico TYPE REF TO zcl_complexo.
" ✅ CERTO: constructor recebe só dados primitivos/estruturas simples.
METHODS constructor IMPORTING iv_doc TYPE vbeln
iv_org TYPE bukrs.CL_ABAP_PARALLEL vs Application Jobs vs bgPF
Paralelizar não é a única forma de tirar trabalho do caminho do usuário. No ABAP Cloud, três frameworks liberados resolvem problemas diferentes — escolher o errado é tão custoso quanto não usar nenhum.
Critério |
| Application Jobs | bgPF |
|---|---|---|---|
Execução | Síncrona ou fork/join | Agendada (background) | Assíncrona (background) |
Resultado na mesma execução | Sim — você aguarda e coleta | Não — vê no monitor/log | Não — desacoplado |
LUW próprio por unidade | Sim, por task | Sim, por job | Sim |
Caso ideal | Lote paralelo in-process, resultado imediato | Jobs recorrentes/agendados | Tarefa longa assíncrona por evento |
CL_ABAP_PARALLEL→ você precisa do resultado agora, na mesma execução, e o trabalho se divide em pedaços paralelizáveis.Application Jobs → processamento recorrente/agendado, com visibilidade de operação no Fiori e log amarrado ao job.
bgPF (Background Processing Framework) → tarefa demorada disparada por evento, cujo resultado não é necessário de imediato.
Checklist antes de ir para produção
As tasks são genuinamente independentes (sem ordem, sem ler resultado de outra)?
Cada task dura mais de 200ms e o volume justifica o paralelismo?
A classe de task implementa
if_abap_paralleleif_serializable_object?O constructor recebe só dados primitivos/estruturas simples (nada de objetos complexos)?
p_num_tasks/p_percentagedimensionados de forma conservadora e testados sob carga (SM50/SM66)?O campo
messagede cada saída é verificado e as falhas são logadas (nunca ignoradas)?Validado primeiro com
p_debug = abap_truee depois com paralelismo real?Se há
COMMITdentro da task: ele está fora da save sequence do RAP?
Perguntas frequentes
CL_ABAP_PARALLEL está liberado no S/4HANA Public Cloud?
Sim. A classe tem status released e faz parte do ABAP Cloud Development Model, disponível no S/4HANA Cloud Public Edition e no ABAP Environment (BTP). O paralelismo clássico (CALL FUNCTION ... STARTING NEW TASK) é que não está no escopo liberado — para Clean Core, o caminho é o CL_ABAP_PARALLEL.
Posso fazer COMMIT dentro de uma task paralela?
Pode. Cada task roda no seu próprio LUW, então COMMIT ENTITIES, COMMIT WORK e BAPIs com commit interno são permitidos dentro do método do. A contrapartida é que não existe rollback global automático: se você precisa de all-or-nothing entre as tasks, o paralelismo não é a ferramenta certa.
Quantas tasks devo configurar?
Comece baixo — p_num_tasks = 3 a 5, ou p_percentage = 30 — e meça. O sistema sempre reserva 2 dialog work processes para os usuários, e mais tasks disputam WP com quem está usando o sistema. Suba só depois de monitorar SM50/SM66 sob carga real; o ganho não é linear.
Por que minha classe de task dá erro de serialização?
Quase sempre porque faltou INTERFACES if_serializable_object na classe, ou porque o constructor recebe um objeto complexo não-serializável (uma referência a outro objeto). A instância precisa ser serializada para viajar até o work process; passe apenas tipos primitivos e estruturas simples.
Como sei se uma task falhou ou só não rodou?
A tabela de saída tem sempre o mesmo número de entradas que a de entrada. Para cada linha, verifique message: se vier vazia, a task rodou (faça o CAST e leia seu resultado); se vier preenchida, houve falha de infraestrutura (timeout, dump, sem WP) e o texto explica o motivo. Falhas de negócio você expõe no seu próprio resultado, não no message.
Conclusão
O CL_ABAP_PARALLEL é a ferramenta certa para um problema específico: trabalho de camada de aplicação, pesado (>200ms por item), independente e que precisa de resultado na mesma execução. Implemente if_abap_parallel + if_serializable_object, dispare com run_inst, dimensione com cautela e trate cada message — e você corta tempo de processamento de minutos para segundos.
O segredo, porém, está em saber recusar. Se a lógica é set-based, ela vai para CDS/AMDP. Se precisa de all-or-nothing, paralelismo não serve. Se cada item dura 10ms, o overhead vence. Paralelizar bem é, antes de tudo, paralelizar só o que merece — e deixar o resto onde ele já estava certo.
Referências oficiais
Comentários 0
Ainda sem comentários
Seja o primeiro a comentar este artigo.
Entre na conversa
Faça login para deixar seu comentário neste artigo.