This is an automated email from the ASF dual-hosted git repository.

diveshdut pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new b81233fc6c OFBIZ-13461 : Avoid async RequirementStatus FK race after 
createRequirement (#1480)
b81233fc6c is described below

commit b81233fc6c5e09c14768e3b9b64ea1f26728ec04
Author: Divesh Dutta <[email protected]>
AuthorDate: Fri Jul 24 20:36:45 2026 +0530

    OFBIZ-13461 : Avoid async RequirementStatus FK race after createRequirement 
(#1480)
    
    ## Jira ticket:
    
    https://issues.apache.org/jira/browse/OFBIZ-13461
    
    ## Summary
    
    This fixes a timing-dependent foreign-key failure during requirement
    status creation.
    
    When `createRequirement` runs inside a larger transaction, its async
    SECA can invoke `createRequirementStatus` before the enclosing
    transaction has fully committed. In that window, the new `Requirement`
    row is not yet visible, and the `RequirementStatus` insert can fail
    with:
    
    `REQ_STTS_REQ: OFBIZ.REQUIREMENT_STATUS FOREIGN KEY(REQUIREMENT_ID)
    REFERENCES OFBIZ.REQUIREMENT(REQUIREMENT_ID)`
    
    This was observed from the Manufacturing `Run MRP` flow, but the
    underlying bug is in the order-side requirement status SECA behavior.
    
    ## Root Cause
    
    The existing `createRequirement` SECA used `event="commit"` with
    `mode="async"`.
    
    That is not a safe boundary when `createRequirement` participates in an
    outer transaction. The async follow-up may run before the outer
    transaction is committed and globally visible.
    
    This is a race condition rather than a deterministic logic error.
    
    ## Changes
    
    - changed the `createRequirement` status SECA to
    `global-commit-post-run`
    - changed the `updateRequirement` status SECA to `global-commit`
    - added a focused order-side regression test for the outer-transaction
    scenario
    - registered the regression test in the order test suite
    
    ## Why `global-commit-post-run`
    
    `createRequirementStatus` needs the generated `requirementId`.
    
    `global-commit-post-run` ensures:
    - the enclosing transaction has committed
    - the generated service output is still available to the follow-up async
    action
    
    ## Tests
    
    Added a focused regression test for the outer-transaction timing case:
    
    -
    
`RequirementStatusEcaTests.testCreateRequirementStatusAfterOuterTransactionCommit`
    
    This verifies that:
    
    - `createRequirement` can run inside an outer transaction
    - `RequirementStatus` is not created before commit
    - `RequirementStatus` is created successfully after commit
---
 applications/order/servicedef/secas.xml            |   4 +-
 .../order/test/RequirementStatusEcaTests.groovy    | 116 +++++++++++++++++++++
 applications/order/testdef/OrderTest.xml           |   3 +
 3 files changed, 121 insertions(+), 2 deletions(-)

diff --git a/applications/order/servicedef/secas.xml 
b/applications/order/servicedef/secas.xml
index 06d5a22e0b..29ee400cb2 100644
--- a/applications/order/servicedef/secas.xml
+++ b/applications/order/servicedef/secas.xml
@@ -349,11 +349,11 @@ under the License.
         <condition field-name="custRequestItemSeqId" operator="is-not-empty"/>
         <action service="createRequirementCustRequest" mode="sync"/>
     </eca>
-    <eca service="createRequirement" event="commit" run-on-error="false">
+    <eca service="createRequirement" event="global-commit-post-run" 
run-on-error="false">
         <condition field-name="statusId" operator="is-not-empty" />
         <action service="createRequirementStatus" mode="async"/>
     </eca>
-    <eca service="updateRequirement" event="commit" run-on-error="false">
+    <eca service="updateRequirement" event="global-commit" 
run-on-error="false">
         <condition-field field-name="statusId" operator="not-equals" 
to-field-name="oldStatusId"/>
         <action service="createRequirementStatus" mode="async"/>
     </eca>
diff --git 
a/applications/order/src/test/groovy/org/apache/ofbiz/order/order/test/RequirementStatusEcaTests.groovy
 
b/applications/order/src/test/groovy/org/apache/ofbiz/order/order/test/RequirementStatusEcaTests.groovy
new file mode 100644
index 0000000000..8b0f1dd8eb
--- /dev/null
+++ 
b/applications/order/src/test/groovy/org/apache/ofbiz/order/order/test/RequirementStatusEcaTests.groovy
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ofbiz.order.order.test
+
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.entity.transaction.TransactionUtil
+import org.apache.ofbiz.service.ServiceUtil
+import org.apache.ofbiz.service.testtools.OFBizTestCase
+
+class RequirementStatusEcaTests extends OFBizTestCase {
+
+    RequirementStatusEcaTests(String name) {
+        super(name)
+    }
+
+    void testCreateRequirementStatusAfterOuterTransactionCommit() {
+        ensureRequirementReferenceData()
+        GenericValue userLogin = ensureUserLogin('mrpRequirementStatusTest')
+        assert userLogin
+
+        boolean beganTransaction = false
+        String requirementId = null
+
+        try {
+            beganTransaction = TransactionUtil.begin()
+            Map serviceCtx = [
+                requirementTypeId: 'CUSTOMER_REQUIREMENT',
+                statusId: 'REQ_PROPOSED',
+                userLogin: userLogin
+            ]
+            Map serviceResult = dispatcher.runSync('createRequirement', 
serviceCtx)
+            assert ServiceUtil.isSuccess(serviceResult)
+
+            requirementId = serviceResult.requirementId
+            assert requirementId
+            assert from('Requirement').where('requirementId', 
requirementId).queryCount() == 1
+
+            sleep(500L)
+            assert from('RequirementStatus').where('requirementId', 
requirementId, 'statusId', 'REQ_PROPOSED').queryCount() == 0
+
+            TransactionUtil.commit(beganTransaction)
+            beganTransaction = false
+        } finally {
+            if (beganTransaction) {
+                TransactionUtil.rollback(beganTransaction, 'Rolling back 
unfinished requirement status ECA test transaction', null)
+            }
+        }
+
+        assert waitForRequirementStatus(requirementId, 'REQ_PROPOSED')
+    }
+
+    private boolean waitForRequirementStatus(String requirementId, String 
statusId) {
+        for (int attempt = 0; attempt < 20; attempt++) {
+            if (from('RequirementStatus').where('requirementId', 
requirementId, 'statusId', statusId).queryCount() == 1) {
+                return true
+            }
+            sleep(100L)
+        }
+        return false
+    }
+
+    private GenericValue ensureUserLogin(String userLoginId) {
+        GenericValue userLogin = from('UserLogin').where('userLoginId', 
userLoginId).queryOne()
+        if (userLogin == null) {
+            userLogin = delegator.makeValue('UserLogin', [
+                userLoginId: userLoginId,
+                userFullName: 'MRP Requirement Status Test User',
+                enabled: 'Y'
+            ])
+            delegator.create(userLogin)
+        }
+        return userLogin
+    }
+
+    private void ensureRequirementReferenceData() {
+        if (from('RequirementType').where('requirementTypeId', 
'CUSTOMER_REQUIREMENT').queryOne() == null) {
+            delegator.create(delegator.makeValue('RequirementType', [
+                requirementTypeId: 'CUSTOMER_REQUIREMENT',
+                hasTable: 'N',
+                description: 'Customer Requirement'
+            ]))
+        }
+        if (from('StatusType').where('statusTypeId', 
'REQUIREMENT_STATUS').queryOne() == null) {
+            delegator.create(delegator.makeValue('StatusType', [
+                statusTypeId: 'REQUIREMENT_STATUS',
+                description: 'Requirement Status'
+            ]))
+        }
+        if (from('StatusItem').where('statusId', 'REQ_PROPOSED').queryOne() == 
null) {
+            delegator.create(delegator.makeValue('StatusItem', [
+                statusId: 'REQ_PROPOSED',
+                statusTypeId: 'REQUIREMENT_STATUS',
+                statusCode: 'PROPOSED',
+                sequenceId: '01',
+                description: 'Proposed'
+            ]))
+        }
+    }
+
+}
diff --git a/applications/order/testdef/OrderTest.xml 
b/applications/order/testdef/OrderTest.xml
index b4aaf25f67..683aaba1af 100644
--- a/applications/order/testdef/OrderTest.xml
+++ b/applications/order/testdef/OrderTest.xml
@@ -42,4 +42,7 @@ under the License.
     <test-case case-name="order-requirement-tests">
         <junit-test-suite 
class-name="org.apache.ofbiz.order.order.test.OrderRequirementTests"/>
     </test-case>
+    <test-case case-name="requirement-status-eca-tests">
+        <junit-test-suite 
class-name="org.apache.ofbiz.order.order.test.RequirementStatusEcaTests"/>
+    </test-case>
 </test-suite>

Reply via email to