mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-14 23:18:40 +00:00
fix(stock): serialize pick list allocation per item on postgres
Two simultaneous allocations for the same item can both claim the same stock on postgres: the picked-items locking read cannot see the rows another in-flight creation is inserting, while MariaDB's gap locks make the creations take turns. Advisory-gate set_item_locations per item (sorted against deadlocks) so the second allocation waits, then subtracts the first's claim. MariaDB unchanged.
This commit is contained in:
@@ -550,6 +550,15 @@ class PickList(TransactionBase):
|
||||
def set_item_locations(self, save: bool = False):
|
||||
self.validate_for_qty()
|
||||
items = self.aggregate_item_qty()
|
||||
|
||||
# Serialize concurrent allocations per item on postgres. MariaDB's gap locks on the
|
||||
# picked-items locking read below already make two simultaneous allocations take turns;
|
||||
# postgres locking reads can't see the rows another in-flight allocation is inserting, so
|
||||
# both could claim the same stock. Sorted so overlapping documents can't deadlock.
|
||||
if frappe.db.db_type == "postgres" and hasattr(frappe.db, "transaction_advisory_lock"):
|
||||
for item_code in sorted({d.item_code for d in items}):
|
||||
frappe.db.transaction_advisory_lock(("pick-allocate", item_code))
|
||||
|
||||
picked_items_details = self.get_picked_items_details(items)
|
||||
self.item_location_map = frappe._dict()
|
||||
|
||||
|
||||
@@ -29,6 +29,23 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPickList(ERPNextTestSuite):
|
||||
def test_pick_list_allocation_takes_advisory_gate(self):
|
||||
if frappe.db.db_type != "postgres":
|
||||
return
|
||||
|
||||
item = make_item(properties={"is_stock_item": 1}).name
|
||||
make_stock_entry(item=item, to_warehouse="_Test Warehouse - _TC", qty=5, basic_rate=100)
|
||||
sales_order = make_sales_order(item_code=item, warehouse="_Test Warehouse - _TC", qty=2, rate=100)
|
||||
|
||||
def held_advisory_locks():
|
||||
return frappe.db.sql(
|
||||
"SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid()"
|
||||
)[0][0]
|
||||
|
||||
before = held_advisory_locks()
|
||||
create_pick_list(sales_order.name)
|
||||
self.assertGreater(held_advisory_locks(), before)
|
||||
|
||||
def test_pick_list_picks_warehouse_for_each_item(self):
|
||||
item_code = make_item().name
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user