From eb12af3b9afce27ed915dd1a4b329d737925a644 Mon Sep 17 00:00:00 2001 From: allebonvi Date: Sat, 4 Jul 2026 18:26:20 +0200 Subject: [PATCH] Ottimizza barcode e disattiva profiler query --- async_msssql_query.py | 210 ++++++++++++++++++++++++++++++++++++++++-- barcode_client.py | 10 ++ barcode_repository.py | 44 ++++++--- barcode_service.py | 59 +++++++++++- db_config.py | 5 + version_info.py | 8 +- 6 files changed, 310 insertions(+), 26 deletions(-) diff --git a/async_msssql_query.py b/async_msssql_query.py index 626d3da..ba505a1 100644 --- a/async_msssql_query.py +++ b/async_msssql_query.py @@ -10,9 +10,13 @@ cross-loop errors. from __future__ import annotations import asyncio +import inspect +import json import logging +import os import time import urllib.parse +from pathlib import Path from typing import Any, Dict, Optional from sqlalchemy import text @@ -22,6 +26,59 @@ from version_info import module_version __version__ = module_version(__name__) +QUERY_PROFILER_LOG = Path(__file__).with_name("warehouse_query_profiler.log") +QUERY_PROFILER_CONFIG_PATH = Path(__file__).with_name("db_connection.json") + + +def _load_query_profiler_config() -> dict[str, Any]: + """Read profiler settings from db_connection.json, with env overrides.""" + + config: dict[str, Any] = { + "enabled": False, + "slow_ms": 0, + "sql_limit": 4000, + } + try: + data = json.loads(QUERY_PROFILER_CONFIG_PATH.read_text(encoding="utf-8")) + section = data.get("query_profiler") if isinstance(data, dict) else None + if isinstance(section, dict): + config.update(section) + except Exception: + pass + if "WAREHOUSE_QUERY_PROFILER" in os.environ: + config["enabled"] = os.environ.get("WAREHOUSE_QUERY_PROFILER", "1").strip().lower() not in { + "0", + "false", + "no", + "off", + } + if "WAREHOUSE_QUERY_PROFILER_SLOW_MS" in os.environ: + config["slow_ms"] = os.environ.get("WAREHOUSE_QUERY_PROFILER_SLOW_MS", "0") + if "WAREHOUSE_QUERY_PROFILER_SQL_LIMIT" in os.environ: + config["sql_limit"] = os.environ.get("WAREHOUSE_QUERY_PROFILER_SQL_LIMIT", "4000") + return config + + +def _query_profiler_enabled() -> bool: + value = _load_query_profiler_config().get("enabled", False) + if isinstance(value, str): + return value.strip().lower() not in {"0", "false", "no", "off"} + return bool(value) + + +def _query_profiler_slow_ms() -> float: + try: + return float(_load_query_profiler_config().get("slow_ms", 0) or 0) + except Exception: + return 0.0 + + +def _query_profiler_sql_limit() -> int: + try: + return int(_load_query_profiler_config().get("sql_limit", 4000) or 4000) + except Exception: + return 4000 + try: import pyodbc @@ -76,6 +133,83 @@ def make_mssql_dsn( return f"mssql+aioodbc:///?odbc_connect={urllib.parse.quote_plus(odbc)}" +def _compact_sql(sql: str, *, limit: int | None = None) -> str: + """Collapse SQL whitespace so profiler entries stay readable.""" + + if limit is None: + limit = _query_profiler_sql_limit() + text_value = " ".join(str(sql or "").split()) + if limit > 0 and len(text_value) > limit: + return text_value[:limit] + "..." + return text_value + + +def _profile_params(params: Optional[Dict[str, Any]]) -> str: + """Serialize SQL parameters for diagnostics.""" + + if not params: + return "{}" + try: + return _dumps(params) + except Exception: + return repr(params) + + +def _query_caller() -> str: + """Return the first external Python frame that triggered the DB call.""" + + current_file = Path(__file__).resolve() + for frame in inspect.stack(context=0)[2:]: + try: + frame_file = Path(frame.filename).resolve() + except Exception: + continue + if frame_file == current_file: + continue + return f"{frame_file.name}:{frame.lineno}:{frame.function}" + return "unknown" + + +def _append_query_profile( + *, + method: str, + elapsed_ms: float, + rows: int | None, + rowcount: int | None, + commit: bool, + ok: bool, + sql: str, + params: Optional[Dict[str, Any]], + caller: str, + error: str = "", +) -> None: + """Append one query timing line to the local profiler log.""" + + if not _query_profiler_enabled(): + return + slow_ms = _query_profiler_slow_ms() + if ok and slow_ms > 0 and elapsed_ms < slow_ms: + return + try: + timestamp = time.strftime("%Y-%m-%d %H:%M:%S") + row_info = f"rows={rows}" if rows is not None else f"rowcount={rowcount}" + lines = [ + ( + f"{timestamp} | {elapsed_ms:.3f} ms | {method} | " + f"{row_info} | commit={int(commit)} | ok={int(ok)} | caller={caller}" + ), + f"PARAMS: {_profile_params(params)}", + f"SQL: {_compact_sql(sql)}", + ] + if error: + lines.append(f"ERROR: {error}") + with QUERY_PROFILER_LOG.open("a", encoding="utf-8") as handle: + handle.write("\n".join(lines) + "\n---\n") + except Exception: + # Profiling must never interfere with warehouse operations. + pass + + class AsyncMSSQLClient: """Thin async query client for SQL Server. @@ -152,23 +286,83 @@ class AsyncMSSQLClient: """ await self._ensure_engine() t0 = time.perf_counter() - async with (self._engine.begin() if commit else self._engine.connect()) as conn: - res = await conn.execute(text(sql), params or {}) - rows = res.fetchall() - cols = list(res.keys()) + caller = _query_caller() + try: + async with (self._engine.begin() if commit else self._engine.connect()) as conn: + res = await conn.execute(text(sql), params or {}) + rows = res.fetchall() + cols = list(res.keys()) + except Exception as exc: + elapsed_ms = round((time.perf_counter() - t0) * 1000, 3) + _append_query_profile( + method="query_json", + elapsed_ms=elapsed_ms, + rows=None, + rowcount=None, + commit=commit, + ok=False, + sql=sql, + params=params, + caller=caller, + error=repr(exc), + ) + raise if as_dict_rows: rows_out = [dict(zip(cols, row)) for row in rows] else: rows_out = [list(row) for row in rows] + elapsed_ms = round((time.perf_counter() - t0) * 1000, 3) + _append_query_profile( + method="query_json", + elapsed_ms=elapsed_ms, + rows=len(rows_out), + rowcount=None, + commit=commit, + ok=True, + sql=sql, + params=params, + caller=caller, + ) return { "columns": cols, "rows": rows_out, - "elapsed_ms": round((time.perf_counter() - t0) * 1000, 3), + "elapsed_ms": elapsed_ms, } async def exec(self, sql: str, params: Optional[Dict[str, Any]] = None, *, commit: bool = False) -> int: """Execute a DML statement and return its row count.""" await self._ensure_engine() - async with (self._engine.begin() if commit else self._engine.connect()) as conn: - res = await conn.execute(text(sql), params or {}) - return res.rowcount or 0 + t0 = time.perf_counter() + caller = _query_caller() + try: + async with (self._engine.begin() if commit else self._engine.connect()) as conn: + res = await conn.execute(text(sql), params or {}) + rowcount = res.rowcount or 0 + except Exception as exc: + elapsed_ms = round((time.perf_counter() - t0) * 1000, 3) + _append_query_profile( + method="exec", + elapsed_ms=elapsed_ms, + rows=None, + rowcount=None, + commit=commit, + ok=False, + sql=sql, + params=params, + caller=caller, + error=repr(exc), + ) + raise + elapsed_ms = round((time.perf_counter() - t0) * 1000, 3) + _append_query_profile( + method="exec", + elapsed_ms=elapsed_ms, + rows=None, + rowcount=rowcount, + commit=commit, + ok=True, + sql=sql, + params=params, + caller=caller, + ) + return rowcount diff --git a/barcode_client.py b/barcode_client.py index 27b1b0a..931aed5 100644 --- a/barcode_client.py +++ b/barcode_client.py @@ -68,6 +68,7 @@ class BarcodeClientApp: self._auto_advance_id: str | None = None self._pallet_auto_focus_id: str | None = None self._paused_queue_id: int | None = None + self._paused_priority_state: BarcodeViewState | None = None self._pallet_user_input_serial = 0 self._priority_loaded_input_serial = 0 self._priority_expected_pallet = "" @@ -531,6 +532,12 @@ class BarcodeClientApp: def _on_escape_key(self, _event=None) -> str: if self._is_priority_pause() and self._paused_queue_id is not None: + if self._paused_priority_state is not None: + state = self.service.resume_priority_state(self._paused_priority_state) + self._paused_priority_state = None + self._paused_queue_id = None + self._apply_state(state) + return "break" self._start_queue(self._paused_queue_id, allow_during_pause=True) return "break" @@ -542,15 +549,18 @@ class BarcodeClientApp: mode = getattr(self.service.state, "mode", "") if mode == "priority_high": self._paused_queue_id = 1 + self._paused_priority_state = self.service.state self._apply_state(self.service.begin_priority_pause(1)) return if mode == "priority_low": self._paused_queue_id = 0 + self._paused_priority_state = self.service.state self._apply_state(self.service.begin_priority_pause(0)) return resumable_queue = self._resumable_queue_from_state(self.service.state) if resumable_queue is not None: self._paused_queue_id = resumable_queue + self._paused_priority_state = None self._apply_state(self.service.begin_priority_pause(resumable_queue)) return self._submit() diff --git a/barcode_repository.py b/barcode_repository.py index f022a4d..94937d7 100644 --- a/barcode_repository.py +++ b/barcode_repository.py @@ -107,15 +107,30 @@ ORDER BY g.BarcodePallet; SQL_CLOSED_PICKING_BY_PALLET = """ SELECT TOP (1) - BarcodePallet, - NumeroPallet, - IDMagazzino, - IDArea, - IDCella, - IDStato -FROM dbo.XMag_GiacenzaPalletPlistChiuse -WHERE BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS -ORDER BY IDCella DESC; + g.BarcodePallet, + g.NumeroPallet, + g.IDMagazzino, + g.IDArea, + g.IDCella, + g.IDStato +FROM dbo.XMag_GiacenzaPallet AS g +WHERE g.IDCella <> 9999 + AND g.BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS + AND EXISTS ( + SELECT 1 + FROM SAMA1.dbo.LOTSER AS ls + INNER JOIN SAMA1.dbo.LOTTIBF AS lb + ON lb.IDLOTSER = ls.ID + INNER JOIN SAMA1.dbo.FATRIG AS fr + ON lb.IDFATRIG = fr.ID + INNER JOIN SAMA1.dbo.BAMTES AS bt + ON bt.ID = fr.IDBAM + WHERE bt.ANNDOC >= 2011 + AND bt.STATO = 'D' + AND LEFT(ls.NUMSER, 6) COLLATE Latin1_General_CI_AS = + :pallet COLLATE Latin1_General_CI_AS + ) +ORDER BY g.IDCella DESC; """ SQL_RESOLVE_PHYSICAL_CELL = """ @@ -142,6 +157,7 @@ SQL_LEGACY_MOVE = """ SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @RC int = 0; +DECLARE @RefreshPickingReservation bit = :refresh_picking_reservation; EXEC dbo.sp_xMagGestioneMagazziniPallet @IDOperatore = :id_operatore, @@ -150,7 +166,10 @@ EXEC dbo.sp_xMagGestioneMagazziniPallet @NumeroCella = :numero_cella, @RC = @RC OUTPUT; -EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew; +IF @RefreshPickingReservation = 1 +BEGIN + EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew; +END; SELECT @RC AS RC, @@ -408,6 +427,7 @@ class BarcodeRepository: barcode_cella: str, barcode_pallet: str, numero_cella: int, + refresh_picking_reservation: bool = True, ) -> LegacyMoveResult: """Execute the same stored procedure used by the C# barcode form.""" @@ -416,6 +436,7 @@ class BarcodeRepository: "barcode_cella": str(barcode_cella or "").strip(), "barcode_pallet": str(barcode_pallet or "").strip(), "numero_cella": int(numero_cella), + "refresh_picking_reservation": 1 if refresh_picking_reservation else 0, } log_runtime_event( "Barcode WMS", @@ -423,7 +444,8 @@ class BarcodeRepository: "MOVE START " f"operator={params['id_operatore']} " f"cell={params['barcode_cella']} " - f"pallet={params['barcode_pallet']}" + f"pallet={params['barcode_pallet']} " + f"refresh_pl={params['refresh_picking_reservation']}" ), ) try: diff --git a/barcode_service.py b/barcode_service.py index 73aa98d..ebcb5dd 100644 --- a/barcode_service.py +++ b/barcode_service.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Literal from barcode_repository import BarcodeRepository, LegacyMoveResult @@ -115,6 +115,15 @@ class BarcodeService: ) return self._state + def resume_priority_state(self, state: BarcodeViewState) -> BarcodeViewState: + """Restore the exact picking pallet that was visible before a local pause.""" + + if state.mode not in ("priority_high", "priority_low"): + return self._state + self._current_priority_state = 1 if state.mode == "priority_high" else 0 + self._state = replace(state, scanned_pallet="", auto_advance_delay_ms=0) + return self._state + async def skip_current_picking_pallet(self) -> BarcodeActionResult: """Mark the currently proposed picking pallet as skipped and advance.""" @@ -277,7 +286,11 @@ class BarcodeService: target_display = resolved_cell.ubicazione or destination current_location = await self.repository.fetch_current_location_by_pallet(pallet) - picking_before_move = await self.repository.fetch_picking_by_pallet(pallet) + picking_before_move = ( + None + if current_location + else await self.repository.fetch_picking_by_pallet(pallet) + ) if not current_location and not picking_before_move: if is_direct_load: trace_row = await self.repository.fetch_trace_by_pallet(pallet) @@ -297,6 +310,7 @@ class BarcodeService: barcode_cella=self.NON_SCAFFALATA_BARCODE, barcode_pallet=pallet, numero_cella=int(self.NON_SCAFFALATA_BARCODE), + refresh_picking_reservation=False, ) current_location = await self.repository.fetch_current_location_by_pallet(pallet) try: @@ -354,6 +368,7 @@ class BarcodeService: barcode_cella=target_barcode, barcode_pallet=pallet, numero_cella=target_numero_cella, + refresh_picking_reservation=is_picking_unload, ) final_location = await self.repository.fetch_current_location_by_pallet(pallet) @@ -476,7 +491,45 @@ class BarcodeService: self._state.status_color = self.RED return BarcodeActionResult(False, self._state, self._state.status_text) - return await self.submit(scanned_pallet=pallet, destination_barcode=self.NON_SCAFFALATA_BARCODE) + await self.repository.execute_legacy_move( + operator_id=self.operator_id, + barcode_cella=self.NON_SCAFFALATA_BARCODE, + barcode_pallet=pallet, + numero_cella=int(self.NON_SCAFFALATA_BARCODE), + refresh_picking_reservation=False, + ) + + final_location = await self.repository.fetch_current_location_by_pallet(pallet) + try: + final_cell = int((final_location or {}).get("IDCella") or 0) + except Exception: + final_cell = 0 + if final_cell != 1000: + self._state.scanned_pallet = pallet + self._state.destination_barcode = source + self._state.status_text = "Movimento non confermato: la UDC non risulta non scaffalata." + self._state.status_color = self.RED + return BarcodeActionResult(False, self._state, self._state.status_text) + + try: + self._state = await self._build_post_move_state( + barcode_pallet=pallet, + destination_barcode=self.NON_SCAFFALATA_BARCODE, + destination_display=self.CONVENTIONAL_LOCATION_BY_CELL[1000], + last_priority_state=self._current_priority_state, + auto_advance_delay_ms=0, + ) + except Exception as exc: + log_exception("Barcode WMS", exc, context=f"post source-check unload pallet={pallet} source={source}") + self._state = BarcodeViewState( + mode="confirm", + queue_label="Conferma movimento", + status_text="Movimento eseguito. Dettagli non aggiornati.", + status_color=self.GREEN_YELLOW, + destination_barcode=self.NON_SCAFFALATA_BARCODE, + scanned_pallet=pallet, + ) + return BarcodeActionResult(True, self._state, self._state.status_text) async def _block_closed_picking_free_move( self, diff --git a/db_config.py b/db_config.py index a4f41de..fedec86 100644 --- a/db_config.py +++ b/db_config.py @@ -26,6 +26,11 @@ DEFAULT_DB_CONFIG: dict[str, Any] = { "driver": "ODBC Driver 17 for SQL Server", "trust_server_certificate": True, "encrypt": "", + "query_profiler": { + "enabled": False, + "slow_ms": 0, + "sql_limit": 4000, + }, } diff --git a/version_info.py b/version_info.py index 14ad195..ca3c6bc 100644 --- a/version_info.py +++ b/version_info.py @@ -10,12 +10,12 @@ APP_VERSION = "1.0.0" MODULE_VERSIONS: dict[str, str] = { "app": APP_VERSION, "async_loop_singleton": "1.0.0", - "async_msssql_query": "1.0.0", + "async_msssql_query": "1.0.1", "audit_log": "1.0.0", "main": "1.0.1", - "barcode_client": "1.0.24", - "barcode_repository": "1.0.10", - "barcode_service": "1.0.22", + "barcode_client": "1.0.25", + "barcode_repository": "1.0.12", + "barcode_service": "1.0.24", "busy_overlay": "1.0.0", "db_config": "1.0.0", "gestione_aree": "1.0.1",