前言:
记录这篇文章起因是调查生产订单报工问题引申出来的一个问题,后来再次调查后了解了其中缘由,大概记录以下,如有不对,欢迎指正。问题原贴如下:
当时疑惑的点是为什么更新进程明明是被COMMIT WORK AND WAIT触发的,但为什么仍然会有锁定问题,经过后来再次debug后发现了问题所在,在更新进程中,货物移动的处理是放在一个background task异步tRFC去处理的,是一个立即开始独立运行的进程,主程序中的同步提交只会等更新进程执行完,并不会等这个异步tRFC执行完毕,因为是不同的进程,而这个异步tRFC中在货物移动时会再次对订单上锁,所以当这个异步tRFC没处理完时,立即进行下一次报工就有可能出现锁定问题。
针对于BAPI异步提交问题可以看这篇:
tRFC流程:
问题说明:
针对于报工BAPI,大致的执行流程如上图所示,
- 将所有检查通过的数据库提交任务绑定至同一个LUW;
- 由COMMIT WORK AND WAIT触发同步提交;
- Debug模式可以在SM13中观测到所有注册的更新模块,并且主程序中的锁会被带到更新进程中去,系统将按照注册顺序依次执行,通常这些模块中只包含数据库增删改操作,执行完所有模块后,系统统一提交,并释放所有锁,如果失败,则会统一回滚,并会收到一个dump快件,也会释放所有锁;
- 在标准设计中,货物移动的处理是预先存在AFFW表中,由最后一个更新模块(CO_RU_VB_CONFIRMATION_POST)处理,在这个模块中,将货物移动的处理放在了BACKGROUND TASK中,查看F1说明文档可以看到这是一种已过时的用于绑定事务性RFC的技术,通过该方式注册的函数会立即开始,并且独立异步执行,通过进一步debug,发现货物移动的处理涉及生产订单处理时会再次对订单上锁;
- 程序结束,因为IN BACKGROUND TASK注册的函数是独立的进程,所以如果该进程还没有执行完毕,在下一次针对该订单进行报工的时候就会出现锁定错误;
- 本次执行结束后,第二次针对同一订单处理时,如果异步RFC执行完毕,则第二次将会成功处理(报工成功,货物移动成功);
- 如果报工时异步RFC尚未处理完毕,第二次处理则会因为锁定直接报错(报工失败,货物移动失败);
- 如果第二次报工时上一次的异步RFC尚未开始处理,但处理第二次货物移动时刚好上一次的异步RFC处理已经开始,则可能会出现报工成功,货物移动因为锁定进入COGI(报工成功,货物移动失败);
这也是为什么调用了BAPI之后COMMIT WORK AND WAIT或者调用BAPI前指定了SET UPDATE LOCAL TASK没有生效的原因。
标准BAPI Debug确认过程:
设置好断点,并打开系统调试和更新调试。
同步提交后进入更新进程:
SM13可以看到注册了以下的更新函数模块,其中对自动货物移动的处理在最后一个函数中:
继续调试:
针对最后一个函数重点关照:
标准逻辑就是在此处触发异步的TRFC去处理货物移动的逻辑,此时打开TRFC调试,则该函数模块不会立即执行,会使其注册到SM58,方便进行后续手工处理调试,如果不勾选则会立刻执行,不会等待手工处理。
F8执行之后,可以看到此时已返回主程序,针对订单的锁也已经释放,但处理货物移动的函数被我们注册到了SM58,等待处理,所以SM58进行下一步调试,看看里面做了些什么。
SM58:
在下面这段处理中将会对订单再次上锁:
只有等该函数执行结束后才会释放锁,所以如果此时再次对该订单发起报工时就会出现锁定问题。
以上差不就是整个关键的debug过程,为了再次验证这个猜想,我重新写了一份测试代码进行验证,以下代码可以复现还原这个问题。
测试代码:
*&---------------------------------------------------------------------*
*& Report ZUPDATE_TEST
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
REPORT zupdate_test.
DATA:
lt_test TYPE STANDARD TABLE OF ztest_trfc,
ls_test TYPE ztest_trfc.
DO 1 TIMES.
TRY .
ls_test-uuid = cl_uuid_factory=>create_system_uuid( )->create_uuid_x16( ).
CATCH cx_uuid_error.
ENDTRY.
ls_test-uname = sy-uname.
CALL FUNCTION 'ZNOMAL_TEST'
EXPORTING
i_test = ls_test.
COMMIT WORK AND WAIT.
IF sy-subrc = 0.
WRITE:'Update success:',ls_test-uuid.
ELSE.
WRITE:'Update failed:',ls_test-uuid.
ENDIF.
ENDDO.
FUNCTION znomal_test.
*"----------------------------------------------------------------------
*"*"本地接口:
*" IMPORTING
*" VALUE(I_TEST) TYPE ZTEST_TRFC
*"----------------------------------------------------------------------
CALL FUNCTION 'ENQUEUE_E_TABLE'
EXPORTING
* MODE_RSTABLE = 'E'
tabname = 'ZTEST_TRFC'
* VARKEY =
* X_TABNAME = ' '
* X_VARKEY = ' '
* _SCOPE = '2'
* _WAIT = ' '
* _COLLECT = ' '
EXCEPTIONS
foreign_lock = 1
system_failure = 2
OTHERS = 3.
IF sy-subrc <> 0.
MESSAGE 'Lock Failed!' TYPE 'E'.
ENDIF.
CALL FUNCTION 'ZCALL_UPDATE_TASK' IN UPDATE TASK
EXPORTING
i_test = i_test.
ENDFUNCTION.
FUNCTION zcall_update_task.
*"----------------------------------------------------------------------
*"*"更新函数模块:
*"
*"*"本地接口:
*" IMPORTING
*" VALUE(I_TEST) TYPE ZTEST_TRFC
*"----------------------------------------------------------------------
CALL FUNCTION 'ZBACKGROUND_TASK_TEST' IN BACKGROUND TASK
EXPORTING
i_test = i_test.
ENDFUNCTION.
FUNCTION zbackground_task_test.
*"----------------------------------------------------------------------
*"*"本地接口:
*" IMPORTING
*" VALUE(I_TEST) TYPE ZTEST_TRFC
*"----------------------------------------------------------------------
SET UPDATE TASK LOCAL.
DO.
SELECT SINGLE * INTO @DATA(ls_lock1) FROM z13065_lock WHERE upd_id = '13065'.
IF ls_lock1-zlock = '1'.
EXIT.
ENDIF.
ENDDO.
CALL FUNCTION 'ENQUEUE_E_TABLE'
EXPORTING
* MODE_RSTABLE = 'E'
tabname = 'ZTEST_TRFC'
* VARKEY =
* X_TABNAME = ' '
* X_VARKEY = ' '
* _SCOPE = '2'
* _WAIT = ' '
* _COLLECT = ' '
EXCEPTIONS
foreign_lock = 1
system_failure = 2
OTHERS = 3.
IF sy-subrc <> 0.
MESSAGE 'Lock Failed!' TYPE 'E'.
ENDIF.
DO.
SELECT SINGLE * INTO @DATA(ls_lock2) FROM z13065_lock WHERE upd_id = '13065'.
IF ls_lock2-zlock = '2'.
EXIT.
ENDIF.
ENDDO.
* CALL FUNCTION 'ZUPDATE_TASK_TEST' IN UPDATE TASK
* EXPORTING
* is_test = i_test.
MODIFY ztest_trfc FROM i_test.
COMMIT WORK.
ENDFUNCTION.
通过LOCK表中的标识字段可以控制进程执行位置,测试过程就不贴图了,感兴趣的可以自己debug看看,根据观察SM12,SM50,SM13,SM58等标准事务代码中的信息就可以理解整个执行过程。
总结:
大多数标准BAPI的更新都是放在更新进程(CALL FUNCTION XXX IN UPDATE TASK)中去统一提交数据库更新的,可以根据COMMIT WORK是否添加AND WAIT附加项来决定是否同步更新,而CALL FUNCTION XXX IN BACKGROUND TASK则以已单独进程进行异步更新的,独立于对话框程序,属于两种互相独立的更新进程。