ABAP Processing Center: como implementar processos com retry, monitor e retomada no ABAP Cloud
O passo 3 de 5 falhou. E agora?
Todo projeto tem um job assim. Ele percorre milhares de documentos e, para cada um, valida, chama uma BAPI ou uma action RAP e grava alguma coisa. Com dez registros no teste, perfeito. Em produção, o documento 4.712 está bloqueado, o 9.030 tem cadastro incompleto, e o job termina com um log que ninguém do negócio consegue ler.
Aí vêm as perguntas de sempre: o que já foi feito? Dá para rodar de novo sem duplicar? Como corrigir os 20 que falharam e continuar só com eles?
O ABAP Processing Center (ABAP-PRC) é um framework open source da Brandeis Consulting, publicado em setembro de 2026, que responde isso de um jeito padrão. Em uma frase: ele é um gerente de tarefas para processamento em massa. Anota o progresso de cada objeto, explica os erros, tenta de novo sozinho e deixa você retomar sem refazer o que já deu certo.
Instalei num S/4HANA e testei recurso por recurso. Este post mostra o que cada um faz, quando usar, e como fica nos apps de monitoramento que vêm junto.
É beta
Versão 0.1.0, licença MIT, um mantenedor só. O próprio README avisa que a API pode mudar sem aviso na 0.2. Ótimo para estudar e pilotar, mas com cuidado num processo crítico.
Onde isso aparece no dia a dia
Antes do código, alguns cenários em que eu pensaria no PRC. Repare que todos têm a mesma cara: muitos objetos, mais de um passo por objeto, e falha parcial como algo normal.
Cenário | Etapas por objeto | O que o PRC resolve |
|---|---|---|
Reajuste anual de contratos | Validar contrato → recalcular preço → atualizar condição → avisar o responsável | Contrato bloqueado entra no retry; cadastro errado fica no monitor esperando correção, sem travar os outros |
Carga de planilha (clientes, materiais, equipamentos) | Validar linha → criar o objeto → criar os dependentes | Cada linha vira um objeto: o usuário vê quais linhas falharam e por quê, corrige e retoma só elas |
Pedidos recebidos por API | Gravar o que chegou → validar → criar a ordem → confirmar para o sistema de origem | A API só grava e responde; o processamento pesado roda depois, com fila por pedido para respeitar a ordem das alterações |
Integração com sistema externo | Montar o envio → chamar o serviço → gravar o retorno | Sistema fora do ar se resolve no retry automático, sem ninguém reprocessar na mão |
Aprovação por valor | Conferir → (acima do limite: revisar) → efetivar | O fork manda só os casos caros para a etapa de revisão |
Para os testes deste post, usei o caso mais simples possível, para não perder tempo explicando regra de negócio.
O exemplo do teste: desconto em viagens
Usei um BO RAP de viagens (ZR_TRAVEL), com uma action deductDiscount. A tarefa: dar 10% de desconto em 12 viagens. Só que o desconto só vale para viagem aberta, e metade delas não está. Ou seja, eu já sabia que 6 iam falhar, e era exatamente isso que eu queria ver.
O processo tem duas etapas:
Validar a viagem (existe? está aberta?)
Aplicar o desconto
O que você precisa escrever
Para colocar um processo no PRC são quatro peças. As três primeiras são código seu, a quarta já vem pronta:
A classe do processo: diz quais são as etapas.
Um handler por etapa: o trabalho de verdade de cada passo.
Um ponto de entrada: quem entrega os objetos ao PRC (um job, uma API, um evento).
O monitor: três apps Fiori que já vêm no pacote.
1. O processo é dividido em etapas
Cada objeto (aqui, cada viagem) tem um estado: começa em START, passa pelos estados que você criar e termina em FINISHED. A classe do processo implementa zif_prc_process e só responde duas perguntas: quais são os passos, e qual classe executa cada um.
CLASS zcl_prc_travel_disc_proc DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_prc_process.
CONSTANTS co_process_name TYPE zprc_process_name VALUE 'TRAVEL_DISCOUNT'.
CONSTANTS co_validated TYPE zif_prc_process=>ty_state VALUE 'VALIDATED'.
ENDCLASS.
CLASS zcl_prc_travel_disc_proc IMPLEMENTATION.
METHOD zif_prc_process~get_transitions.
" START -> VALIDATED -> FINISHED
rt_transitions = VALUE #(
( start_state = zif_prc_process=>co_start end_state = co_validated )
( start_state = co_validated end_state = zif_prc_process=>co_finished ) ).
ENDMETHOD.
METHOD zif_prc_process~get_transition_handler.
CASE i_start_state.
WHEN zif_prc_process=>co_start. ro_transition_handler = NEW lcl_validate( ).
WHEN co_validated. ro_transition_handler = NEW lcl_apply_discount( ).
ENDCASE.
ENDMETHOD.
ENDCLASS.Cada passo é uma classe que herda de zcl_prc_transition_handlr_base. Você implementa o trabalho (perform_transition) e as duas mensagens que vão aparecer no monitor: a de sucesso e a de falha. O passo de validar ficou assim:
METHOD perform_transition.
READ ENTITIES OF zr_travel
ENTITY Travel FIELDS ( Status )
WITH VALUE #( ( TravelUUID = i_processed_object_ext_uuid ) )
RESULT DATA(lt_travel).
IF lt_travel[ 1 ]-Status <> 'O'.
MESSAGE e002(zprc_travel_demo) WITH i_processed_object_ext_id lt_travel[ 1 ]-Status
INTO DATA(lv_msg).
get_message_handler( )->add_message_from_sy( ). " erro: o passo para aqui
ENDIF.
ENDMETHOD.
METHOD get_success_message.
MESSAGE s003(zprc_travel_demo) WITH i_processed_object_ext_id INTO DATA(lv_msg).
r_message = CORRESPONDING #( sy ). " "Viagem 1 validada"
ENDMETHOD.
METHOD get_failure_message.
get_processed_object( IMPORTING e_processed_object_ext_id = DATA(lv_id) ).
MESSAGE e007(zprc_travel_demo) WITH lv_id INTO DATA(lv_msg).
r_message = CORRESPONDING #( sy ). " "A viagem 2 não pôde ser validada"
ENDMETHOD.E o de aplicar o desconto chama a action do RAP e entrega o resultado ao PRC:
METHOD perform_transition.
MODIFY ENTITIES OF zr_travel
ENTITY Travel EXECUTE deductDiscount
FROM VALUE #( ( TravelUUID = i_processed_object_ext_uuid
%param-discount_percent = '10' ) )
FAILED DATA(ls_failed) REPORTED DATA(ls_reported).
get_message_handler( )->add_eml_modify_result( i_failed = ls_failed
i_reported = ls_reported ).
ENDMETHOD.Repare no que não tem aí: nenhum COMMIT. Quem grava é o PRC, depois de cada passo. Se o passo foi bem, ele grava a alteração da viagem e o registro do passo juntos. Se falhou, desfaz tudo e grava só o erro.
O que o PRC faz com cada objeto, passo a passo. Você escreve só o miolo (perform_transition).
Como decidir as etapas: crie uma etapa nova em cada ponto em que faria sentido parar e continuar depois. Validar e efetivar são etapas diferentes, porque se a efetivação falhar você não quer validar de novo. Já "ler o contrato" e "calcular o preço" provavelmente cabem na mesma etapa.
2. Entregar os objetos ao PRC
Com o processo pronto, você entrega os objetos. Um por viagem:
zcl_prc_processing_api=>get_instance( )->create_processed_objects(
i_create_processed_objects = VALUE #(
FOR t IN lt_travel
( processedObject = t-travel_id
processedObjectUUID = t-travel_uuid
processName = 'TRAVEL_DISCOUNT'
factoryClassName = 'ZCL_PRC_TRAVEL_DISC_PROC'
payloadJson = `{"DISCOUNT_PERCENT":10}` ) )
i_perform_commit = abap_true
i_trigger_processing = zcl_prc_processing_api=>execution_mode-direct_execution ).O payloadJson leva os dados que o processo precisa e que não estão no próprio objeto (aqui, o percentual; numa carga de planilha, a linha inteira). Cada viagem foi tratada sozinha: as 6 abertas terminaram com desconto, as 6 outras falharam, e uma não atrapalhou a outra.
O último parâmetro diz quando processar:
Modo | O que acontece | Quando usar |
|---|---|---|
| Processa na hora, na mesma sessão | Dentro de um job, que já roda em background |
| Entrega ao background processing framework e volta | API ou ação do usuário que não pode esperar o processamento terminar |
| Agenda um application job com o retry job do PRC | Volume grande disparado a partir de uma tela |
| Só grava; o retry job pega depois | Receber agora e processar na próxima janela |
3. Erro com explicação
Quando um passo falha, o PRC guarda duas coisas: um resumo ("A viagem 2 não pôde ser validada"), que vem do get_failure_message, e a causa ("Viagem 2 não está aberta (status A)"), que é a mensagem que derrubou o passo.
Escreva o resumo pensando no usuário de negócio: o que não aconteceu com o documento dele. A causa técnica vem logo abaixo.
Além de mensagem própria e de resultado de EML, o message handler entende retorno de BAPI. Um E na BAPIRET2 derruba o passo, e os W e S da mesma tabela ficam registrados junto:
CALL FUNCTION 'BAPI_...'
EXPORTING ...
TABLES return = lt_return.
get_message_handler( )->add_bapi_result( lt_return ). " E ou A no RETURN: o passo falhaExiste também o add_message_from_text, para erro que só existe como texto. Funciona, mas o texto fica picado nas variáveis da mensagem e aparece com espaços estranhos no monitor. Use classe de mensagem sempre que der.
4. Retry automático
Objeto que falhou é tentado de novo sozinho, em intervalos cada vez maiores. Medi no teste e bateu certinho com o código:
5 min, 55 min, 23 h e 6 h. Depois da quinta falha o PRC desiste e espera alguém olhar.
Quando ajuda: registro bloqueado por outro usuário, sistema externo fora do ar, documento que ainda não foi criado por outro processo. Problemas que se resolvem sozinhos com o tempo.
Quando não ajuda: erro de negócio. O PRC não diferencia um do outro, então uma viagem rejeitada vai falhar em todas as tentativas:
A viagem 10 estava rejeitada. Cada tentativa virou um passo novo, com a causa logo abaixo. (Na última a causa mudou para "não encontrada" porque eu tinha recriado os dados de teste no meio do caminho: o PRC registra a causa de cada tentativa.)
Não causa estrago, só ruído. Quem dispara as tentativas é o retry job do PRC, que você agenda como application job (a cada 5 minutos, por exemplo).
5. Corrigir e retomar
Este é o recurso que mais me convenceu. Abri as viagens que tinham falhado e mandei rodar de novo. Só as pendentes rodaram, cada uma continuou do passo em que tinha parado, e as que já estavam em FINISHED não levaram desconto duas vezes.
Dá para retomar de três jeitos:
Pelo monitor: botão Resume Processing, na linha do objeto ou na mensagem de erro (este último retoma todos os objetos que falharam com aquela mensagem).
Pelo retry job, com o parâmetro
P_IGNRmarcado, que ignora a espera do retry e inclui objetos que já esgotaram as tentativas.Por código, chamando o motor com os mesmos parâmetros:
zcl_prc_processing_engine=>get_instance( )->execute_synchronously( VALUE #(
sign = 'I' option = 'EQ'
( selname = zcl_prc_retry_job=>c_process_name low = 'TRAVEL_DISCOUNT' )
( selname = zcl_prc_retry_job=>p_ignore_restart low = abap_true ) ) ).A viagem 4 falhou, foi corrigida e terminou. O histórico fica inteiro: o erro, a validação e o desconto.
Testei também uma viagem aberta em edição (com draft) durante o processamento. O RAP bloqueou o desconto e o objeto parou em VALIDATED. Descartei o draft, retomei, e ele foi direto para o desconto, sem validar de novo. É exatamente o caso do "usuário esqueceu o documento aberto": a etapa feita fica feita.
6. Tudo ou nada
Se um passo altera algo e depois dá erro, a alteração é desfeita. Testei com um passo que mudava a descrição da viagem e falhava logo em seguida: a descrição voltou ao original. A viagem nunca fica pela metade.
Antes de gravar, o PRC ainda faz uma simulação do save. Se uma validação do BO recusar (no teste, data final antes da inicial), nada é gravado, mesmo que o comando EML em si tenha sido aceito.
O que o rollback não desfaz
Chamada HTTP, e-mail enviado, BAPI que faz o próprio COMMIT WORK: isso sai da LUW e não volta. Se um passo faz algo assim, confira no começo dele se a tentativa anterior já fez o trabalho (por exemplo, guardando o ID retornado pelo sistema externo). Assim, rodar de novo não duplica nada.
7. Agendar para depois
Na criação do objeto existe o campo doNotProcessBefore. Criei uma tarefa para dali a uma hora e, mesmo pedindo execução imediata, o PRC esperou. Passado o horário, processou normalmente.
( processedObject = t-travel_id
...
doNotProcessBefore = lv_amanha_22h ) " timestamp: antes disso o motor ignora o objetoQuando usar: mudança que só pode valer a partir de uma data, processamento pesado que deve rodar na janela da noite, ou um passo que depende de algo que outro sistema só manda mais tarde.
8. Caminhos diferentes (fork)
Um passo pode ter mais de uma saída. No teste, viagem acima de R$ 3.000 passava por uma etapa de revisão, e a mais barata terminava direto. Nas transições você declara as duas saídas, e o handler devolve qual seguir:
" Transições: CHECKED -> FINISHED e CHECKED -> REVIEW -> FINISHED
METHOD perform_transition.
" lv_price lido da viagem via READ ENTITIES
r_new_state = COND #( WHEN lv_price > 3000 THEN 'REVIEW'
ELSE zif_prc_process=>co_finished ).
ENDMETHOD.No monitor, a barra de progresso mostra "2 de 2" para quem foi direto e "3 de 3" para quem passou pela revisão.
Quando usar: aprovação por valor, tratamento diferente por tipo de documento, ou um passo extra só para clientes de determinado país.
9. Filas, quando a ordem importa
Às vezes um objeto só pode rodar depois do outro. Três alterações do mesmo pedido, por exemplo: a terceira não pode ser aplicada antes da primeira. Para isso existem os campos queueID e queuePosition:
( processedObject = ch-change_id
queueID = |PEDIDO_{ ch-sales_order }| " uma fila por pedido
queuePosition = ch-sequence
... )No teste, coloquei duas viagens em cada fila e fiz a primeira da fila Q-AG4 falhar. A segunda da Q-AG4 nem rodou: ficou esperando com a mensagem "Stopped working on queue Q-AG4". A outra fila seguiu normalmente. Corrigi a primeira viagem, retomei, e as duas terminaram na ordem certa.
Use uma fila por objeto de negócio (por pedido, por contrato). Uma fila única para tudo transforma qualquer erro numa parada geral.
10. Execuções (run job)
Para processamento recorrente, o PRC tem uma classe base de job, zcl_prc_run_job. Você herda dela, define os parâmetros e escreve a seleção. O resto (criar o run, contar sucessos e erros, fechar no final) é da classe base:
CLASS zcl_prc_travel_disc_job DEFINITION PUBLIC FINAL
INHERITING FROM zcl_prc_run_job CREATE PUBLIC.
PUBLIC SECTION.
METHODS if_apj_dt_exec_object~get_parameters REDEFINITION. " S_AGENCY e P_DISC
PROTECTED SECTION.
METHODS execute REDEFINITION.
METHODS get_application_name REDEFINITION.
ENDCLASS.
METHOD execute.
" ... lê S_AGENCY e P_DISC, seleciona as viagens ...
set_total_number( lines( lt_travel ) ).
zcl_prc_processing_api=>get_instance( )->create_processed_objects(
i_create_processed_objects = VALUE #( FOR t IN lt_travel
( runUUID = mv_current_run_uuid " liga cada objeto ao run
processedObject = t-travel_id
processedObjectUUID = t-travel_uuid
processName = 'TRAVEL_DISC_JOB'
factoryClassName = 'ZCL_PRC_TRAVEL_DISC_PROC'
payloadJson = lv_payload ) )
i_perform_commit = abap_true
i_trigger_processing = zcl_prc_processing_api=>execution_mode-direct_execution ).
ENDMETHOD.Para virar um application job de verdade, a classe precisa de um catálogo e de um template de job. Esses objetos não vieram na importação, então criei pela API cl_apj_dt_create_content e agendei com cl_apj_rt_api=>schedule_job, com a agência AG0005 e 15% de desconto. O resultado aparece no app de Runs, que mostro mais abaixo.
Quando usar: tudo que roda por agendamento. Reajuste mensal, reprocessamento noturno, carga semanal. O run responde a pergunta que todo mundo faz na segunda de manhã: "o job de ontem rodou certo?".
11. Segundo plano (bgPF)
Com bgpf_execution, o PRC entrega o processamento ao background processing framework e devolve o controle na hora. É o modo para quando quem dispara não pode esperar: uma API que precisa responder rápido, ou um botão no Fiori.
Aqui tive um problema que vale o aviso: no meu sistema o bgRFC estava parado (mais de 1.500 unidades esperando desde julho). O PRC entregou a tarefa sem erro nenhum, e ela simplesmente ficou parada em START. Nenhum alerta, nada no monitor.
Se for usar esse modo, confira antes na transação SBGRFCMON se o destino BGPF está processando. O retry job funciona como rede de segurança e pega esses objetos depois.
Os apps de monitoramento
Este é o lado que o usuário de negócio vê, e para mim é metade do valor do PRC. São três apps Fiori Elements que já vêm no pacote, cada um respondendo uma pergunta diferente:
App | Pergunta que responde | Quem usa |
|---|---|---|
Runs ( | O job de ontem rodou? Quantos deram certo? | Quem é dono do processo, suporte |
Processed Objects ( | O que aconteceu com este documento? | Usuário-chave, suporte |
Processed Messages ( | Qual erro está acontecendo mais, e em quantos objetos? | Suporte, desenvolvedor |
Runs: o placar de cada execução
Uma linha por execução do job, com o progresso, o número de erros e de sucessos e o link para o log de aplicação.
A lista de runs. O nome é o texto que dei ao job no agendamento.
Abrindo um run, o cabeçalho repete o placar e logo abaixo vêm duas listas separadas: objetos com erro e objetos com sucesso. Mais embaixo, os dados do job (ID, início, fim, log) e os parâmetros usados naquela execução.
O detalhe do run: quem falhou, quem passou, os dados do job e os parâmetros. (O "Execution Status" aparece vazio porque o PRC só traz o texto desse campo em inglês, e eu estava logado em português.)
O que mais gostei aqui foi a aba de parâmetros. Quando alguém pergunta "por que a viagem X não pegou desconto?", dá para ver na hora que o job rodou só para a agência AG0005.
Processed Objects: a história de cada documento
A lista mostra todos os objetos, de todos os processos, com filtro por processo, estado, severidade e usuário. Cada linha tem a última mensagem (verde ou vermelha), a barra de progresso das etapas, o estado, quantas vezes já tentou e quando será a próxima tentativa.
Os objetos do meu teste. O botão Resume Processing só fica ativo nos que não terminaram.
Abrindo um objeto, aparecem três blocos: o status atual (mais fila, agendamento e dados de retry em "Mostrar mais"), a lista de passos executados e a lista de todas as mensagens, de todas as tentativas.
Um objeto que deu certo: um passo por etapa, cada um com a mensagem de sucesso que escrevi no handler.
Repare que as mensagens que aparecem ali são exatamente as do get_success_message e do get_failure_message. É por isso que vale caprichar nelas: o monitor é tão bom quanto as mensagens que você escreve.
Processed Messages: o erro mais comum, de uma vez
Este é o app que eu não esperava e acabei achando o mais útil para suporte. Em vez de listar objetos, ele lista mensagens, com o processo e quantos objetos foram afetados por cada uma.
Imagine uma carga de 5 mil linhas em que 300 falharam porque um centro não estava cadastrado. Aqui isso aparece como uma linha só, "300 objetos afetados". Você cadastra o centro, clica em Resume Processing naquela mensagem e as 300 voltam a andar. O botão só fica ativo em mensagem de erro, e por baixo ele chama a retomada de cada objeto afetado.
Um detalhe dessa versão: a mensagem não tem um ID próprio, a chave dela é o texto mais as variáveis. O serviço responde certo (testei a leitura direto no OData), mas no meu teste a tela de detalhe abriu vazia para mensagens com % no texto, como "Desconto de 10.00% aplicado". Para o dia a dia não atrapalha: o que interessa nesse app é a lista e o botão de retomar.
Para ver os apps no seu sistema
Os três serviços vêm no pacote, mas os apps do launchpad (BSP) não vieram na importação pelo abapGit. Para testar, publique os service bindings e use o Preview do ADT, que foi o que fiz nos prints. Em produção você publicaria os apps no launchpad, como qualquer Fiori Elements.
O que encontrei na instalação
A instalação é pelo abapGit, e não foi direta. Fica a lista para quem for testar:
Não compila sem o pacote de demo. Duas classes de teste do núcleo usam uma constante da demo. Ou você importa a demo junto, ou ajusta essas duas linhas.
S/4HANA 2023 on-premise. O README lista o 2023 como suportado, mas duas classes usam
/UI2/CL_JSON, que não é liberada para ABAP Cloud nesse release. Troquei porXCO_CP_JSON.Objetos que o abapGit não trouxe. Objeto de log de aplicação, catálogo e template de job e os apps BSP. O log eu criei na mão; catálogo e template, pela API
cl_apj_dt_create_content.Apps sem anotação. As metadata extensions vieram só como rascunho inativo, e os apps abriam com colunas cruas. Regravar e ativar resolveu.
Erro na tela do objeto pelo app de Runs. O serviço
ZUI_PRC_RUN_O4não expõe a entidade de mensagens que a tela usa ("Unable to find annotationPath"). Incluí a entidade no service definition.Textos só em inglês. Os rótulos dos apps e alguns valores (como o status do run) não têm tradução. As suas mensagens saem no idioma de logon normalmente.
Quando usar, e quando não
Situação | Vale o PRC? |
|---|---|
Vários passos por objeto, falha parcial esperada, negócio precisa acompanhar e retomar | Sim, é o caso para o qual ele foi feito |
Carga de planilha ou de API em que cada linha pode falhar por um motivo diferente | Sim. O app de mensagens agrupa os erros e a retomada corrige em lote |
Integração que depende de sistema externo instável | Sim, pelo retry automático. Cuide da idempotência no passo que chama o externo |
Um passo só, volume pequeno, sem retomada | Não. Um application job com log resolve |
Só performance importa, sem rastreio por objeto | Não. |
Lógica dentro de um app Fiori em que o RAP controla o commit | Não. O PRC precisa ser dono do commit. Do app, no máximo, entregue o objeto com |
Conclusão
O que me ganhou no PRC foi a retomada. Corrigir 20 objetos e continuar só com eles, do passo onde pararam, sem medo de duplicar nada, é o tipo de coisa que todo projeto acaba construindo na mão, e quase sempre pela metade. Aqui isso vem pronto, junto com três apps de monitoramento que o usuário de negócio consegue ler sem chamar ninguém.
Ainda é uma beta, e a instalação mostra isso. Mas como modelo, funcionou em tudo que testei. Se tiver um processo em massa que vive dando dor de cabeça, vale pegar um caso pequeno e real e fazer o teste.
Referências
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.