fix(containers): 配额只统计 docker 句柄,修复 host 模式工作区列表 503

- ensure_container 的全局/每用户配额闸门仅在创建 docker 句柄时执行,
  host 句柄(会话标识,不占容器池)不再计入任何配额,修复 host 模式
  下第 4 个工作区起 GET /api/conversations 返回 503 的问题
- _has_capacity / has_capacity 全局上限计数同步只统计 docker 句柄
- MAX_ACTIVE_CONTAINERS_PER_USER 默认值 3 -> 8
- admin 面板 available_slots 展示对齐为 docker 句柄计数
This commit is contained in:
JOJO 2026-09-02 23:39:42 +08:00
parent 0522414a6c
commit 08f7adb0fd
4 changed files with 29 additions and 17 deletions

View File

@ -57,7 +57,8 @@ LINUX_SAFETY = os.environ.get("LINUX_SAFETY", "0") not in {"0", "false", "False"
TOOLBOX_TERMINAL_IDLE_SECONDS = int(os.environ.get("TOOLBOX_TERMINAL_IDLE_SECONDS", "900"))
MAX_ACTIVE_USER_CONTAINERS = int(os.environ.get("MAX_ACTIVE_USER_CONTAINERS", "8"))
# 每用户同时活跃的容器上限防单用户多工作区占满全局容器池2026-09-02 审计新增)
MAX_ACTIVE_CONTAINERS_PER_USER = int(os.environ.get("MAX_ACTIVE_CONTAINERS_PER_USER", "3"))
# 仅统计 docker 句柄host 句柄不计入);默认值 8 与全局上限对齐2026-09-02 调整)
MAX_ACTIVE_CONTAINERS_PER_USER = int(os.environ.get("MAX_ACTIVE_CONTAINERS_PER_USER", "8"))
HOST_EXECUTION_MODE_DEFAULT = os.environ.get("HOST_EXECUTION_MODE_DEFAULT", "sandbox").strip().lower()
# 沙箱可写路径的「部署通道」(逗号分隔)。路径授权只有两个来源:
# config/host_sandbox_policy.json前端「路径授权」UI+ 本变量(真·环境变量),

View File

@ -158,6 +158,9 @@ class UserContainerManager:
handle.touch()
return handle
# 配额检查仅针对 docker 句柄host 句柄只是会话标识,不占用容器池资源,
# 创建 host 句柄时直接跳过全局/每用户两道配额闸门
if mode == "docker":
if not self._has_capacity(key):
raise RuntimeError(tr("container_mgr.quota_exhausted"))
@ -165,8 +168,9 @@ class UserContainerManager:
per_user_limit = MAX_ACTIVE_CONTAINERS_PER_USER
if per_user_limit > 0:
owned = sum(
1 for k in self._containers
if k == username_norm or k.startswith(f"{username_norm}::")
1 for k, h in self._containers.items()
if (k == username_norm or k.startswith(f"{username_norm}::"))
and h.mode == "docker"
)
if owned >= per_user_limit:
raise RuntimeError(tr("container_mgr.per_user_quota_exhausted", limit=per_user_limit))
@ -193,7 +197,8 @@ class UserContainerManager:
return True
if self.max_containers <= 0:
return True
return len(self._containers) < self.max_containers
docker_total = sum(1 for h in self._containers.values() if h.mode == "docker")
return docker_total < self.max_containers
def get_handle(self, container_key: str) -> Optional[ContainerHandle]:
key = self._normalize_username(container_key)
@ -293,8 +298,11 @@ class UserContainerManager:
def _has_capacity(self, username: str) -> bool:
if self.max_containers <= 0:
return True
existing = 1 if username in self._containers else 0
return (len(self._containers) - existing) < self.max_containers
# 全局上限同样只统计 docker 句柄host 句柄不占用容器池)
docker_total = sum(1 for h in self._containers.values() if h.mode == "docker")
existing_handle = self._containers.get(username)
existing = 1 if (existing_handle and existing_handle.mode == "docker") else 0
return (docker_total - existing) < self.max_containers
def _create_handle(self, username: str, workspace: str, mode: str) -> ContainerHandle:
if mode != "docker":

View File

@ -419,7 +419,9 @@ def build_api_admin_dashboard_snapshot():
max_containers = getattr(container_manager, "max_containers", None) or len(handle_map)
available_slots = None
if max_containers:
available_slots = max(0, max_containers - len(handle_map))
# 可用余量只对 docker 句柄计数host 句柄不占用容器池配额)
docker_total = sum(1 for val in handle_map.values() if val.get("mode") == "docker")
available_slots = max(0, max_containers - docker_total)
upload_events = collect_upload_events()
uploads_summary = summarize_upload_events(upload_events, quarantine_total_bytes)

View File

@ -312,7 +312,8 @@ def collect_container_snapshots(handle_map: Dict[str, Dict[str, Any]]) -> Dict[s
"host": active_total - docker_count,
"issues": failure_count,
"max_containers": container_manager.max_containers,
"available_slots": max(0, container_manager.max_containers - active_total) if container_manager.max_containers > 0 else None,
# 可用余量只对 docker 句柄计数host 句柄不占用容器池配额)
"available_slots": max(0, container_manager.max_containers - docker_count) if container_manager.max_containers > 0 else None,
"avg_cpu_percent": round(sum(cpu_values) / len(cpu_values), 2) if cpu_values else None,
"avg_mem_percent": round(sum(mem_percent_values) / len(mem_percent_values), 2) if mem_percent_values else None,
"total_mem_used_bytes": total_mem_used,