資料庫模型
下列五張表都已註冊到 ORM metadata。其中四張(edge_workers、edge_worker_versions、edge_worker_outbox、edge_worker_audit_logs)自後端 5.10.11 起,欄位、索引、CHECK 與外鍵都沒有變動;edge_worker_maintenance 則是新增的。有變動的是寫入既有欄位的值集合:見下方 自 5.10.11 起新增的儲存值。
結構佈署:沒有 alembic migrations
alembic/versions/ 刻意不含這些表的任何 revision(任何其他東西的也沒有 — 在後端 5.22.0,該目錄仍只有一個 dummy 檔,alembic/ 底下也沒有任何東西提到 edge-worker 表)。結構經由 ORM metadata 出貨:Base.metadata.create_all 只建立缺少的表、永不 ALTER,edge_worker_maintenance 也是這樣建立的。模型檔自己的 docstring 就寫明政策:只編輯這個 ORM 檔;永遠不要手寫 alembic/versions revision。
實務後果:
- 除主鍵外,每個 NOT NULL 的非 TEXT 欄位都帶
server_default(字面值,時間戳記則是now()),滾動的 hotfix 才能對線上表加欄位。 - TEXT/BLOB 欄位(
sbom、build_receipt、error_message)沒有字面 server default — MySQL 無法給 TEXT 預設值。它們可為 null 純粹是為了混版滾動佈署安全;V2 對每個ready修訂版都要求兩份證據文件。
不要跑 alembic upgrade head 期待出現 edge-worker 表 — 這套結構不是那樣佈署的。
edge_workers — 部門單例
一個部門擁有的單一 Worker 的穩定身分(每部門每環境一列)。
| 欄位 | 型別 | Null | 預設 | 備註 |
|---|---|---|---|---|
id | String(36) | PK | — | 有索引、唯一 |
company_id | String(36) | 否 | "" | FK companies.id ON DELETE CASCADE,索引 |
department_id | String(36) | 否 | "" | FK departments.id ON DELETE CASCADE,索引 |
script_name | String(63) | 否 | "" | 推導(見下),絕非請求輸入 |
status | String(16) | 否 | "draft" | CHECK:draft / active / paused |
latest_revision | Integer | 否 | 0 | 每次插入修訂版前遞增,所以編號從 1 開始 |
deployment_sequence | BigInteger | 否 | 0 | 單調的准入配發器 |
current_version_id | String(36) | 可 | — | 沒有 FK |
created_by_user_id | String(36) | 否 | "" | |
created_at / updated_at | DateTime | 否 | now() | updated_at 另有 onupdate=now() |
約束:
uq_edge_worker_department— UNIQUE(department_id)。部門單例不變量;資料庫是併發首次提交的最終權威。uq_edge_worker_script_name— UNIQUE(script_name)。全域唯一的 Cloudflare script 名稱。ix_edge_worker_company_department— INDEX(company_id,department_id),雙錨定租戶讀取。ck_edge_worker_status— CHECKstatus IN ('draft','active','paused')。
script_name 是 "dw-" + sha256("edge-worker-v2\0{company_id}\0{department_id}")[:48] — 63 字元欄位裡的 51 字元,不透明(兩個 id 都不洩漏)、改名不變。
edge_worker_versions — 不可變修訂版
不可變的 V2 專案/建置修訂版,加上可變的部署證據。
| 欄位 | 型別 | Null | 預設 | 備註 |
|---|---|---|---|---|
id | String(36) | PK | — | |
company_id / department_id | String(36) | 否 | "" | 索引,無 FK |
worker_id | String(36) | 否 | "" | FK edge_workers.id CASCADE,索引 |
revision | Integer | 否 | 1 | |
language | String(16) | 否 | "javascript" | CHECK:javascript / typescript |
module_name / module_version | String(64) | 否 | "" | manifest 身分 |
entrypoint | String(256) | 否 | "" | |
compatibility_date | String(10) | 否 | "" | |
manifest | JSON | 可 | — | 僅正規化的已驗證 manifest;壓縮檔/bundle 在 R2 |
manifest_digest / contract_digest | String(64) | 否 | "" | |
project_object_key | String(1024) | 否 | "" | R2 key |
project_digest | String(64) | 否 | "" | |
project_size_bytes | Integer | 否 | 0 | |
lock_digest / dependency_digest | String(64) | 否 | "" | |
build_policy_version | String(64) | 否 | "" | 此修訂版建置時所用的 builder policy(新提交:department-worker-project-v2.10);部署規則以數值方式比較它,見版本與部署 |
bundle_object_key | String(1024) | 否 | "" | R2 key |
bundle_digest / sbom_digest / build_receipt_digest | String(64) | 否 | "" | |
sbom | TEXT(MySQL 上 MEDIUMTEXT) | 可 | — | builder 允許到 512 KiB;64 KiB 的 TEXT 不夠 |
build_receipt | TEXT(MySQL 上 MEDIUMTEXT) | 可 | — | |
state | String(24) | 否 | "uploading" | 見下方 CHECK |
error_code | String(64) | 否 | "" | |
error_message | Text | 可 | — | |
created_by_user_id | String(36) | 否 | "" | |
deployed_at | DateTime | 可 | — | |
created_at / updated_at | DateTime | 否 | now() |
約束:
uq_edge_worker_version_revision— UNIQUE(worker_id,revision):不可變修訂版階梯。ix_edge_worker_version_tenant— INDEX(company_id,department_id,worker_id)。ix_edge_worker_version_state— INDEX(company_id,department_id,state)。ck_edge_worker_version_language— CHECKlanguage IN ('javascript','typescript')。ck_edge_worker_version_state— CHECKstate IN ('uploading','ready','deploy_pending','deploying','deployed','superseded','failed','uncertain')。
uploading 預設是 fail-closed 哨兵值,不是公開提交階段:V2 路由一律以 ready 插入修訂版,任何忘記設 state 的插入路徑都會產生不可部署的列。狀態轉換見版本與部署。
edge_worker_outbox — 耐久意圖與公開票證
僅存純量的耐久部署、控制面稽核與清理意圖。ticket_id 就是 GET /worker/deployments/{ticket_id} 所輪詢的對象。
| 欄位 | 型別 | Null | 預設 | 備註 |
|---|---|---|---|---|
id | String(36) | PK | — | |
ticket_id | String(36) | 否 | "" | 索引、唯一 |
company_id / department_id / worker_id / version_id | String(36) | 否 | "" | 索引,無 FK |
dispatch_namespace | String(64) | 否 | "" | 索引;該列的 provider 部署範圍:雲端是 Worker 已記錄之服務位置的 namespace(Worker 的首次部署與每一筆產物清理意圖,預設取該環境的 EDGE_WORKERS_CLOUDFLARE_DISPATCH_NAMESPACE),地端是 onprem:<12 hex>。雲端的回收掃描認領共用位置加上自己環境的舊有位置,不再認領舊有的 "" 列(沒有可用的範圍設定時,什麼都不認領);地端的掃描只認領自己的 onprem:<12 hex> 範圍,絕不認領 "";屬於另一個環境之舊有位置的列,留給該環境處理。見背景任務 |
operation | String(16) | 否 | "deploy" | CHECK:deploy / delete / artifact_cleanup / control_audit |
dedupe_key | String(256) | 否 | "" | 部署列:deploy:{worker_id}:{version_id}:{base_revision},部署請求帶有 operation_settings 時會再附加 :{operation_settings_digest} |
admission_sequence | BigInteger | 否 | 0 | 准入時從 worker.deployment_sequence 複製 |
active_deployment_worker_id | String(36),生成且持久化 | 可 | — | CASE WHEN operation='deploy' AND status IN ('pending','leased','uncertain') THEN worker_id ELSE NULL END |
status | String(16) | 否 | "pending" | CHECK:pending / leased / completed / failed / cancelled / uncertain |
attempts | Integer | 否 | 0 | |
lease_owner | String(128) | 否 | "" | |
lease_expires_at / next_attempt_at / completed_at | DateTime | 可 | — | |
last_error_code | String(64) | 否 | "" | |
last_error | String(1024) | 否 | "" | |
payload | JSON | 可 | — | 僅純量的任務參數(部署 payload 帶有 operation_settings_digest,沒有設定時為 null,在雲端還帶有一筆伺服端擁有、記錄服務該 Worker 之位置的紀錄,沒有任何路由會回傳它);絕不含機密值、API 金鑰或服務帳號密碼 |
created_at / updated_at | DateTime | 否 | now() |
約束:
uq_edge_worker_outbox_dedupe— UNIQUE(operation,dedupe_key):冪等的意圖准入。uq_edge_worker_outbox_ticket— UNIQUE(ticket_id):公開票證身分。uq_edge_worker_outbox_active_deployment_worker— UNIQUE(active_deployment_worker_id):每個 Worker 至多一個存活部署。因 MySQL 唯一索引允許多個 NULL,作用等同部分唯一索引;已結算的列生成欄位為 NULL。ix_edge_worker_outbox_pending— INDEX(status,next_attempt_at):worker 迴圈掃描。ix_edge_worker_outbox_tenant— INDEX(company_id,department_id,status)。ix_edge_worker_outbox_worker_operation_sequence— INDEX(worker_id,operation,admission_sequence):最新部署查詢。ck_edge_worker_outbox_operation— CHECKoperation IN ('deploy','delete','artifact_cleanup','control_audit')。ck_edge_worker_outbox_status— CHECKstatus IN ('pending','leased','completed','failed','cancelled','uncertain')。
edge_worker_maintenance — resync epoch 與備份圍欄(地端)
每個部署範圍各有一份耐久的 resync epoch 與備份啟用圍欄。主鍵就是範圍,即 onprem:<12 hex>。地端 provider 會讀寫它(維護與還原閘門、備份圍欄、resync;見地端)。在雲端,這張表同其他表一樣由 create_all 建立,但從不被讀取:雲端閘門恆為開啟。
| 欄位 | 型別 | Null | 預設 | 備註 |
|---|---|---|---|---|
scope | String(64) | PK | — | 地端部署範圍 |
state | String(16) | 否 | "idle" | CHECK:idle / active |
epoch | String(64) | 否 | "" | |
started_by | String(64) | 否 | "" | |
started_at | DateTime | 可 | — | |
activation_fence_until | DateTime | 可 | — | |
edge_data_hold | Boolean | 否 | false | 還原保護 |
updated_at | DateTime | 否 | now() | 另有 onupdate=now() |
約束:ck_edge_worker_maintenance_state — CHECK state IN ('idle','active')。除主鍵外,沒有其他索引、沒有 FK、沒有唯一約束。地端若缺少該列,會失敗即關閉:部署與修復回 409 edge_workers_maintenance。
edge_worker_audit_logs — 只追加的事實
只追加、有界的稽核事實;永不含原始碼、bundle 或憑證(只存摘要:機密值、API 金鑰或服務帳號密碼絕不寫入,只寫入它的加鹽或未加鹽 SHA-256 摘要)。寫什麼、何時寫見稽核。
| 欄位 | 型別 | Null | 預設 |
|---|---|---|---|
id | String(36) | PK | — |
company_id / department_id | String(36) | 否 | ""(索引) |
actor_type | String(32) | 否 | "" |
actor_id | String(36) | 否 | "" |
auth_method | String(32) | 否 | "" |
action | String(128) | 否 | "" |
resource_type | String(64) | 否 | "" |
resource_id | String(36) | 否 | "" |
result | String(32) | 否 | "success" |
reason_code | String(64) | 否 | "" |
digest | String(64) | 否 | "" |
created_at | DateTime | 否 | now() |
索引:ix_edge_worker_audit_tenant_created(company_id, department_id, created_at)與 ix_edge_worker_audit_resource(resource_type, resource_id)。沒有唯一約束、沒有 FK、沒有 updated_at、沒有 CHECK — 冪等性(以 audit_id)只存在於應用層寫入器,它會截斷 actor_type(32)、auth_method(32)、action(128)、resource_type(64)、result(32)、reason_code(64)、digest(64),但不截斷 id 欄位。
自 5.10.11 起新增的儲存值
沒有任何欄位改變,但這些值現在會出現在既有欄位中。
| 欄位 | 新值 |
|---|---|
edge_worker_audit_logs.action | 操作設定、Worker 機密與服務帳號的動作,退役確認 department.worker_retired / department.worker_script_neutralized / department.worker_script_deleted / department.worker_tombstone_deleted(其 reason_code 為 dispatch namespace),secrets.purged_before_legacy_deploy,service_account.*;地端另有 edge_worker.resync。完整清單:稽核 |
edge_worker_outbox.last_error_code | department_deleted、edge_workers_provider_invalid、legacy_serving_target_unavailable(部署列、被復原輪判為 failed 的控制面稽核列,以及控制面稽核的回執確認)、deployment_target_unavailable、deployment_target_release_changed、worker_deleted(Worker 列已不存在的部署列)、worker_retired(因其 Worker 已退役而結算的控制面稽核列);地端另有 edge_host_fenced、其他 edge_host_* 代碼、edge_worker_deleted、edge_host_resync_failed |
edge_worker_versions.error_code | department_deleted、edge_workers_provider_invalid、legacy_serving_target_unavailable、deployment_target_unavailable、deployment_target_release_changed;地端另有 edge_host_fenced(error_message 為 activation deferred by backup fence)、edge_host_capacity_exhausted(以 host 的固定指引文字作為 error_message;其他所有終態部署失敗則存 deployment failed)、resync 代碼(error_message 為 deployment failed during Edge Worker resync,或 ... resync abort) |
edge_worker_outbox.dispatch_namespace | 地端 onprem:<12 hex> |
edge_worker_outbox.dedupe_key | 部署的 dedupe key 可能以 :<operation_settings_digest> 結尾 |
值得知道的怪癖
- 非 null String 欄位的通用哨兵是空字串而非 NULL;API 層在讀取時把
""轉成None。 - FK 只存在於
edge_workers(company/department)與edge_worker_versions.worker_id。outbox、audit 與 maintenance 列沒有 FK;edge_workers.current_version_id也沒有。 - 刪除部門不會刪除任何列。部門只是軟刪除,因此不會級聯:
edge_workers、修訂版、outbox 與 audit 列都保留,雲端 script 則另行退役(見退役)。公司的根層級硬刪除會移除部門列,這會級聯到edge_workers,並透過edge_worker_versions.worker_id再級聯到修訂版;outbox 與 audit 列則留存,是這個資料庫部署過哪些 script 的唯一證據,孤兒調解會用到它。Worker 列已不存在的部署列,會以worker_deleted結算為failed。 - outbox 的
deleteoperation 在 CHECK 裡但沒有任何存活程式路徑(沒有EVENT_*常數)— 純向前相容。 - 公司管理的「最新部署」排序最後以原始字串
id決勝,而admission_sequence = 0的舊列刻意排在有序號的之後。 - CRUD 寫入 helper(
create_submission、write_audit)flush 但永不 commit — 交易與 R2 補償編排由路由擁有。