Dish_Recognition/service/result_outbox.py

167 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding=utf-8
"""识别订单上报失败落盘队列:网络恢复后自动重发,避免订单丢失。"""
import json
import os
import threading
import time
from datetime import datetime
class ResultOutbox:
"""JSON 文件出站队列(按 request_id+kind 去重)。"""
def __init__(self, root_dir):
self.root_dir = os.path.abspath(root_dir or "data/result_outbox")
self.index_path = os.path.join(self.root_dir, "queue.json")
self.lock = threading.RLock()
os.makedirs(self.root_dir, exist_ok=True)
self._items = []
self._load()
def _load(self):
if not os.path.isfile(self.index_path):
self._items = []
return
try:
with open(self.index_path, "r", encoding="utf-8") as f:
data = json.load(f)
self._items = list(data.get("items") or [])
except Exception as exc:
print(f"[outbox] load failed: {exc}")
self._items = []
def _save(self):
tmp = self.index_path + ".tmp"
try:
with open(tmp, "w", encoding="utf-8") as f:
json.dump(
{"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "items": self._items},
f,
ensure_ascii=False,
indent=2,
)
os.replace(tmp, self.index_path)
except Exception as exc:
print(f"[outbox] save failed: {exc}")
try:
if os.path.isfile(tmp):
os.remove(tmp)
except Exception:
pass
@staticmethod
def _key(kind, request_id):
return f"{kind}:{request_id or ''}"
def enqueue(self, kind, payload, error=None, next_delay=15.0):
"""写入/更新一条待重发记录。返回 True 表示新建。"""
request_id = (payload or {}).get("request_id") or ""
if not request_id:
request_id = f"REQ_LOCAL_{int(time.time() * 1000)}"
payload = dict(payload or {})
payload["request_id"] = request_id
kind = (kind or "recognition").strip() or "recognition"
now = time.time()
with self.lock:
key = self._key(kind, request_id)
for item in self._items:
if self._key(item.get("kind"), item.get("request_id")) == key:
item["payload"] = payload
item["last_error"] = str(error or "")[:500]
item["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
item["next_retry_at"] = now + float(next_delay or 15)
self._save()
return False
self._items.append({
"kind": kind,
"request_id": request_id,
"payload": payload,
"retry_count": 0,
"last_error": str(error or "")[:500],
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"next_retry_at": now + float(next_delay or 15),
})
self._save()
print(f"[outbox] queued {kind} request_id={request_id} err={error}")
return True
def ack(self, kind, request_id):
kind = (kind or "recognition").strip() or "recognition"
with self.lock:
before = len(self._items)
self._items = [
it for it in self._items
if not (
it.get("kind") == kind
and it.get("request_id") == request_id
)
]
if len(self._items) != before:
self._save()
print(f"[outbox] acked {kind} request_id={request_id}")
return True
return False
def bump(self, kind, request_id, error=None, next_delay=30.0, max_attempts=0):
"""失败后增加重试计数;超过上限则标记 dead(仍保留便于人工处理)。"""
kind = (kind or "recognition").strip() or "recognition"
now = time.time()
with self.lock:
for item in self._items:
if item.get("kind") != kind or item.get("request_id") != request_id:
continue
item["retry_count"] = int(item.get("retry_count") or 0) + 1
item["last_error"] = str(error or "")[:500]
item["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
max_n = int(max_attempts or 0)
if max_n > 0 and item["retry_count"] >= max_n:
item["status"] = "dead"
item["next_retry_at"] = now + 86400 * 365
print(
f"[outbox] dead {kind} request_id={request_id} "
f"retries={item['retry_count']} err={error}"
)
else:
item["status"] = "pending"
# 指数退避,最长 10 分钟
delay = min(float(next_delay or 30) * (2 ** min(item["retry_count"] - 1, 5)), 600.0)
item["next_retry_at"] = now + delay
print(
f"[outbox] retry#{item['retry_count']} {kind} "
f"request_id={request_id} next={delay:.0f}s err={error}"
)
self._save()
return item["retry_count"]
return 0
def due_items(self, limit=20):
now = time.time()
with self.lock:
due = []
for item in self._items:
if (item.get("status") or "pending") == "dead":
continue
if float(item.get("next_retry_at") or 0) > now:
continue
due.append(item)
due.sort(key=lambda x: float(x.get("next_retry_at") or 0))
return [dict(it) for it in due[: max(1, int(limit or 20))]]
def pending_count(self):
with self.lock:
return sum(
1 for it in self._items
if (it.get("status") or "pending") != "dead"
)
def stats(self):
with self.lock:
pending = dead = 0
for it in self._items:
if (it.get("status") or "pending") == "dead":
dead += 1
else:
pending += 1
return {"pending": pending, "dead": dead, "total": len(self._items)}