feat: implement transaction-level session locking in pos_order and idempotent payment creation in pos_payment
This commit is contained in:
parent
52565d3f92
commit
932db6e5f7
@ -2,4 +2,5 @@
|
|||||||
from . import pos_config
|
from . import pos_config
|
||||||
from . import res_config_settings
|
from . import res_config_settings
|
||||||
from . import pos_order
|
from . import pos_order
|
||||||
|
from . import pos_payment
|
||||||
|
|
||||||
|
|||||||
@ -106,7 +106,34 @@ class PosOrder(models.Model):
|
|||||||
Override to prevent concurrent processing of the same order uuid.
|
Override to prevent concurrent processing of the same order uuid.
|
||||||
Uses pg_advisory_xact_lock on the order's uuid to force subsequent
|
Uses pg_advisory_xact_lock on the order's uuid to force subsequent
|
||||||
duplicate requests to wait until the first request completes/commits.
|
duplicate requests to wait until the first request completes/commits.
|
||||||
|
Also, locks the session associated with the incoming orders at the database
|
||||||
|
transaction level to serialize updates affecting the same session and prevent
|
||||||
|
serialization failures (concurrent updates on pos.session).
|
||||||
"""
|
"""
|
||||||
|
# Extract unique session IDs from the orders list
|
||||||
|
session_ids = set()
|
||||||
|
for order in orders:
|
||||||
|
session_id = order.get('session_id')
|
||||||
|
if session_id:
|
||||||
|
session_ids.add(session_id)
|
||||||
|
|
||||||
|
if session_ids:
|
||||||
|
# Also find if any of these sessions are closed/closing, and if so, their potential open rescue/config sessions
|
||||||
|
sessions = self.env['pos.session'].browse(session_ids).exists()
|
||||||
|
closed_or_closing = sessions.filtered(lambda s: s.state in ('closing_control', 'closed'))
|
||||||
|
if closed_or_closing:
|
||||||
|
# Find open sessions for those configs
|
||||||
|
open_sessions = self.env['pos.session'].search([
|
||||||
|
('state', '=', 'opened'),
|
||||||
|
('config_id', 'in', closed_or_closing.mapped('config_id').ids)
|
||||||
|
])
|
||||||
|
session_ids.update(open_sessions.ids)
|
||||||
|
|
||||||
|
# De-duplicate, filter to existing, sort to prevent deadlocks, and lock using FOR UPDATE
|
||||||
|
resolved_session_ids = sorted(list(session_ids))
|
||||||
|
if resolved_session_ids:
|
||||||
|
self.env.cr.execute("SELECT id FROM pos_session WHERE id IN %s FOR UPDATE", (tuple(resolved_session_ids),))
|
||||||
|
|
||||||
# Sort orders by uuid to prevent potential deadlocks when locking multiple orders
|
# Sort orders by uuid to prevent potential deadlocks when locking multiple orders
|
||||||
sorted_orders = sorted(orders, key=lambda x: x.get('uuid') or '')
|
sorted_orders = sorted(orders, key=lambda x: x.get('uuid') or '')
|
||||||
for order in sorted_orders:
|
for order in sorted_orders:
|
||||||
|
|||||||
53
models/pos_payment.py
Normal file
53
models/pos_payment.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, api
|
||||||
|
|
||||||
|
class PosPayment(models.Model):
|
||||||
|
_inherit = 'pos.payment'
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
# 1. Extract and sort unique uuids to prevent deadlocks
|
||||||
|
uuids = []
|
||||||
|
for vals in vals_list:
|
||||||
|
if isinstance(vals, dict) and vals.get('uuid'):
|
||||||
|
uuids.append(vals['uuid'])
|
||||||
|
|
||||||
|
sorted_uuids = sorted(list(set(uuids)))
|
||||||
|
|
||||||
|
# 2. Acquire transaction-level PostgreSQL advisory locks on the sorted hashes of uuids
|
||||||
|
for uuid in sorted_uuids:
|
||||||
|
self.env.cr.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (uuid,))
|
||||||
|
|
||||||
|
# 3. Check which payments already exist in the database
|
||||||
|
existing_payments_map = {}
|
||||||
|
if sorted_uuids:
|
||||||
|
existing_payments = self.search([('uuid', 'in', sorted_uuids)])
|
||||||
|
for payment in existing_payments:
|
||||||
|
existing_payments_map[payment.uuid] = payment
|
||||||
|
|
||||||
|
# 4. Prepare values to create (only those that don't already exist)
|
||||||
|
final_payments_list = [None] * len(vals_list)
|
||||||
|
vals_to_create = []
|
||||||
|
create_indices = []
|
||||||
|
|
||||||
|
for i, vals in enumerate(vals_list):
|
||||||
|
uuid = vals.get('uuid') if isinstance(vals, dict) else None
|
||||||
|
if uuid and uuid in existing_payments_map:
|
||||||
|
final_payments_list[i] = existing_payments_map[uuid]
|
||||||
|
else:
|
||||||
|
vals_to_create.append(vals)
|
||||||
|
create_indices.append(i)
|
||||||
|
|
||||||
|
# 5. Create new payments
|
||||||
|
if vals_to_create:
|
||||||
|
created_payments = super(PosPayment, self).create(vals_to_create)
|
||||||
|
for idx, payment in zip(create_indices, created_payments):
|
||||||
|
final_payments_list[idx] = payment
|
||||||
|
|
||||||
|
# 6. Reconstruct and return the final combined recordset
|
||||||
|
final_payments = self.env['pos.payment']
|
||||||
|
for payment in final_payments_list:
|
||||||
|
if payment:
|
||||||
|
final_payments += payment
|
||||||
|
|
||||||
|
return final_payments
|
||||||
Loading…
Reference in New Issue
Block a user