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>