save
This commit is contained in:
+1
-1
@@ -115,7 +115,7 @@
|
|||||||
(不再是"网关不装 torch/mediapipe/opencv",只是仍不需要 torch/transformers/scikit-image 等重依赖)。
|
(不再是"网关不装 torch/mediapipe/opencv",只是仍不需要 torch/transformers/scikit-image 等重依赖)。
|
||||||
- 配置 `gateway/config.json`(不入 git):`workers` 列表、`shared_password`、
|
- 配置 `gateway/config.json`(不入 git):`workers` 列表、`shared_password`、
|
||||||
`ark` 的 api_key/base_url/model、`public_base_url`、超时(**生发接口慢,`request_timeout_seconds` 调大 ≥120s**)。
|
`ark` 的 api_key/base_url/model、`public_base_url`、超时(**生发接口慢,`request_timeout_seconds` 调大 ≥120s**)。
|
||||||
- 托管 `/static/annotations/`(落盘的图),定期清理。
|
- 托管 `/static/annotations/`(落盘的图),永久保留,不自动清理。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-67
@@ -4,13 +4,10 @@
|
|||||||
不跑任何算法(无 torch/mediapipe/opencv 依赖)。
|
不跑任何算法(无 torch/mediapipe/opencv 依赖)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from io import BytesIO
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -67,32 +64,15 @@ async def lifespan(app: FastAPI):
|
|||||||
else:
|
else:
|
||||||
app.state._pool_shutdown = None
|
app.state._pool_shutdown = None
|
||||||
|
|
||||||
# 确保标注图目录存在
|
# 确保标注图目录存在(永久保留,不做定期清理)
|
||||||
static_dir = Path(cfg["static_dir"])
|
static_dir = Path(cfg["static_dir"])
|
||||||
static_dir.mkdir(parents=True, exist_ok=True)
|
static_dir.mkdir(parents=True, exist_ok=True)
|
||||||
logger.info("标注图目录: %s", static_dir)
|
logger.info("标注图目录: %s(永久保留,不自动清理)", static_dir)
|
||||||
|
|
||||||
# 启动定期清理任务(阶段四)
|
|
||||||
cleanup_shutdown = asyncio.Event()
|
|
||||||
cleanup_task = asyncio.create_task(
|
|
||||||
_cleanup_loop(static_dir, cfg, cleanup_shutdown)
|
|
||||||
)
|
|
||||||
app.state._cleanup_shutdown = cleanup_shutdown
|
|
||||||
app.state._cleanup_task = cleanup_task
|
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
# 关闭
|
# 关闭
|
||||||
logger.info("网关关闭中...")
|
logger.info("网关关闭中...")
|
||||||
# 停止清理任务
|
|
||||||
if app.state._cleanup_shutdown:
|
|
||||||
app.state._cleanup_shutdown.set()
|
|
||||||
if app.state._cleanup_task:
|
|
||||||
app.state._cleanup_task.cancel()
|
|
||||||
try:
|
|
||||||
await app.state._cleanup_task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
if app.state._pool_shutdown:
|
if app.state._pool_shutdown:
|
||||||
await app.state._pool_shutdown()
|
await app.state._pool_shutdown()
|
||||||
logger.info("网关已关闭")
|
logger.info("网关已关闭")
|
||||||
@@ -132,51 +112,6 @@ def _get_pool_status_safe():
|
|||||||
return {"total": 0, "healthy": 0, "busy": 0}
|
return {"total": 0, "healthy": 0, "busy": 0}
|
||||||
|
|
||||||
|
|
||||||
async def _cleanup_loop(annotations_dir: Path, cfg: dict, shutdown: asyncio.Event):
|
|
||||||
"""定期清理 static/annotations/ 中过期的标注图文件。
|
|
||||||
|
|
||||||
配置项(可选,在 config.json 中设定):
|
|
||||||
- cleanup.interval_minutes: 清理间隔,默认 60
|
|
||||||
- cleanup.max_age_hours: 文件保留时长(小时),默认 24
|
|
||||||
"""
|
|
||||||
cleanup_cfg = cfg.get("cleanup", {})
|
|
||||||
interval_s = cleanup_cfg.get("interval_minutes", 60) * 60
|
|
||||||
max_age_s = cleanup_cfg.get("max_age_hours", 24) * 3600
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"清理任务启动 | 间隔=%dmin | 保留=%dh | 目录=%s",
|
|
||||||
interval_s // 60, max_age_s // 3600, annotations_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
while not shutdown.is_set():
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(shutdown.wait(), timeout=interval_s)
|
|
||||||
break # shutdown
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
pass # 正常到时,执行清理
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
deleted = 0
|
|
||||||
for f in annotations_dir.iterdir():
|
|
||||||
if f.name == ".gitkeep":
|
|
||||||
continue
|
|
||||||
if not f.is_file():
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
age_s = now - f.stat().st_mtime
|
|
||||||
if age_s > max_age_s:
|
|
||||||
f.unlink()
|
|
||||||
deleted += 1
|
|
||||||
logger.debug("清理过期文件: %s (age=%.1fh)", f.name, age_s / 3600)
|
|
||||||
except Exception:
|
|
||||||
logger.warning("清理文件失败: %s", f.name, exc_info=True)
|
|
||||||
|
|
||||||
if deleted:
|
|
||||||
logger.info("清理完成: 删除 %d 个过期文件", deleted)
|
|
||||||
|
|
||||||
logger.info("清理任务已停止")
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/gateway-health", include_in_schema=False)
|
@app.get("/gateway-health", include_in_schema=False)
|
||||||
async def gateway_health():
|
async def gateway_health():
|
||||||
"""网关自身健康检查(区别于 worker 的 /health)。"""
|
"""网关自身健康检查(区别于 worker 的 /health)。"""
|
||||||
|
|||||||
@@ -22,9 +22,5 @@
|
|||||||
"request_timeout_seconds": 600,
|
"request_timeout_seconds": 600,
|
||||||
"retry_on_failure": true,
|
"retry_on_failure": true,
|
||||||
"max_retries": 1
|
"max_retries": 1
|
||||||
},
|
|
||||||
"cleanup": {
|
|
||||||
"interval_minutes": 60,
|
|
||||||
"max_age_hours": 24
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ DEFAULTS = {
|
|||||||
"retry_on_failure": True,
|
"retry_on_failure": True,
|
||||||
"max_retries": 1,
|
"max_retries": 1,
|
||||||
},
|
},
|
||||||
"cleanup": {
|
|
||||||
"interval_minutes": 60,
|
|
||||||
"max_age_hours": 24,
|
|
||||||
},
|
|
||||||
"request_log": {
|
"request_log": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"log_file": "gateway/request_log.jsonl",
|
"log_file": "gateway/request_log.jsonl",
|
||||||
|
|||||||
+3
-3
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
设计原则(与用户约定):
|
设计原则(与用户约定):
|
||||||
- 入参/出参日志里**绝不内嵌图片 base64**。图片统一存盘后用 URL 引用,保持日志短小。
|
- 入参/出参日志里**绝不内嵌图片 base64**。图片统一存盘后用 URL 引用,保持日志短小。
|
||||||
- 入参图片(image_file / *_base64)当前网关不存盘——这里补存到 static_dir(in_ 前缀),
|
- 入参图片(image_file / *_base64)补存到 static_dir(in_ 前缀),永久保留;
|
||||||
复用现有 24h 清理循环自动回收;url 输入图本身就在远端,不重新下载,直接记 URL。
|
url 输入图本身就在远端,不重新下载,直接记 URL。
|
||||||
- 出参响应在 forward.py 已把 base64 改写成 *_url(无大图),记录完整 data,
|
- 出参响应在 forward.py 已把 base64 改写成 *_url(无大图),记录完整 data,
|
||||||
但用递归摘要器截断超长结构(landmarks、长字符串),单条上限 ~8KB。
|
但用递归摘要器截断超长结构(landmarks、长字符串),单条上限 ~8KB。
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ def save_image_bytes(
|
|||||||
"""把图片字节存盘并返回公网 URL。失败返回 None(不抛异常)。
|
"""把图片字节存盘并返回公网 URL。失败返回 None(不抛异常)。
|
||||||
|
|
||||||
存到 static_dir/{prefix}{uuid}.{ext},URL = {public_base_url}/static/annotations/{file}。
|
存到 static_dir/{prefix}{uuid}.{ext},URL = {public_base_url}/static/annotations/{file}。
|
||||||
与 forward.py 的 rewrite_base64_to_url 落盘路径/URL 规则一致,可被同一清理循环回收。
|
与 forward.py 的 rewrite_base64_to_url 落盘路径/URL 规则一致;文件永久保留,不自动清理。
|
||||||
"""
|
"""
|
||||||
if not data:
|
if not data:
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user