這是一個專為 Online Judge (OJ) 設計的高效能、非同步程式碼評測沙盒服務。核心基於 isolate 技術,提供安全、資源受限的程式碼執行環境。
本專案採用 Producer-Consumer 架構,利用 Python 的 asyncio 實現高並發處理,確保在高負載下 API 依然能快速回應。
為了讓您快速理解系統運作,以下使用圖形化方式呈現各元件的互動關係。
| 服務名稱 | 容器內埠 | 對外埠 | 說明 |
|---|---|---|---|
| sandbox-oj | 8000 | 8000 | 主評測 API 服務 |
| static-analysis | 8000 | 8001 | 靜態分析 API 服務 |
| llm-test-data | 8787 | 8787 | LLM 測資生成服務 |
| portal | 9999 | 9999 | 監控入口頁面 |
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ 🌐 外部系統 (External Systems) │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ HTTP Requests ┌──────────────────┐ │
│ │ Backend Service │ ─────────────────────────▶ │ Sandbox API │ │
│ │ (後端主服務) │ │ Port: 8000 │ │
│ │ │ ◀───── Webhook Callback ─── │ │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ 📦 Sandbox 服務內部架構 │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ api_server.py (FastAPI :8000) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ /submissions│ │ /checkers │ │ /queue/* │ │ /health │ │ /dashboard │ │ │
│ │ │ 提交評測 │ │ 上傳Checker │ │ 佇列控制 │ │ 健康檢查 │ │ 監控頁面 │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │ │
│ │ │ ① 驗證 API Key │ ② 儲存 Checker │ │
│ │ │ ② 驗證參數 │ 到 /checkers │ │
│ │ │ ③ 確認題包 │ │ │
│ │ ▼ ▼ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ Push Job │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ queue_manager.py (Priority Queue) │ │
│ │ ┌───────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ [Priority 1] ─▶ [Priority 5] ─▶ [Priority 10] ─▶ [Priority 10] ─▶ ... │ │ │
│ │ │ 比賽提交 高優先級 一般提交 一般提交 │ │ │
│ │ └───────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ ⬆ pause() / resume() / clear() (佇列控制 API) │ │
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ Pop Job (依優先級) │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ worker.py (Worker Pool) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Worker 0 │ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ ... │ │
│ │ │ Box ID: 0 │ │ Box ID: 1 │ │ Box ID: 2 │ │ Box ID: 3 │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │ │
│ │ └────────────────┴────────────────┴────────────────┘ │ │
│ │ │ │ │
│ └────────────────────────────────────┼──────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┼────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ ProxyManager │ │ SidecarManager │ │ IsolateRunner │ │
│ │ (TinyProxy) │ │ (Docker) │ │ (Isolate CLI) │ │
│ │ 網路白名單過濾 │ │ Redis/MySQL等 │ │ 安全沙盒執行 │ │
│ └──────────────────┘ └──────────────────┘ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ /usr/bin/isolate │ │
│ │ Linux Kernel │ │
│ │ 沙盒隔離執行 │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ 🔬 靜態分析服務 (Port: 8001) │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ static-analysis-service/api/app.py │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ POST /scan │ │ GET /health │ │ GET /stats │ │ /dashboard │ │ │
│ │ │ 執行分析 │ │ 健康檢查 │ │ 統計資訊 │ │ 監控頁面 │ │ │
│ │ └──────┬──────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Clang-Tidy Engine (Custom Checks) │ │ │
│ │ │ • misc-forbid-loops 禁止迴圈 │ │ │
│ │ │ • misc-forbid-arrays 禁止陣列 │ │ │
│ │ │ • misc-forbid-stl 禁止 STL │ │ │
│ │ │ • misc-forbid-functions 禁止特定函式 │ │ │
│ │ └───────────────────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
| 檔案名稱 | 角色 | 功能描述 |
|---|---|---|
api_server.py |
櫃台接待 (Receptionist) | 系統入口。負責接收 HTTP 請求、驗證參數、將任務推入隊列,並提供 /health 與 /logs 查詢介面。 |
queue_manager.py |
點餐單 (Order Rail) | 管理一個優先級隊列 (Priority Queue)。確保高優先級的任務 (如比賽) 能被優先處理。 |
worker.py |
廚師 (Chef) | 背景工作者。不斷從隊列取出任務,準備環境,並指揮 Runner 執行程式。每個 Worker 綁定一個獨立的 Box ID。負責管理 Proxy 與 Sidecar。 |
isolate_runner.py |
廚具 (Tools) | 底層驅動。負責與 isolate 二進位檔溝通,處理檔案 I/O、解壓縮、執行指令並解析執行結果 (Meta file)。 |
models.py |
菜單規格 (Schema) | 定義資料結構 (Pydantic Models),如 SubmissionRequest, Job,確保系統內部資料傳遞的一致性。 |
docker-compose.yml |
餐廳藍圖 (Blueprint) | 定義系統如何部署。設定 Port 映射、Volume 掛載 (Log 保存) 與容器權限。 |
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 📤 後端提交評測請求 │
│ POST /api/v1/submissions │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 1: 請求驗證 (Request Validation) │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ ① API Key 驗證 │───▶│ ② 參數格式驗證 │───▶│ ③ 檔案Hash驗證 │───▶│ ④ 儲存上傳檔案 │ │ │
│ │ │ X-API-KEY │ │ Form Data │ │ SHA256 校驗 │ │ /tmp/sandbox/ │ │ │
│ │ │ │ │ Pydantic Model │ │ │ │ submissions/ │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
│ │ ❌ 403 ❌ 422 ❌ 400 │ │
│ │ 驗證失敗 格式錯誤 Hash不符 │ │
│ │ (Log記錄) (回傳錯誤) (回傳錯誤) │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│ ✅ 驗證通過
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 2: 題包管理 (Problem Package Management) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ensure_problem_package(problem_id, problem_hash) │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────────────────────┴────────────────────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ 檢查本地 .hash 檔案 │ │ Hash 不符 / 不存在 │ │ │
│ │ │ test_data/problems/ │ │ │ │ │
│ │ │ {problem_id}/.hash │ │ ⬇ 向後端索取題包 │ │ │
│ │ └───────────┬─────────────┘ │ GET /api/v1/problems/ │ │ │
│ │ │ │ {id}/package │ │ │
│ │ ┌───────────┴───────────┐ └───────────┬─────────────┘ │ │
│ │ ▼ ▼ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ▼ │ │
│ │ │ Hash 符合 │ │ Hash 不符 │ ┌─────────────────────────┐ │ │
│ │ │ ✅ 使用快取 │ │ ➡ 重新下載 │ │ ⑤ 解壓縮題包 (ZIP) │ │ │
│ │ └─────────────┘ └─────────────┘ │ ⑥ 寫入 .hash 檔案 │ │ │
│ │ │ ⑦ 題包前處理完成 │ │ │
│ │ │ (Log 記錄更新) │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 3: 建立 Job 並推入佇列 │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Job 物件包含: │ │ │
│ │ │ • submission_id, problem_id, language, mode │ │ │
│ │ │ • file_path (上傳的檔案), file_hash, problem_hash │ │ │
│ │ │ • use_checker, checker_file_path ◀── Checker 設定 │ │ │
│ │ │ • use_static_analysis, static_analysis_config ◀── 靜態分析設定 │ │ │
│ │ │ • allow_network, network_whitelist ◀── 網路設定 │ │ │
│ │ │ • sidecar_image ◀── Sidecar 設定 │ │ │
│ │ │ • callback_url, callback_token ◀── Webhook 回調設定 │ │ │
│ │ └──────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ job_queue.push(job) │ │ │
│ │ │ Priority Queue (Max: 20000) │ │ │
│ │ │ 依 priority + timestamp 排序 │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ 回傳 HTTP 202 Accepted │ │ │
│ │ │ { submission_id, queue_position }│ │ │
│ │ └──────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
═══════════════════════════
║ 非同步處理 (Async) ║
═══════════════════════════
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Worker 從佇列取得 Job │
│ job = await job_queue.pop() │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 4: 環境準備 (Environment Setup) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ⑧ Sidecar 啟動 (可選) │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ job.sidecar_image? │──Yes──▶ SidecarManager.start(image) │ │ │
│ │ │ │ redis:alpine │ • Docker 拉取映像 │ │ │
│ │ │ │ mysql:8.0 │ • 啟動容器 (Network: container:sandbox-oj) │ │ │
│ │ │ │ postgres:14-alpine │ • 等待服務就緒 (5s) │ │ │
│ │ │ │ mongo:6.0 │ • Log: "Sidecar started" │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ⑨ 網路代理啟動 (可選) │ │ │
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ job.allow_network = true? │ │ │ │
│ │ │ └─────────────┬───────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌──────────┴──────────┐ │ │ │
│ │ │ ▼ ▼ │ │ │
│ │ │ 有白名單 無白名單 │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │
│ │ │ │ ProxyManager│ │ 直接開放網路 │ │ │ │
│ │ │ │ .start() │ │ (警告 Log) │ │ │ │
│ │ │ │ TinyProxy │ └─────────────┘ │ │ │
│ │ │ │ ACL 過濾 │ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 5: 編譯階段 (Compilation Phase) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ 判斷提交模式 (job.mode) │ │ │
│ │ └──────────────────┬──────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────────────────────────┼───────────────────────────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │
│ │ │ Mode: "normal" │ │ Mode: "zip" │ │ Mode: "normal" │ │ │
│ │ │ 單一檔案模式 │ │ ZIP 專案模式 │ │ 函式實作模式 │ │ │
│ │ │ (Single File) │ │ (ZIP Project) │ │ (Implementation) │ │ │
│ │ └─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │
│ │ │ ⑩ 讀取程式碼 │ │ ⑩ 解壓縮 ZIP │ │ ⑩ 載入題目提供 │ │ │
│ │ │ main.c/cpp/py │ │ 檢查內容物: │ │ 的檔案: │ │ │
│ │ │ │ │ • Makefile? │ │ • main.c/cpp │ │ │
│ │ │ │ │ • main.cpp? │ │ • Makefile │ │ │
│ │ │ │ │ • main.c? │ │ • header.h │ │ │
│ │ └─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 編譯策略決定 (Compile Strategy) │ │ │
│ │ │ │ │ │
│ │ │ Python: │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ ✓ 無需編譯,直接設定執行命令: ["/usr/bin/python3", "main.py"] │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ C/C++ (有 Makefile): │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ ⑪ 執行 make 編譯 │ │ │ │
│ │ │ │ command: ["/usr/bin/make"] │ │ │ │
│ │ │ │ limits: time=30s, wall=60s, processes=50 │ │ │ │
│ │ │ │ 輸出執行檔: ./main │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ C/C++ (無 Makefile): │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ ⑪ 自動編譯所有原始碼 │ │ │ │
│ │ │ │ C: gcc *.c -o main │ │ │ │
│ │ │ │ C++: g++ *.cpp -o main │ │ │ │
│ │ │ │ limits: time=10s, wall=20s, processes=20 │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ 編譯結果: │ │ │
│ │ │ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ │ │
│ │ │ │ ✅ 編譯成功 │ │ ❌ 編譯失敗 (CE) │ │ │ │
│ │ │ │ compile_info.status = "OK" │ │ compile_info.status = "CE" │ │ │ │
│ │ │ │ 繼續執行測試 │ │ 記錄 stdout/stderr │ │ │ │
│ │ │ │ │ │ 跳過執行階段 │ │ │ │
│ │ │ └──────────────────────────────┘ └──────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 6: 測資執行 (Test Case Execution) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ⑫ 讀取 meta.json 決定測試模式 │ │ │
│ │ │ │ │ │
│ │ │ meta.json 範例: │ │ │
│ │ │ { │ │ │
│ │ │ "tasks": [ │ │ │
│ │ │ { "taskScore": 50, "caseCount": 3, "timeLimit": 1000, "memoryLimit": 256 }, │ │ │
│ │ │ { "taskScore": 50, "caseCount": 2, "timeLimit": 2000, "memoryLimit": 512 } │ │ │
│ │ │ ] │ │ │
│ │ │ } │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ⑬ 遍歷所有 Task & Case │ │ │
│ │ │ │ │ │
│ │ │ for task in tasks: │ │ │
│ │ │ for case in range(caseCount): │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 讀取測資檔案 │ │ │ │
│ │ │ │ testcase/{taskIdx:02d}{caseIdx+1:02d}.in (輸入) │ │ │ │
│ │ │ │ testcase/{taskIdx:02d}{caseIdx+1:02d}.out (預期輸出) │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ ⑭ IsolateRunner.execute() │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ • command: ["./main"] 或 ["/usr/bin/python3", "main.py"] │ │ │ │
│ │ │ │ • stdin: 測資輸入內容 │ │ │ │
│ │ │ │ • limits: {time, wall_time, memory, processes} │ │ │ │
│ │ │ │ • share_net: 是否共享網路 │ │ │ │
│ │ │ │ • env: 環境變數 (Proxy 設定) │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 執行結果狀態判定 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │
│ │ │ │ │ OK │ │ TO │ │ RE │ │ SG │ │ XX │ │ │ │ │
│ │ │ │ │ 正常結束 │ │ 超時 │ │ 運行錯誤 │ │ 信號終止 │ │ 內部錯誤 │ │ │ │ │
│ │ │ │ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ ▼ │ │ │ │
│ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ ⑮ 輸出比對 (Output Verification) │ │ │ │ │
│ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 7: 輸出驗證 (Output Verification) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ job.use_checker == true ? │ │ │
│ │ └────────────────────────┬─────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────────────────────────────┴─────────────────────────────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ 使用預設 diff 比對 │ │ 使用自訂 Checker │ │ │
│ │ │ │ │ │ │ │
│ │ │ actual_output.strip() │ │ checker_cmd = [ │ │ │
│ │ │ == │ │ checker_path, │ │ │
│ │ │ expected_output.strip() │ │ input_file, │ │ │
│ │ │ │ │ output_file, │ │ │
│ │ │ ┌───────────┐ ┌───────────┐ │ │ answer_file │ │ │
│ │ │ │ 相同: AC │ │ 不同: WA │ │ │ ] │ │ │
│ │ │ └───────────┘ └───────────┘ │ │ │ │ │
│ │ └─────────────────────────────────┘ │ ⑯ subprocess.run(checker_cmd) │ │ │
│ │ │ │ │ │
│ │ │ return_code == 0 ? AC : WA │ │ │
│ │ │ stderr -> checker message │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ │ │
│ │ ⬆ Checker 來源: │ │
│ │ • POST /api/v1/checkers 上傳自訂 Checker │ │
│ │ • 儲存於 /sandbox/checkers/{name} │ │
│ │ • 支援 Binary 或 Python Script │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 8: 靜態分析 (Static Analysis) [可選] │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ job.use_static_analysis == true ? │ │ │
│ │ └────────────────────────┬─────────────────────────────┘ │ │
│ │ │ Yes │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ⑰ 呼叫靜態分析服務 │ │ │
│ │ │ │ │ │
│ │ │ POST http://static-analysis:8000/scan │ │ │
│ │ │ │ │ │
│ │ │ Request Body: │ │ │
│ │ │ { │ │ │
│ │ │ "code": "<source_code>", │ │ │
│ │ │ "language": "cpp", │ │ │
│ │ │ "rules": ["--forbid-loops", "--forbid-arrays", "--forbid-stl"] │ │ │
│ │ │ } │ │ │
│ │ │ │ │ │
│ │ │ Response: │ │ │
│ │ │ { │ │ │
│ │ │ "success": true, │ │ │
│ │ │ "violations": [ │ │ │
│ │ │ { "rule": "forbid-loops", "line": 10, "message": "for loop detected" } │ │ │
│ │ │ ] │ │ │
│ │ │ } │ │ │
│ │ │ │ │ │
│ │ │ 結果附加到 result_data["static_analysis"] │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ⬆ 靜態分析設定來源: │ │
│ │ • 提交時透過 static_analysis_config 參數傳入 (字串格式) │ │
│ │ • 格式: "forbid_loops,forbid_arrays" 或 "--forbid-loops --forbid-arrays" │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Phase 9: 結果回報與清理 (Result & Cleanup) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ⑱ 清理資源 │ │ │
│ │ │ │ │ │
│ │ │ • 停止 TinyProxy (若有啟動),收集 Proxy Log │ │ │
│ │ │ • 停止 Sidecar 容器 (若有啟動),收集 Sidecar Log (錯誤時) │ │ │
│ │ │ • 清理暫存檔案 (/tmp/sandbox/submissions/{submission_id}) │ │ │
│ │ │ • Isolate Box 在 Worker 生命週期內保持,僅清理內容 │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ⑲ 發送 Webhook 回調 (若有設定) │ │ │
│ │ │ │ │ │
│ │ │ POST {callback_url} │ │ │
│ │ │ Headers: Authorization: Bearer {callback_token} │ │ │
│ │ │ │ │ │
│ │ │ Body: │ │ │
│ │ │ { │ │ │
│ │ │ "submission_id": "xxx", │ │ │
│ │ │ "problem_id": "yyy", │ │ │
│ │ │ "result": { │ │ │
│ │ │ "status": "AC", │ │ │
│ │ │ "score": 100, │ │ │
│ │ │ "tasks": [...], │ │ │
│ │ │ "compile_info": {...}, │ │ │
│ │ │ "static_analysis": {...} │ │ │
│ │ │ } │ │ │
│ │ │ } │ │ │
│ │ │ │ │ │
│ │ │ 重試機制: 3 次, Backoff: 1s, 2s, 3s │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ⑳ 記錄歷史 │ │ │
│ │ │ │ │ │
│ │ │ • WorkerManager.add_history() - 記憶體歷史 (最近 50 筆) │ │ │
│ │ │ • logs/submission_history.jsonl - 持久化紀錄 (JSONL 格式) │ │ │
│ │ │ • logs/sandbox.log - 詳細評測 Log │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
當一個評測請求進來時,系統內部發生了什麼事?
sequenceDiagram
participant User as User / Frontend
participant Backend as Backend Service
participant Sandbox as Sandbox API
participant Worker as Sandbox Worker
User->>Backend: 提交程式碼
Backend->>Sandbox: POST /api/v1/submissions
activate Sandbox
opt 題目包不存在 (Cache Miss)
Sandbox->>Backend: GET /api/v1/problems/{id}/package
Backend-->>Sandbox: 回傳題目 ZIP
end
Sandbox-->>Backend: 202 Accepted (已排程)
deactivate Sandbox
Note over Sandbox,Worker: 任務進入記憶體佇列 (Priority Queue)
loop 非同步評測 (Async Worker)
Worker->>Worker: 從佇列取出任務
activate Worker
Worker->>Worker: 建立沙盒 & 執行評測
opt 靜態分析 (Static Analysis)
Worker->>Worker: 呼叫 Clang-Tidy Service
end
Worker->>Backend: POST {callback_url} (回傳結果)
deactivate Worker
end
Backend-->>User: 通知評測結果
1. 接收 (Receive)
Client -> POST /api/v1/submissions
|
+-> api_server.py: 驗證 API Key 與參數
+-> api_server.py: 產生 Submission ID
+-> api_server.py: 將任務包裝成 Job 物件 -> Push to Queue
+-> Client: 收到 HTTP 202 Accepted (立即回應,不阻塞)
2. 排程 (Queueing)
Job 停留在 queue_manager.py 中等待空閒的 Worker。
(依照 Priority 排序,數字越小越優先)
3. 執行 (Execution)
Worker (空閒中) -> Pop Job from Queue
|
+-> worker.py: 建立暫存目錄,寫入程式碼檔案 (支援 ZIP 解壓)
+-> worker.py: (Optional) 啟動 Sidecar 容器 (如 Redis/MySQL)
+-> worker.py: (Optional) 啟動 TinyProxy 進行網路白名單過濾
+-> worker.py: 呼叫 isolate_runner.py 初始化沙盒 (Init Box)
+-> isolate_runner.py: 將檔案複製進沙盒內部
+-> isolate_runner.py: 執行 isolate 指令 (Run)
|
+-> [ISOLATE SANDBOX]: 執行程式 (限制 CPU/Mem/Network)
+-> [ISOLATE SANDBOX]: 產出 stdout, stderr, meta file
|
+-> isolate_runner.py: 讀取執行結果與資源使用量
+-> worker.py: (Optional) 呼叫 Static Analysis Service
+-> worker.py: 清理沙盒、暫存檔、Sidecar 與 Proxy
4. 結算 (Result)
worker.py 取得結果 (AC/WA/TLE...)
|
+-> Log: 寫入 logs/sandbox.log (包含時間、記憶體)
+-> History: 更新 WorkerManager 的記憶體歷史紀錄
+-> Webhook: (若有設定) POST 結果到 callback_url
5. 查詢 (Query)
Client -> GET /logs/view 或 /api/v1/submissions/{id}
|
+-> api_server.py: 從 Log 檔或記憶體中讀取資料回傳
| 端點 | 方法 | 認證 | 說明 | 狀態 |
|---|---|---|---|---|
/api/v1/submissions |
POST | ✅ | 提交評測任務 (正式) | ✅ 完成 |
/api/v1/selftest-submissions |
POST | ✅ | 學生自測提交 | ✅ 完成 |
/api/v1/checkers |
POST | ✅ | 上傳自訂 Checker | ✅ 完成 |
/api/v1/test_local/{problem_id} |
POST | ✅ | 測試本地題目 (Dev) | ✅ 完成 |
/api/v1/submissions/{submission_id} |
GET | ❌ | 查詢提交結果 | ✅ 完成 |
/api/v1/history |
GET | ✅ | 獲取評測歷史 | ✅ 完成 |
/api/v1/auth-history |
GET | ✅ | 獲取認證失敗紀錄 | ✅ 完成 |
/api/v1/system_stats |
GET | ✅ | Docker 容器資源統計 | ✅ 完成 |
/api/v1/queue/pause |
POST | ✅ | 暫停佇列處理 | ✅ 完成 |
/api/v1/queue/resume |
POST | ✅ | 恢復佇列處理 | ✅ 完成 |
/api/v1/queue/clear |
POST | ✅ | 清空佇列 | ✅ 完成 |
/health |
GET | ❌ | 健康檢查 | ✅ 完成 |
/dashboard |
GET | ❌ | 監控儀表板 | ✅ 完成 |
/logs/view |
GET | ❌ | Log 檢視頁面 | ✅ 完成 |
/logs/raw |
GET | ❌ | 原始 Log | ✅ 完成 |
| 端點 | 方法 | 認證 | 說明 | 狀態 |
|---|---|---|---|---|
/scan |
POST | ✅ | 執行程式碼靜態分析 | ✅ 完成 |
/health |
GET | ❌ | 健康檢查 | ✅ 完成 |
/stats |
GET | ❌ | 分析統計資訊 | ✅ 完成 |
/dashboard |
GET | ❌ | 分析儀表板 | ✅ 完成 |
| 端點 | 方法 | 說明 | 備註 |
|---|---|---|---|
/api/v1/problems/{id}/package |
GET | 取得題目包 (ZIP) | Sandbox 會帶入 problem_hash 驗證 |
{callback_url} |
POST | 接收評測結果 | 由提交時指定 |
- Docker & Docker Compose (強烈建議,因為
isolate依賴 Linux Kernel 特性) - 若在 Windows/Mac 開發,必須使用 Docker。
我們已經配置好了 docker-compose.yml,一鍵啟動:
# 建置並啟動 (背景執行)
docker-compose up -d --build啟動後,您可以透過以下方式確認系統是否活著:
- 健康檢查 API:
http://localhost:8000/health - 即時 Log 儀表板:
http://localhost:8000/logs/view - 系統狀態儀表板:
http://localhost:8000/dashboard
- Endpoint:
POST /api/v1/submissions - Content-Type:
multipart/form-data - Header:
X-API-KEY: <your-api-key>
| 參數名稱 | 類型 | 必填 | 預設值 | 說明 |
|---|---|---|---|---|
submission_id |
String | Yes | - | 唯一的提交 ID (通常由後端生成 UUID)。 |
problem_id |
String | Yes | - | 題目 ID (對應 test_data/problems/{id})。 |
language |
String | Yes | - | 程式語言 (c, cpp, python, java, go)。 |
file |
File | Yes | - | 程式碼檔案 (單檔或 ZIP)。 |
file_hash |
String | Yes | - | 檔案的 SHA256 Hash (用於驗證完整性)。 |
mode |
String | Yes | normal |
normal (單檔) 或 zip (多檔專案,支援 Makefile)。 |
time_limit |
Float | No | 2.0 |
CPU 時間限制 (秒)。 |
memory_limit |
Int | No | 256000 |
記憶體限制 (KB)。 |
use_checker |
Bool | No | false |
是否使用 Checker。 |
checker_name |
String | No | diff |
Checker 名稱 (diff 或自定義 Checker 檔名)。 |
use_static_analysis |
Bool | No | false |
是否啟用靜態分析。 |
static_analysis_config |
String | No | - | 分析規則 (如 --forbid-loops)。 |
callback_url |
String | No | - | 評測完成後通知的 Webhook URL。 |
allow_network |
Bool | No | false |
是否允許沙盒對外連線。 |
network_whitelist |
String | No | - | 網路白名單 (逗號分隔 Domain),需搭配 allow_network=true。 |
sidecar_image |
String | No | - | 啟動 Sidecar 容器 (如 redis:alpine, mysql:8.0)。 |
系統支援以下三種程式碼提交與編譯策略:
-
單一檔案 (Single File)
- 參數:
mode="normal" - 說明: 學生上傳單一
.c,.cpp或.py檔。系統自動編譯 (C/C++) 或直譯 (Python)。
- 參數:
-
ZIP 專案包 (ZIP Project)
- 參數:
mode="zip" - 說明: 學生上傳包含完整專案的
.zip檔。 - 編譯策略:
- 若 ZIP 內含
Makefile:執行make(產出執行檔需命名為main)。 - 若無
Makefile:自動搜尋main.c或main.cpp進行編譯。
- 若 ZIP 內含
- 參數:
-
函式實作 / 填空題 (Function Implementation)
- 參數:
mode="normal"(通常) - 說明: 題目提供框架程式碼 (如
main.c,header.h),學生僅上傳實作檔 (如solution.c)。 - 設定: 需在題目
meta.json中設定"provided_files": ["main.c", "Makefile", ...]。 - 編譯策略: 系統會自動將學生檔案與題目提供的檔案合併編譯。若題目提供了
Makefile,將優先使用該 Makefile。
- 參數:
- Endpoint:
POST /api/v1/selftest-submissions - 說明: 功能與參數完全同上,專供學生在練習區進行自我測試使用 (區隔正式提交)。
- Endpoint:
POST /api/v1/checkers - 用途: 上傳自定義的 Special Judge 程式 (Python 或 Binary)。
| Endpoint | Method | 說明 |
|---|---|---|
/api/v1/queue/pause |
POST | 暫停佇列處理 (維護用)。 |
/api/v1/queue/resume |
POST | 恢復佇列處理。 |
/api/v1/queue/clear |
POST | 清空佇列中所有等待的任務。 |
/api/v1/system_stats |
GET | 獲取 Docker 容器即時資源使用量 (CPU/Mem)。 |
/api/v1/history |
GET | 獲取最近的評測歷史紀錄。 |
本系統提供多層次的網路控制,以滿足不同題目的需求。
- 網路狀態:
Blocked - 說明: 沙盒內完全無法存取外部網路,最安全的模式。
- 啟用方式:
allow_network=true且提供network_whitelist(e.g.,google.com,github.com)。 - 機制: 系統會為該次評測啟動一個專屬的
TinyProxy,並配置 ACL 只允許特定 Domain。 - 用途: 爬蟲練習題、API 串接題。
- 啟用方式: 設定
sidecar_image(e.g.,redis:alpine)。 - 機制: 系統會啟動一個 Docker 容器,並與沙盒共享 Network Namespace (
localhost互通)。 - 支援清單:
redis:alpine,mysql:8.0,postgres:14-alpine,mongo:6.0。 - 用途: 資料庫練習題、快取機制實作題。
系統依賴 test_data/problems/{problem_id} 目錄下的檔案進行評測。
test_data/problems/sum_n_integers/
├── meta.json # 題目設定檔 (重要)
├── solution.py # (選用) 參考解答
└── testcase/ # 測資目錄
├── 0001.in # Task 0, Case 1 輸入
├── 0001.out # Task 0, Case 1 輸出
├── 0002.in
└── 0002.out
meta.json 定義了評測的具體規則,優先級高於 API 傳入的參數。
{
"tasks": [
{
"taskScore": 100, // 該 Task 的總分
"caseCount": 2, // 該 Task 有幾個測資點
"timeLimit": 1000, // 時間限制 (ms)
"memoryLimit": 256 // 記憶體限制 (MB)
}
],
"allow_network": false, // (選用) 是否允許網路
"network_whitelist": [], // (選用) 白名單
"sidecar_image": "redis:alpine" // (選用) Sidecar 映像檔
}本系統整合了基於 Clang-Tidy 的靜態分析微服務,允許出題者對提交的程式碼進行語法結構檢查。
+-----------+ HTTP POST /scan +-------------------------+
| | ----------------------------------> | |
| worker.py | {code, rules, lang} | static-analysis-service |
| | | (Port 8001) |
| | <---------------------------------- | |
+-----------+ {success, violations} +-------------------------+
| 規則參數 | 說明 | 範例 |
|---|---|---|
--forbid-loops |
禁止使用 for, while, do-while 迴圈 |
遞迴練習題 |
--forbid-arrays |
禁止宣告 C/C++ 陣列 (int a[10]) |
指標練習題 |
--forbid-stl |
禁止使用 C++ STL (std::vector, std::sort...) |
手刻資料結構題 |
--forbid-functions=... |
禁止使用特定函式 (逗號分隔) | --forbid-functions=printf,scanf |
當題目的答案不唯一(例如:輸出任意一組解),標準的 diff 比對無法使用,此時需要 Custom Checker。
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ Custom Checker 管理流程 │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 上傳 Checker │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ POST /api/v1/checkers │ │
│ │ │ │
│ │ Headers: X-API-KEY: <your-api-key> │ │
│ │ Content-Type: multipart/form-data │ │
│ │ │ │
│ │ Form Data: │ │
│ │ • name: "my_checker" (Checker 名稱) │ │
│ │ • file: <binary or python script> (Checker 執行檔) │ │
│ │ │ │
│ │ ▼ │ │
│ │ 儲存至: /sandbox/checkers/my_checker │ │
│ │ 設定權限: chmod 755 │ │
│ └───────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 使用 Checker (提交評測時) │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ POST /api/v1/submissions │ │
│ │ │ │
│ │ Form Data: │ │
│ │ • use_checker: true │ │
│ │ • checker_name: "my_checker" │ │
│ │ • ... (其他參數) │ │
│ └───────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 3. Checker 執行方式 │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ # Python Checker │ │
│ │ python3 /sandbox/checkers/my_checker.py <input> <output> <answer> │ │
│ │ │ │
│ │ # Binary Checker │ │
│ │ /sandbox/checkers/my_checker <input> <output> <answer> │ │
│ │ │ │
│ │ Return Code: │ │
│ │ • 0 = AC (Accepted) │ │
│ │ • Non-zero = WA (Wrong Answer) │ │
│ │ • stderr = Checker 訊息 (會回傳給使用者) │ │
│ └───────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
- 撰寫 Checker: 接收三個參數
input,user_output,answer。 - 上傳 Checker: 透過
/api/v1/checkers上傳。 - 使用 Checker: 提交評測時設定
use_checker=true與checker_name=你的checker檔名。
系統運作的紀錄對於除錯至關重要。
- 儲存位置: 專案根目錄下的
logs/資料夾。 - 持久化: 透過 Docker Volume 掛載 (
./logs:/sandbox/logs),即使容器刪除,Log 檔案也會保留在您的電腦上。 - 檔案格式:
sandbox.log: 當天的最新 Log。sandbox.log.YYYY-MM-DD: 歷史 Log (系統每天午夜自動切割並封存)。submission_history.jsonl: 詳細的評測歷史紀錄 (JSON Lines 格式)。auth_failures.jsonl: API Key 驗證失敗紀錄。
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ 系統 Log 記錄點總覽 │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ api_server.py: │
│ ├── INFO: 收到提交請求 (submission_id, problem_id, language, mode) │
│ ├── INFO: 題包更新完成 / 使用快取 │
│ ├── WARN: API Key 驗證失敗 (記錄 IP, UA, timestamp) │
│ ├── ERROR: 檔案儲存失敗 / Hash 驗證失敗 │
│ └── ERROR: 題包下載失敗 │
│ │
│ queue_manager.py: │
│ ├── INFO: Job 加入佇列 (submission_id, priority, queue_size) │
│ ├── INFO: Job 取出佇列 │
│ ├── WARN: 佇列已滿 (拒絕提交) │
│ └── INFO: 佇列暫停 / 恢復 / 清空 │
│ │
│ worker.py: │
│ ├── INFO: Worker 啟動 / 停止 (worker_id, box_id) │
│ ├── INFO: 開始處理 Job (submission_id) │
│ ├── INFO: Sidecar 啟動 / 停止 (image) │
│ ├── INFO: Proxy 啟動 / 停止 (port, whitelist) │
│ ├── INFO: 編譯結果 (CE/OK) │
│ ├── INFO: Job 詳細結果 (JSON: status, time, memory, tasks) │
│ ├── INFO: Final Verdict (status, time, memory) │
│ ├── INFO: Callback 發送成功 / 失敗 │
│ ├── ERROR: Worker 執行錯誤 (exc_info=True) │
│ └── ERROR: Cleanup 錯誤 │
│ │
│ isolate_runner.py: │
│ ├── INFO: Box 初始化 / 清理 │
│ ├── INFO: 檔案寫入沙盒 (file_count) │
│ ├── INFO: ZIP 解壓縮完成 │
│ ├── DEBUG: Isolate 執行命令 (完整命令列) │
│ ├── ERROR: Init 失敗 / 執行錯誤 (含 traceback) │
│ └── WARN: Security 警告 (允許不安全 syscall 時) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ 錯誤處理機制總覽 │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. API 層錯誤處理 (api_server.py) │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ ┌─────────────────┐ │ │
│ │ │ API Key 錯誤 │ ─▶ 403 Forbidden + Log 記錄 (auth_failures.jsonl) │ │
│ │ ├─────────────────┤ │ │
│ │ │ 參數驗證失敗 │ ─▶ 422 Validation Error (Pydantic 自動處理) │ │
│ │ ├─────────────────┤ │ │
│ │ │ Hash 不符 │ ─▶ 400 Bad Request (檔案完整性錯誤) │ │
│ │ ├─────────────────┤ │ │
│ │ │ 佇列已滿 │ ─▶ 503 Service Unavailable (可重試) │ │
│ │ ├─────────────────┤ │ │
│ │ │ 題包下載失敗 │ ─▶ 使用本地快取 (若有) 或 503 錯誤 │ │
│ │ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 2. Worker 層容錯 (worker.py) │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ ┌─────────────────┐ │ │
│ │ │ Sidecar 啟動失敗│ ─▶ 記錄錯誤, 回傳 status="XX", 跳過執行 │ │
│ │ ├─────────────────┤ │ │
│ │ │ 編譯失敗 │ ─▶ 記錄 stderr, 回傳 status="CE", 跳過測資執行 │ │
│ │ ├─────────────────┤ │ │
│ │ │ 執行超時 │ ─▶ Isolate 內建處理, 回傳 status="TO" │ │
│ │ ├─────────────────┤ │ │
│ │ │ 執行時錯誤 │ ─▶ 回傳 status="RE", 附帶 stderr 訊息 │ │
│ │ ├─────────────────┤ │ │
│ │ │ Callback 失敗 │ ─▶ 重試 3 次 (Backoff: 1s, 2s, 3s), 記錄失敗 │ │
│ │ ├─────────────────┤ │ │
│ │ │ 靜態分析失敗 │ ─▶ 不影響評測結果, 附加 error 訊息到 static_analysis │ │
│ │ ├─────────────────┤ │ │
│ │ │ 任意異常 │ ─▶ try-except 包覆, 記錄 exc_info, Worker 繼續運行 │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ │ ⚠️ 重要: Worker 不會因為單一 Job 錯誤而崩潰 │ │
│ │ 所有異常都被捕捉並記錄, Worker 會繼續處理下一個 Job │ │
│ └───────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 資源清理保證 (Finally Block) │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 無論執行成功或失敗, 都會執行: │ │
│ │ • Proxy 停止 & Log 收集 │ │
│ │ • Sidecar 容器停止 & Log 收集 (錯誤時) │ │
│ │ • 暫存檔案清理 (/tmp/sandbox/submissions/{id}) │ │
│ │ • Worker 狀態重設 (is_busy=False, current_job=None) │ │
│ └───────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 4. 系統級容錯 │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ • Docker restart: unless-stopped (容器異常退出自動重啟) │ │
│ │ • Worker Pool: 多個 Worker 並行, 單一 Worker 故障不影響整體 │ │
│ │ • Priority Queue: 持久化 (記憶體內, 但有 History 備份) │ │
│ │ • Log 輪替: TimedRotatingFileHandler (每日切割, 保留 30 天) │ │
│ └───────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
| 狀態碼 | 全稱 | 說明 |
|---|---|---|
AC |
Accepted | 答案正確 |
WA |
Wrong Answer | 答案錯誤 |
CE |
Compilation Error | 編譯錯誤 |
RE |
Runtime Error | 執行時錯誤 (Segfault, Exception 等) |
TO |
Time Out | 超過時間限制 |
SG |
Signal | 被信號終止 (如 SIGKILL) |
XX |
Internal Error | 系統內部錯誤 |
OK |
OK | 執行成功 (未驗證輸出時的狀態) |