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:
parent
0522414a6c
commit
08f7adb0fd
@ -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)+ 本变量(真·环境变量),
|
||||
|
||||
@ -158,18 +158,22 @@ class UserContainerManager:
|
||||
handle.touch()
|
||||
return handle
|
||||
|
||||
if not self._has_capacity(key):
|
||||
raise RuntimeError(tr("container_mgr.quota_exhausted"))
|
||||
# 配额检查仅针对 docker 句柄:host 句柄只是会话标识,不占用容器池资源,
|
||||
# 创建 host 句柄时直接跳过全局/每用户两道配额闸门
|
||||
if mode == "docker":
|
||||
if not self._has_capacity(key):
|
||||
raise RuntimeError(tr("container_mgr.quota_exhausted"))
|
||||
|
||||
# 每用户容器配额:防单用户多工作区占满全局容器池,挤占其他用户
|
||||
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}::")
|
||||
)
|
||||
if owned >= per_user_limit:
|
||||
raise RuntimeError(tr("container_mgr.per_user_quota_exhausted", limit=per_user_limit))
|
||||
# 每用户容器配额:防单用户多工作区占满全局容器池,挤占其他用户
|
||||
per_user_limit = MAX_ACTIVE_CONTAINERS_PER_USER
|
||||
if per_user_limit > 0:
|
||||
owned = sum(
|
||||
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))
|
||||
|
||||
# Important: create container using the cache key so each workspace gets its own container name.
|
||||
handle = self._create_handle(key, workspace, mode)
|
||||
@ -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":
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user