refactor: move KDS report logic into background thread and optimize state lookup

This commit is contained in:
Suherdy Yacob 2026-05-07 13:58:24 +07:00
parent 88571bc418
commit 243ebdb4d5

View File

@ -6,15 +6,16 @@ from odoo import models, fields, api
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
def _threaded_report_update(registry, uid, context, pdis_state_id, is_reset, is_completed): def _threaded_report_update(registry, uid, context, pdis_state_id):
""" """
Background worker with Retry Logic and Row Locking. Final Optimized Background Worker.
Moves all logic (including stage lookups) to the background.
""" """
max_retries = 5 max_retries = 3
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
# Staggered start to avoid simultaneous collisions # Staggered start
time.sleep(random.uniform(0.05, 0.2)) time.sleep(random.uniform(0.1, 0.3))
with registry.cursor() as new_cr: with registry.cursor() as new_cr:
new_env = api.Environment(new_cr, uid, context) new_env = api.Environment(new_cr, uid, context)
@ -27,22 +28,48 @@ def _threaded_report_update(registry, uid, context, pdis_state_id, is_reset, is_
return return
order = order_line.order_id order = order_line.order_id
order_name = order.name or order.pos_reference or "Unknown Order" order_name = order.name or order.pos_reference or "Order"
stage = pdis_state.stage_id stage = pdis_state.stage_id
display_id = stage.prep_display_id.id if not stage:
return
display = stage.prep_display_id
display_id = display.id
order_id = order.id order_id = order.id
# Fetch stage info inside the thread
stage_ids = display.stage_ids.ids
if not stage_ids:
return
first_stage_id = stage_ids[0]
last_stage_id = stage_ids[-1]
second_last_stage_id = stage_ids[-2] if len(stage_ids) > 1 else False
is_completed = False
if len(stage_ids) > 1:
if stage.id == last_stage_id:
is_completed = True
elif second_last_stage_id and stage.id == second_last_stage_id and not pdis_state.todo:
is_completed = True
else:
if not pdis_state.todo:
is_completed = True
is_reset = (stage.id == first_stage_id and pdis_state.todo)
if not (is_completed or is_reset):
return
KdsLineReport = new_env['pos.kds.report.line'].sudo() KdsLineReport = new_env['pos.kds.report.line'].sudo()
KdsOrderReport = new_env['pos.kds.report.order'].sudo() KdsOrderReport = new_env['pos.kds.report.order'].sudo()
# --- ROW LOCKING --- # Row Locking
# Attempt to lock the existing order report if it exists
new_cr.execute(""" new_cr.execute("""
SELECT id FROM pos_kds_report_order SELECT id FROM pos_kds_report_order
WHERE pos_order_id = %s AND prep_display_id = %s WHERE pos_order_id = %s AND prep_display_id = %s
FOR UPDATE FOR UPDATE
""", (order_id, display_id)) """, (order_id, display_id))
# -------------------
if is_reset: if is_reset:
line_report = KdsLineReport.search([ line_report = KdsLineReport.search([
@ -68,7 +95,6 @@ def _threaded_report_update(registry, uid, context, pdis_state_id, is_reset, is_
else: else:
order_report.write({'completion_time': max_comp_time}) order_report.write({'completion_time': max_comp_time})
new_cr.commit() new_cr.commit()
_logger.info("POS_KDS_PERF: Reset report SUCCESS for %s", order_name)
return return
# If completed # If completed
@ -97,7 +123,6 @@ def _threaded_report_update(registry, uid, context, pdis_state_id, is_reset, is_
else: else:
KdsLineReport.create(vals) KdsLineReport.create(vals)
# Update order-level report
order_report = KdsOrderReport.search([ order_report = KdsOrderReport.search([
('pos_order_id', '=', order_id), ('pos_order_id', '=', order_id),
('prep_display_id', '=', display_id) ('prep_display_id', '=', display_id)
@ -122,74 +147,36 @@ def _threaded_report_update(registry, uid, context, pdis_state_id, is_reset, is_
new_cr.commit() new_cr.commit()
_logger.info("POS_KDS_PERF: Background report update SUCCESS for order %s", order_name) _logger.info("POS_KDS_PERF: Background report update SUCCESS for order %s", order_name)
return # Exit loop on success return
except Exception as e: except Exception as e:
if "could not serialize access" in str(e) or "concurrent update" in str(e): if "could not serialize access" in str(e) or "concurrent update" in str(e):
_logger.warning("POS_KDS_PERF: Concurrent update detected, retrying (%s/%s)...", attempt + 1, max_retries)
continue continue
else: else:
_logger.error("POS_KDS_PERF: Background KDS reporting failed: %s", e) _logger.error("POS_KDS_PERF: Background KDS reporting failed: %s", e)
break break
else: else:
_logger.error("POS_KDS_PERF: Failed to update report after %s retries", max_retries) _logger.error("POS_KDS_PERF: Failed to update report after retries")
class PosPreparationState(models.Model): class PosPreparationState(models.Model):
_inherit = 'pos.prep.state' _inherit = 'pos.prep.state'
def _update_kds_report(self, pdis_state): def _update_kds_report_async(self, pdis_state):
if not pdis_state.prep_line_id or not pdis_state.prep_line_id.pos_order_line_id: """Ultra-fast hook that only starts the thread."""
return if not pdis_state.prep_line_id:
stage = pdis_state.stage_id
if not stage:
return
if not hasattr(self.env, '_kds_display_cache'):
self.env._kds_display_cache = {}
display_id = stage.prep_display_id.id
if display_id not in self.env._kds_display_cache:
prep_display = stage.prep_display_id
stage_ids = prep_display.stage_ids.ids
self.env._kds_display_cache[display_id] = {
'num_stages': len(stage_ids),
'first_stage_id': stage_ids[0] if stage_ids else False,
'last_stage_id': stage_ids[-1] if stage_ids else False,
'second_last_stage_id': stage_ids[-2] if len(stage_ids) > 1 else False,
}
display_info = self.env._kds_display_cache[display_id]
num_stages = display_info['num_stages']
is_completed = False
if num_stages > 1:
if stage.id == display_info['last_stage_id']:
is_completed = True
elif display_info['second_last_stage_id'] and stage.id == display_info['second_last_stage_id'] and not pdis_state.todo:
is_completed = True
elif num_stages == 1:
if not pdis_state.todo:
is_completed = True
is_reset = False
if stage.id == display_info['first_stage_id'] and pdis_state.todo:
is_reset = True
if not (is_completed or is_reset):
return return
thread = threading.Thread( thread = threading.Thread(
target=_threaded_report_update, target=_threaded_report_update,
args=(self.env.registry, self.env.uid, self.env.context, pdis_state.id, is_reset, is_completed) args=(self.env.registry, self.env.uid, self.env.context, pdis_state.id)
) )
thread.daemon = True thread.daemon = True
thread.start() thread.start()
def _record_status_change_prep_time(self, pdis_state): def _record_status_change_prep_time(self, pdis_state):
super()._record_status_change_prep_time(pdis_state) super()._record_status_change_prep_time(pdis_state)
self._update_kds_report(pdis_state) self._update_kds_report_async(pdis_state)
def _record_stage_change_prep_time(self, pdis_state, old_last_stage_change, prep_order_completion_time): def _record_stage_change_prep_time(self, pdis_state, old_last_stage_change, prep_order_completion_time):
super()._record_stage_change_prep_time(pdis_state, old_last_stage_change, prep_order_completion_time) super()._record_stage_change_prep_time(pdis_state, old_last_stage_change, prep_order_completion_time)
self._update_kds_report(pdis_state) self._update_kds_report_async(pdis_state)