Ottimizza barcode e disattiva profiler query

This commit is contained in:
2026-07-04 18:26:20 +02:00
parent 77da8d9bad
commit eb12af3b9a
6 changed files with 310 additions and 26 deletions

View File

@@ -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] + "...<truncated>"
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()
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()
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 {})
return res.rowcount or 0
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

View File

@@ -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()

View File

@@ -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:

View File

@@ -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,

View File

@@ -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,
},
}

View File

@@ -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",