147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
|
|
"""调试截图资源回收器:每小时清理,100MB 上限。
|
|||
|
|
|
|||
|
|
清理 /tmp/woc_debug/ 下的调试截图:
|
|||
|
|
- 按 mtime 删除超过 max_age_hours 的文件
|
|||
|
|
- 总量超 max_total_mb 时按 LRU(最旧 mtime)删除最老文件直到达标
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import logging
|
|||
|
|
import os
|
|||
|
|
import time
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
logger = logging.getLogger("woc-bridge")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ResourceReaper:
|
|||
|
|
"""调试截图资源回收器。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
debug_screenshot_dir: 调试截图目录,默认 /tmp/woc_debug
|
|||
|
|
max_age_hours: 文件最大存活小时数,默认 24
|
|||
|
|
max_total_mb: 目录总大小上限 MB,默认 100
|
|||
|
|
interval: 检查间隔秒数,默认 3600(1 小时)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
debug_screenshot_dir: str = "/tmp/woc_debug",
|
|||
|
|
max_age_hours: int = 24,
|
|||
|
|
max_total_mb: int = 100,
|
|||
|
|
interval: float = 3600.0,
|
|||
|
|
) -> None:
|
|||
|
|
self.debug_screenshot_dir = debug_screenshot_dir
|
|||
|
|
self.max_age_hours = max_age_hours
|
|||
|
|
self.max_total_mb = max_total_mb
|
|||
|
|
self.interval = interval
|
|||
|
|
self._task: Optional[asyncio.Task] = None
|
|||
|
|
self._stopped = False
|
|||
|
|
|
|||
|
|
async def start(self) -> None:
|
|||
|
|
"""启动后台回收任务。"""
|
|||
|
|
if self._task is not None and not self._task.done():
|
|||
|
|
return
|
|||
|
|
self._stopped = False
|
|||
|
|
self._task = asyncio.create_task(self._run())
|
|||
|
|
logger.info(
|
|||
|
|
"[reaper] started (dir=%s max_age=%dh max_total=%dMB)",
|
|||
|
|
self.debug_screenshot_dir, self.max_age_hours, self.max_total_mb,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
async def stop(self) -> None:
|
|||
|
|
"""停止回收任务。"""
|
|||
|
|
self._stopped = True
|
|||
|
|
if self._task is not None and not self._task.done():
|
|||
|
|
self._task.cancel()
|
|||
|
|
try:
|
|||
|
|
await asyncio.wait_for(self._task, timeout=2.0)
|
|||
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|||
|
|
pass
|
|||
|
|
self._task = None
|
|||
|
|
logger.info("[reaper] stopped")
|
|||
|
|
|
|||
|
|
async def _run(self) -> None:
|
|||
|
|
"""主循环:每 interval 秒清理一次。"""
|
|||
|
|
while not self._stopped:
|
|||
|
|
try:
|
|||
|
|
await asyncio.sleep(self.interval)
|
|||
|
|
await self.cleanup()
|
|||
|
|
except asyncio.CancelledError:
|
|||
|
|
break
|
|||
|
|
except Exception as exc:
|
|||
|
|
logger.warning("[reaper] cleanup exception: %s", exc)
|
|||
|
|
|
|||
|
|
async def cleanup(self) -> None:
|
|||
|
|
"""执行一次清理:TTL 删除 + 总量限制 LRU 删除。"""
|
|||
|
|
await asyncio.to_thread(self._cleanup_sync)
|
|||
|
|
|
|||
|
|
def _cleanup_sync(self) -> None:
|
|||
|
|
"""同步清理实现(在 to_thread 中执行)。"""
|
|||
|
|
if not os.path.isdir(self.debug_screenshot_dir):
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
now = time.time()
|
|||
|
|
max_age_sec = self.max_age_hours * 3600
|
|||
|
|
|
|||
|
|
# 1. 收集所有文件 + mtime + size
|
|||
|
|
files: list[tuple[str, float, int]] = [] # (path, mtime, size)
|
|||
|
|
for name in os.listdir(self.debug_screenshot_dir):
|
|||
|
|
path = os.path.join(self.debug_screenshot_dir, name)
|
|||
|
|
if not os.path.isfile(path):
|
|||
|
|
continue
|
|||
|
|
try:
|
|||
|
|
stat = os.stat(path)
|
|||
|
|
files.append((path, stat.st_mtime, stat.st_size))
|
|||
|
|
except OSError:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if not files:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 2. 删除超 TTL 的文件
|
|||
|
|
deleted_ttl = 0
|
|||
|
|
survivors: list[tuple[str, float, int]] = []
|
|||
|
|
for path, mtime, size in files:
|
|||
|
|
if now - mtime > max_age_sec:
|
|||
|
|
try:
|
|||
|
|
os.remove(path)
|
|||
|
|
deleted_ttl += 1
|
|||
|
|
except OSError as exc:
|
|||
|
|
logger.debug("[reaper] remove %s failed: %s", path, exc)
|
|||
|
|
survivors.append((path, mtime, size))
|
|||
|
|
else:
|
|||
|
|
survivors.append((path, mtime, size))
|
|||
|
|
|
|||
|
|
if deleted_ttl > 0:
|
|||
|
|
logger.info(
|
|||
|
|
"[reaper] deleted %d files (TTL > %dh)", deleted_ttl, self.max_age_hours
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 3. 总量超限时按 LRU(最旧 mtime)删除
|
|||
|
|
total_bytes = sum(s for _, _, s in survivors)
|
|||
|
|
max_bytes = self.max_total_mb * 1024 * 1024
|
|||
|
|
if total_bytes <= max_bytes:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 按 mtime 升序(最旧在前)
|
|||
|
|
survivors.sort(key=lambda x: x[1])
|
|||
|
|
deleted_lru = 0
|
|||
|
|
for path, _, size in survivors:
|
|||
|
|
if total_bytes <= max_bytes:
|
|||
|
|
break
|
|||
|
|
try:
|
|||
|
|
os.remove(path)
|
|||
|
|
total_bytes -= size
|
|||
|
|
deleted_lru += 1
|
|||
|
|
except OSError as exc:
|
|||
|
|
logger.debug("[reaper] remove %s failed: %s", path, exc)
|
|||
|
|
|
|||
|
|
if deleted_lru > 0:
|
|||
|
|
logger.info(
|
|||
|
|
"[reaper] deleted %d files (LRU, total now %.1fMB)",
|
|||
|
|
deleted_lru, total_bytes / (1024 * 1024),
|
|||
|
|
)
|