jim800121chen 4d381c0b50 feat(task-scheduler): Phase 1 — modularize server + add OAuth/JWKS + /api/v1/* routes
Refactor server.js (647 → 99 lines) into 30+ modules under src/:
- auth/: JWKS validation, JWT middleware, OAuth client_credentials
- routes/v1/: jobs (POST/GET/:id) + promote with input validation
- routes/legacy.js: existing /jobs multipart path (backward compatible)
- services/: jobService, healthService, sseService, statusMapper,
  doneListener
- middleware/: requestId, errorHandler, perClientRateLimit,
  uploadConcurrency, upload (multer + storage)
- redis/: Lua scripts for atomic claim/release_active_job
- storage/: local + minio adapters; fileAccessAgent/: PUT promote client
- config.js: env var validation with fail-fast

Phase 1 features (T1–T11):
- T1 Auth middleware + JWKS (Member Center OAuth2 resource server)
- T2 OAuth client (Member Center client_credentials, Basic auth)
- T3 /api/v1/* router skeleton
- T4 server.js refactor (legacy endpoints fully preserved, real-Redis
  regression verified — existing worker consumer group untouched)
- T5 POST /api/v1/jobs (multipart, OWASP-audited, 2 Critical / 6 Major
  fixed; Risk-A/B documented as accepted)
- T6 GET /api/v1/jobs + GET /:id (cursor pagination, ETag, IDOR-safe)
- T7 POST /jobs/:id/promote (FAA PUT with own service token, 300s
  timeout, fail-fast on missing FAA URL)
- T8 /health upgrade (healthy/degraded/unhealthy + 30s background cache)
- T9 stage_timings (release_active_job in terminal states)
- T10 env + Docker integration (MULTIPART_* + concurrency limiter)
- T11 README (498 lines) + OpenAPI 3.0 spec (1588 lines)

Tests: 630 pass across 29 suites. Updated Dockerfile + .dockerignore +
docker-compose.yml env passthrough (no hardcoded secrets, fail-fast on
missing required vars).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:55:05 +08:00

20 KiB
Raw Blame History

Task Scheduler — Kneron Model Converter Phase 1

Kneron Model Converter 的 Job 管理與 queue orchestration 服務。負責接收上游 visionA-backend / Web UI的轉檔請求協調 ONNX → BIE → NEF pipeline並把成功 的結果檔 promote 到 File Access Agent / NAS 模型庫。

Phase 1 對外 API 完整規格 → 見 docs/openapi.yaml


1. 專案介紹

1.1 服務角色

                   public Internet                              internal
                                                                 ↓
visionA-backend ─→ Nginx (443, public vhost) ─→ /api/v1/* ─→ task-scheduler ─→ Worker
                                                              │
Web UI         ─→ Nginx (80,  internal vhost) ─→ /jobs    ──┘     │
                                                                  ↓
                                                            ONNX → BIE → NEF
                                                                  ↓
                                                              MinIO Bucket
                                                                  ↓
                                                  POST /api/v1/jobs/:id/promote
                                                                  ↓
                                                          File Access Agent
                                                                  ↓
                                                              NAS 模型庫

task-scheduler 是 Phase 1 唯一暴露給上游的應用層元件,承擔:

  • 對外 APIPhase 1 新增/api/v1/* 共 4 個端點 + 2 個 Phase 2 預留
  • 內部 API保留既有/jobs/* 共 6 個 legacy 端點Web UI 用)
  • 健康檢查:/health(公開)

1.2 技術堆疊

層級 技術 版本
執行環境 Node.js 18+ (alpine image, 部署用)
Web framework Express 4.x
Queue Redis Stream + ioredis 5.x
物件儲存 MinIOS3 compatibleAWS SDK v3 latest
認證 OAuth 2.0 + JWTjose jose 5.x
上傳 multer (memoryStorage) 1.4.x
速率限制 express-rate-limit 6.x
安全 headers helmet 7.x
測試 Jest 29.x

2. 前置需求

項目 版本 / 說明
Node.js 18+fetch 原生支援、duplex: 'half'
npm 9+
Docker / docker-compose可選 24.x+
Redis 7.xdev / prod 都需要)
MinIO latestPOST /api/v1/jobs 必須啟用)
Member Center OAuth 2.0 Authorization Server提供 JWKS / token endpoint
File Access Agent promote 階段呼叫,需支援 PUT /files/{key}

dev 環境若無真實 Member Center / FAA可用 placeholder 值(見 env.example)。


3. 啟動方式

3.1 本機開發(純 Node

cd apps/task-scheduler
cp env.example .env
# 編輯 .env至少把以下 placeholder 替換為真實值:
#   - MEMBER_CENTER_*(若要實際打 Member Center
#   - KNERON_CONVERTER_CLIENT_SECRET
#   - MINIO_*(若 STORAGE_BACKEND=minio

npm install
npm start
# → 監聽 PORT預設 4000

3.2 Docker 單體

docker build -t task-scheduler:dev apps/task-scheduler
docker run --rm --env-file apps/task-scheduler/.env -p 4000:4000 task-scheduler:dev

3.3 docker-compose推薦

專案根目錄已有 docker-compose.yml,會一併啟動 Redis、MinIO、Workers、frontend

cd /path/to/kneron_model_converter
cp apps/task-scheduler/env.example .env  # 或維護一份 root .env
docker compose up -d --build

服務埠對外:

  • Scheduler APIhttp://localhost:4000
  • Web UIhttp://localhost:3000
  • MinIO Consolehttp://localhost:9001

3.4 Health check

curl http://localhost:4000/health | jq .

回應為三層 statushealthy / degraded / unhealthy+ 各依賴狀態, 詳見 § 7. 監控

3.5 Graceful shutdown

服務監聽 SIGTERM / SIGINT:收到後會先停掉 health background polling 再讓 Express 自然關閉。容器 / K8s 部署時 terminationGracePeriodSeconds 建議至少 30 秒。


4. 專案結構

apps/task-scheduler/
├── server.js                      ← entry< 140 行;組裝 deps、啟動 listener、listen
├── src/
│   ├── app.js                     ← Express app factory
│   ├── config.js                  ← 集中讀 env啟動時 fail-fast
│   ├── redis.js                   ← Redis client + helpers
│   ├── auth/
│   │   ├── jwks.js                ← jose remote JWKS cache + jwtVerify
│   │   ├── middleware.js          ← requireAuth(scope) Express middleware
│   │   └── oauthClient.js         ← Converter as OAuth Clientclient_credentials
│   ├── fileAccessAgent/
│   │   ├── client.js              ← FAA HTTP clientPUT only重試 + 401 invalidate
│   │   └── errors.js
│   ├── middleware/
│   │   ├── errorHandler.js        ← 統一 error 格式v1 限定)
│   │   ├── requestId.js           ← X-Request-Id 透傳 / 生成
│   │   ├── perClientRateLimit.js  ← per-client_id rate limiter
│   │   ├── upload.js              ← multer 設定
│   │   └── uploadConcurrency.js   ← per-process upload semaphore防 OOM
│   ├── routes/
│   │   ├── legacy.js              ← /jobs* 6 個端點Web UI 用)
│   │   └── v1/
│   │       ├── index.js           ← /api/v1 mount + 內部 errorHandler
│   │       ├── jobs.js            ← POST/GET /jobs, GET /jobs/:id, 預留 501
│   │       ├── promote.js         ← POST /jobs/:id/promote
│   │       └── validators/
│   │           └── createJob.js   ← multipart fields validator
│   ├── services/
│   │   ├── jobService.js          ← Job CRUD + claim_active / advance / fail
│   │   ├── doneListener.js        ← Redis Stream 背景 listener
│   │   ├── healthService.js       ← /health 背景 polling cache
│   │   ├── statusMapper.js        ← 內部大寫 status → 對外 status + stage
│   │   └── sseService.js          ← SSE 推送legacy
│   ├── storage/
│   │   ├── minio.js               ← AWS SDK v3 S3 facade
│   │   └── local.js               ← STORAGE_BACKEND=local 模式
│   ├── redis/
│   │   └── luaScripts.js          ← claim_active_job / release_active_job
│   └── utils/
│       └── sanitize.js            ← filename / user_id / path 安全處理
├── docs/
│   └── openapi.yaml               ← Phase 1 對外 API spec給 visionA 等消費者)
├── tests/                         ← 單元 + 整合測試(見 src/**/__tests__/
├── package.json
├── Dockerfile                     ← 多層快取 + 非 root user + HEALTHCHECK
├── env.example                    ← 完整環境變數範本(不含真實 secret
└── README.md                      ← 本檔

5. 環境變數

完整清單(含預設、必填與否、說明)見 env.example

簡表(依分類):

5.1 必填(缺漏會 fail-fast、process exit code 1

變數 用途
REDIS_URL Redis 連線(含 password
STORAGE_BACKEND local / minioPOST /api/v1/jobs 必須 minio
MEMBER_CENTER_ISSUER JWT iss 比對基準
MEMBER_CENTER_JWKS_URL JWKS endpoint驗 token 用)
MEMBER_CENTER_TOKEN_URL token endpoint取 promote 用 token
KNERON_CONVERTER_AUDIENCE 接受 JWT 的 aud
KNERON_CONVERTER_CLIENT_ID Converter 自己 OAuth client
KNERON_CONVERTER_CLIENT_SECRET 不要進 git用 secret manager
FILE_ACCESS_AGENT_BASE_URL promote 目標production 強制 https
FILE_ACCESS_AGENT_AUDIENCE promote token 的 aud

STORAGE_BACKEND=minio 時還需:MINIO_ENDPOINT_URL / MINIO_BUCKET / MINIO_ACCESS_KEY / MINIO_SECRET_KEY

5.2 可選(有合理預設)

涵蓋:

  • 上傳上限(MULTIPART_MODEL_MAX_BYTES 預設 500MB、MULTIPART_REF_IMAGE_MAX_BYTES 預設 10MB、MULTIPART_REF_IMAGES_MAX_COUNT 預設 100
  • 上傳並發(MAX_CONCURRENT_UPLOADS 預設 5、UPLOAD_RETRY_AFTER_SECONDS 預設 30
  • Rate limitAPI_V1_RATE_LIMIT_WINDOW_MS 預設 5min、API_V1_RATE_LIMIT_MAX 預設 300
  • JWKS 行為(JWKS_CACHE_MAX_AGE_MSJWKS_COOLDOWN_MSJWT_CLOCK_TOLERANCE_SEC
  • OAuth clientOAUTH_TOKEN_REFRESH_SKEW_MSOAUTH_TOKEN_TIMEOUT_MS
  • promote timeoutPROMOTE_TIMEOUT_MS 預設 300s
  • Tenant 隔離(CONVERTER_TENANT_ID,空字串 = 不檢查)
  • Scope 命名覆寫(CONVERTER_SCOPE_WRITE / CONVERTER_SCOPE_READ

5.3 安全提醒

  • .env 已在 .gitignore;不要 commit
  • production 用 secret managerVault / AWS Secrets Manager / K8s Secret 而不是把 secret 直接放進 docker-compose env
  • 任何含 REPLACE-ME 字樣或 .invalid TLD 的 placeholder部署前必須替換

6. API 概覽

6.1 Phase 1 對外 API/api/v1/*

方法 路徑 scope 說明
POST /api/v1/jobs converter:job.write 建立轉檔 jobmultipart
GET /api/v1/jobs converter:job.read Recovery 列表user_id 必填)
GET /api/v1/jobs/:id converter:job.read 單一 job 狀態(含 ETag
POST /api/v1/jobs/:id/promote converter:job.write 結果檔搬到 FAA
POST /api/v1/jobs/:id/download-tokens converter:job.read Phase 2 預留,回 501
DELETE /api/v1/jobs/:id converter:job.write Phase 2 預留,回 501

完整規格、所有 schema、所有錯誤情境的 exampledocs/openapi.yaml

6.2 Legacy / 內部 API/jobs/*,僅內網 vhost 暴露)

對 Web UI 100% 不變更行為T4 重構僅是「移動 + 抽象」):

方法 路徑 說明
POST /jobs Web UI 上傳建 jobmultipart無 user_id 概念)
GET /jobs 列出全部 joblegacy KEYS scan
GET /jobs/:jobId 查單一 job
GET /jobs/:jobId/events SSE 推送
GET /jobs/:jobId/download/:filename 下載結果檔
GET /queues/stats Redis Stream / Group 統計

6.3 健康檢查

方法 路徑 說明
GET /health 公開,不需認證

7. Auth 流程

7.1 上游消費者visionA-backend取 token

Converter 是 OAuth 2.0 Resource Server。建議消費者用 client_credentials grant 從 Member Center 取得 service-to-service token

POST {member-center}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<your-client>
&client_secret=<your-secret>
&scope=converter:job.write converter:job.read
&audience=kneron_converter_api

7.2 Converter 端驗證

每個 /api/v1/* request 進入時:

  1. Bearer token 驗章(jose.createRemoteJWKSet + jwtVerify
  2. iss / aud / exp(含 60 秒 clock skew
  3. scope(端點要求的 scope 必須在 token claim 內)
  4. tenant_id(若 CONVERTER_TENANT_ID 非空則檢查)
  5. client_id(用於 rate limit / log / job 隔離)

驗證失敗時:

  • 回 v1 標準錯誤格式({error: {code, message, details, request_id}}
  • Connection: close header + req.socket.destroy():阻止 unauthorized client 繼續灌大檔。但這是 best-effort真正的 body 上限 靠 Nginx client_max_body_size(部署層)

7.3 Converter 取 promote 用 token

promote 時 Converter 切換成 OAuth Clientclient_credentialsfiles:upload.write scope tokenPUT 到 FAA。

token cache per scope過期前 60s 主動 refreshFAA 回 401 時自動 invalidate cache 並重試一次。


8. 錯誤碼總表

HTTP code 說明
400 validation_error 欄位格式錯(details.fields[] 列具體欄位)
400 invalid_multipart multipart parse 失敗、缺必要 file、副檔名不符
401 invalid_token JWT 無效 / 簽章錯 / 缺 claim
401 token_expired JWT 過期
403 insufficient_scope scope 不足(details.required_scope / provided_scopes
403 tenant_mismatch tenant_id 不符
404 job_not_found job 不存在或不屬於該 client不洩漏存在性
404 not_found 路徑不存在
409 user_has_active_job 同 user 已有未完成 jobdetails.active_job_*
409 job_not_ready_for_promote promote 時 job 非 completed
409 source_not_available promote 的 source stage 沒產出
413 file_too_large 上傳超過大小上限model 500MB / ref_image 10MB
422 invalid_object_key promote target_object_key 格式不合法
429 rate_limit_exceeded per-client rate limit
500 misconfiguration 伺服器設定錯(如 STORAGE_BACKEND 非 minio
500 internal_error 其他未分類錯誤
501 not_implemented Phase 2 預留端點
502 storage_unavailable MinIO 寫入失敗
502 file_gateway_unavailable FAA 不可用 / 拒絕
503 auth_service_unavailable Member Center 取 token 失敗
503 service_busy upload concurrency 已滿(Retry-After header

response 完整 schema 見 docs/openapi.yaml


9. 與其他服務的關係

服務 連接方式 用途 失敗影響
Member Center HTTPS 驗 visionA token / 取 promote token 新 token 無法驗cache 內舊 token 仍可用promote 階段失敗
File Access Agent HTTPS promote 結果檔搬到 NAS promote 失敗,但 job 本身已 completed可重試
MinIO HTTP / HTTPS 原始模型 / 結果檔暫存7 天 lifecycle POST /jobs 直接 502promote 也會失敗
Redis TCP Job state、active_job lock、Stream queue 整個服務 unhealthy
Workeronnx / bie / nef Redis Stream 跑 pipeline Job 卡在某個 stageTTL 7 天會自動清

10. 監控

10.1 /health 的三層 status

status HTTP 對應狀態
healthy 200 Redis / MC / FAA 都連通
degraded 200 Redis 連通,但 MC / FAA 任一不可達
unhealthy 503 Redis 斷線

response body 同時包含 dependencies.{redis, member_center, file_access_agent} 細節,可給 K8s readiness / liveness probe 區分嚴重度。

10.2 結構化日誌

所有 v1 路徑的 handler 都輸出 JSON logstdout

{
  "service": "task-scheduler",
  "timestamp": "2026-04-25T12:00:00.123Z",
  "level": "INFO",
  "action": "jobs.create.success",
  "request_id": "7c6e4f3b-...",
  "job_id": "550e8400-...",
  "user_id": "alice",
  "client_id": "kneron_converter_dev",
  "size_bytes": 204800000,
  "ref_images_count": 0,
  "duration_ms": 234
}

action 欄位採 domain.event 格式,便於用 jq / loki 過濾。

10.3 Rate limit headers

回應自動帶:

  • X-RateLimit-Limit / RateLimit-Limit
  • X-RateLimit-Remaining / RateLimit-Remaining
  • 超限時:Retry-After(秒)

11. Phase 1 已知接受風險

本節為摘要,完整內容見 .autoflow/04-architecture/security.md

11.1 user_id 信任邊界(最重要)

  • user_id 來自 multipart form fieldPOST或 query stringGET 從 JWT claim derive
  • Converter 完全信任 visionA-backend 帶來的 user_id 是對的,不做 user 層級 ACL
  • visionA-backend 一旦被 compromiseattacker 可冒充任何 user_id

Phase 1 接受此風險的理由

  1. visionA-backend 是內部受控系統,非 Internet-facing
  2. Phase 1 重點是 pipeline 跑通;安全強化排在 Phase 2
  3. HMAC / OBO 流程要 visionA / Member Center 配合,已對齊但尚未實作

Phase 1 mitigation

  • per-client_id rate limit300 req / 5 min
  • 結構化 audit log 含 client_id + user_id
  • 7 天 active_job TTL避免 lock 永久不釋放)
  • user_id 嚴格白名單(^[A-Za-z0-9._-]{1,128}$)擋 XSS / Redis key injection

Phase 2 候補HMAC-signed user_id短期/ OAuth Token Exchange中期

11.2 大檔上傳的 OOM 風險

  • multer 用 memoryStorage — 每個並發 upload 吃 model size 大小的 heap
  • 5 並發 × 500MB = 2.5GBMAX_CONCURRENT_UPLOADS 預設 54GB 容器安全)
  • 超過時 503 + Retry-Afterclient 主動 backoff

11.3 Trust boundary 與 Nginx 層

  • 401/403 後 server 雖會 socket.destroy(),但這是 best-effort
  • 真正的 body 大小上限由 Nginx vhost client_max_body_size 600M 把關
  • Nginx 雙 vhost 設定詳見 TDD §7.1DevOps 範圍,非後端)

11.4 Per-process statePhase 2 才需處理)

  • rate limiter / upload concurrency 都是 in-process counter
  • Phase 1 部署為單 instance無問題Phase 2 多 instance 時要改 Redis store

12. 測試

npm test               # 跑所有 unit + integration test630 tests~4 秒)
npm test -- --watch    # watch 模式
npm test -- src/auth   # 只跑 auth 模組的測試

測試金字塔:

  • 單元測試70%service / validator / utils / middleware
  • 整合測試20%route + middleware + Redis 模擬 / FAA mock
  • E2E10%):由 Testing Agent 跑(不在本套件內)

CI 用:npm test


13. 故障排除(常見場景)

症狀 可能原因 排查
啟動立刻 exit 1 env 缺漏 [Scheduler] Config validation failed log對照 env.example
401 invalid_token / token_expired clock skew、JWKS cache 沒拿到新 kid 檢查 server 時鐘、MEMBER_CENTER_JWKS_URL 可達性
401 後 client 連線立刻斷 設計如此(Connection: close + socket.destroy() 正常行為,避免 client 繼續灌 body
409 user_has_active_job 但前一個 job 已 failed active_job lock 沒被釋放 看 worker done listener 是否運作;最壞情況 7 天 TTL 會自動清
502 storage_unavailable MinIO 不可達 / 認證錯 檢查 MINIO_* env、bucket 是否存在
502 file_gateway_unavailable FAA 5xx 或 4xx 拒絕(非 401 看 server log promote.faa_put_failedFAA 端排查
503 auth_service_unavailable Member Center token endpoint 死 / 401 兩次 確認 MEMBER_CENTER_TOKEN_URL 可達、KNERON_CONVERTER_CLIENT_*
503 service_busy + Retry-After upload concurrency 已滿 等 Retry-After或調高 MAX_CONCURRENT_UPLOADS(注意 OOM
503 unhealthy/health Redis 斷線 檢查 REDIS_URL 與 Redis 服務狀態
GET /jobs 回 400 missing user_id Phase 1 強制 user_id 必填 client 端帶 user_id query string
大檔上傳跑到一半 5xx Nginx client_max_body_size 太小 部署層調 client_max_body_size 600M(不在 backend 範圍)

更多細節:

  • .autoflow/04-architecture/TDD.md(完整規格)
  • .autoflow/04-architecture/security.md(安全模型 / 接受風險)
  • .autoflow/05-implementation/tasks-phase1.md(任務拆分與決策紀錄)

14. 文件參照

文件 內容
docs/openapi.yaml Phase 1 對外 API spec給 visionA-backend 等消費者 import
env.example 完整環境變數清單(含說明、預設、必填與否)
../../.autoflow/04-architecture/TDD.md 完整技術設計文件
../../.autoflow/04-architecture/security.md 安全模型 / 接受風險 / Phase 2 候補
../../.autoflow/04-architecture/design-doc.md 架構決策(為什麼選這些方案)
../../.autoflow/02-prd/PRD.md 產品需求 / user stories
../../.autoflow/05-implementation/tasks-phase1.md T1-T11 任務拆分與審查紀錄

15. License

MIT