版本與部署
本頁描述雲端 provider(EDGE_WORKERS_PROVIDER=cloudflare,預設值)。地端 provider 有差異之處只用一行說明,維運細節見地端。
修訂版階梯
每次被接受的提交建立一筆不可變的 edge_worker_versions 列:state="ready"、revision = worker.latest_revision + 1、以 (worker_id, revision) 唯一。版本的專案、bundle 與證據在建立後永不改變;部署只改它的 state、error_* 與 deployed_at。
ready ──deploy admission──▶ deploy_pending ──pre-provider fence──▶ deploying
│
┌───────────────┬───────────────┼────────────────┐
▼ ▼ ▼ ▼
deployed failed uncertain (back to deploy_pending
(success) (terminal, (ambiguous, on a retryable result)
provider Worker paused)
untouched)
previous current version ──▶ superseded (same settlement transaction)- 沒有回滾。
deployed或superseded的修訂版永遠不能再部署。完全相同的部署重試若發現其完成票證已不再是current_version_id,會得到 409 “Worker version was superseded; V2 rollback is not supported”。 uploading只是 fail-closed 的資料庫預設;API 永遠不會產生它。- 不能降級到操作設定之前。 一旦以 builder policy
department-worker-project-v2.8或更新版本建置的 release 上線,以較舊 builder policy 建置的 release 就完全無法部署(見下方 Release 支援與降級規則)。
部署准入是雙重單一併發
POST /worker/deployments 先讓請求經過路由,再經過部署 outbox。
路由,依序:
- provider 檢查:
EDGE_WORKERS_PROVIDER不合法(或地端 provider 未安裝)→ 503Edge Worker provider is not configured。同一個檢查也把關POST /worker/versions與公司修復路由。 - 先查完全相同的重試,再暫存任何東西:同一個
(version_id, base_revision)與同一個設定摘要的票證若已存在,會直接回傳(202),不動設定;見下方 重試與設定摘要。對於新的請求,路由接著會檢查能否驗證 Worker 的服務位置,否則 409{"code":"worker_serving_target_unavailable","message":"Worker deployment target cannot be verified"},且不暫存任何東西。 - 設定鎖:每個部門一把 Redis 鎖(TTL 600 秒),讓部署與操作設定寫入序列化。被另一個請求持有 → 409
{"code":"deployment_in_progress","message":"Another Worker settings mutation or deployment is in progress"};Redis 無法使用 → 503Operation-settings mutation lock is unavailable。 - 在途票證檢查:該部門任何不是
completed、failed或cancelled的部署票證(也就是pending、leased與uncertain)→ 409{"code":"deployment_in_progress","message":"Operation settings cannot change during a deployment"}。即使請求沒有帶設定,訊息仍提到設定。現在第二個部署請求通常拿到的就是這個帶 code 的 409;舊的純字串a deployment is already active for this department Worker只留給闖過這道檢查的競態(下方部署 outbox 的第 4 步)。用戶端必須讀detail.code,不要比對舊字串。 - 操作設定暫存,涉及設定時才執行:見下方 部署中的操作設定。
- 部署 outbox。
部署 outbox:
- 僅地端:維護閘門。resync epoch 進行中(或維護資料列不存在)→ 409
edge_workers_maintenance;還原保護開啟中 → 409edge_workers_restore_pending。雲端的閘門永遠開啟,所以這兩個 409 在雲端絕不會出現。 FOR UPDATE鎖定 Worker(並在鎖定下重新讀取)。服務位置必須仍可驗證(否則 409 “Worker deployment target cannot be verified”),且 Worker 不得有尚未結束的搬移(否則 409worker_migration_in_progress,搬移 Worker);接著要求base_revision == worker.latest_revision,否則 409 “Worker revision changed; refresh before deploying”。- 查詢同一個 Worker 與版本既有的部署列。
dedupe_key與本次請求不同的 → 409 “operation-settings digest differs from the existing deployment”;相同的則作為既有票證回傳(completed票證但其版本已不是 current → 409 “Worker version was superseded; V2 rollback is not supported”;uncertain的 → 409 “uncertain deployment requires company reconciliation”)。 - Worker 的任何修訂版處於
deploy_pending/deploying/uncertain→ 409 “a deployment is already active for this department Worker”;目標版本不是ready→ 409Worker version cannot deploy from state '<state>';任何尚未釋放的 outbox 部署列 → 同一條「already active」字串。 - 配發
worker.deployment_sequence + 1並複製到 outbox 列的admission_sequence(在 BigInteger 上限耗盡 → 409)。 - 插入 outbox 列(在雲端,此環境無法使用的服務位置會在這一步被擋下:409 “Worker deployment target is unavailable”):
operation="deploy"、status="pending"、ticket_id(公開)、dedupe_key = deploy:{worker_id}:{version_id}:{base_revision},請求帶有operation_settings時再附加:{operation_settings_digest}(依 operation 唯一 — 完全相同的重試回同一票證),payload 為純量{outbox_id, company_id, department_id, message_id, worker_id, version_id, base_revision, operation_settings_digest}(沒有設定時摘要為null),在雲端另外加上一筆由伺服器端持有的紀錄,記下服務該 Worker 的位置(以及它是從哪個位置部署而來),沒有任何路由會回傳它。 - 設定
version.state = "deploy_pending"。
僅地端:edge host 的執行閘門被備份圍欄關閉時,該列仍會建立(回應為 202),但其 last_error_code 為 edge_host_fenced 且不發佈任何東西;在圍欄結束被確認之前,票證讀作 status: "pending"、error_code: "edge_host_fenced"。
兩道獨立防線確保單一併發:上面的應用層檢查,加上 schema 層的生成欄位 active_deployment_worker_id(operation='deploy' AND status IN ('pending','leased','uncertain') 時等於 worker_id,否則 NULL)配唯一索引 — 舊版程式也造不出第二筆存活的部署列。
耐久列在 transport 發佈之前先提交。ADMISSION_RELEASE_STATUSES = {completed, failed, cancelled} — uncertain 刻意保留圍欄,所以在不確定結果被調解之前,該部門不能再部署任何東西。
重試與設定摘要
請求的設定摘要是 operation_settings 物件正規化 JSON 的 SHA-256(服務帳號欄位的明確 null 與省略會被區分);請求本體沒有 operation_settings 時則無摘要。
- 同一個
(version_id, base_revision)與同一個摘要 → 既有票證,202,不會再暫存任何東西。 - 同一個
(version_id, base_revision)但設定不同(或有設定對上沒有設定),且票證已存在 → 409{"code":"operation_settings_dedupe_mismatch","message":"Deployment settings differ from the existing ticket"}。在只有 outbox 看得到的競態中,同樣的條件讀作 409 “operation-settings digest differs from the existing deployment”。 - 既有票證的版本已被取代,或票證是
uncertain,維持各自的 409(字串見上方部署 outbox 的第 3 步)。
部署中的操作設定
部署請求本體可以帶 operation_settings,它與 PUT /worker/operation-settings 接受的是同一個物件(base_revision、選填的 default_service_account_id、operations)。它會在 provider 能服務目標 bundle 之前,以目標 release 的 generation 暫存進 Worker 的 Durable Object。稽核事實為 operation_settings.staged(稽核)。撰寫設定:管理逐操作存取;概念:逐操作存取。
- 部署時,
operations可以只列出有變更的操作:列出的列覆蓋上線中的列;兩個 release 都有、但沒被列出的操作,保留其完整的上線中列(存取、金鑰摘要與擁有權、服務帳號綁定);不在目標 manifest 中的操作則被捨棄。列出的 id 若不在目標 manifest 中 → 422,帶type: "operation_not_found"與msg: "operation_id is not present in the target release manifest"。 operation_settings.base_revision必須等於上線中的設定修訂版,否則 409 “Operation-settings revision is stale”。- 沒有
operation_settings時,持有非 legacy 設定表的上線中 release 會自動帶過去:共有的操作 id 保留各自的列,並保留上線中的預設服務帳號。 - 服務帳號欄位遵循 PUT 的規則(省略 = 保留,
null= 清除);無法使用的帳號為 422service_account_not_found。見服務帳號。
Release 支援與降級規則
上線中的 release 決定一次部署可以做什麼;各 builder policy 的能力對照表在提交管線。下列全部是 409(另有註明者除外),detail = {code, message}:
| 情況 | code/訊息 |
|---|---|
送出了 operation_settings,但 Worker 沒有上線中的 release(從未部署,或不是 active) | operation_settings_require_supported_release — Deploy the Worker once before adding operation settings |
送出了 operation_settings,上線中的 release 早於 v2.8 | operation_settings_require_supported_release — The live release does not support operation settings |
| 上線中的 release 為 v2.8 或更新,目標早於 v2.8 — 即使沒有設定也拒絕 | operation_settings_require_supported_release — The target release does not support operation settings |
上線中的設定表為 pending(上線中的 release 還沒有自己的設定表) | operation_settings_pending — Reconcile the live release before deploying settings |
| 上線中設定的讀取失敗兩次(503) | operation_settings_live_read_unavailable — Retry the deployment after the live settings read recovers |
| 上線中的 release 已送達服務帳號憑證,目標早於 v2.9 | service_account_credentials_require_supported_release — Unbind every service account (PUT operation-settings) before deploying a release without service-account support |
| 最終得到的設定表(列出的或帶過去的)引用了服務帳號,但目標或上線中的 release 早於 v2.9 | service_account_credentials_require_supported_release — The target release(或 live release) does not support service-account bindings; re-upload and deploy the project first |
| 設定引用了服務帳號,而部署環境的 API base 尚未設定(503) | api_base_unconfigured |
針對「release does not support」的修法永遠是:重新上傳專案(它會以目前的 builder policy 重建)並部署新的修訂版。Deployed department Worker release is unavailable(503)表示無法推導出上線中或目標 release 的身分。
舊版部署會清除機密。 把以早於 department-worker-project-v2.6(沒有 Worker 機密)的 builder policy 建置的 release,部署到 v2.6 或更新的上線中 release 之上時,會先透過上線中 release 的控制通道清除每一個 Worker 機密,不做存在檢查;部署任務在 deploying 圍欄之後、上傳之前做這件事,並寫入稽核事實 secrets.purged_before_legacy_deploy。移到較新的修訂版之後,機密必須重新寫入。見 Worker 機密。
部署任務實際做的事
部署任務(常數與完整分支見背景任務):
- Redis 投遞認領(諮詢性、30 秒、owner-fenced、有心跳)合併重複的 transport 投遞;耐久權威是 DB 租約(90 秒,每 30 秒心跳,owner token
owner:uuid4)。provider 在取得租約之前就已解析;若其設定不合法,任務一取得租約就結算票證:failed,帶edge_workers_provider_invalid(版本已是deploying時為uncertain)。僅地端:執行閘門關閉時,任務延後,且不結算任何東西。 - 部門檢查(僅雲端,GET-only 調解中不做):在取得租約之後、讀取或上傳任何東西之前,部門資料列不存在或已軟刪除,就把票證結算為
failed,error_code: "department_deleted"— 版本變成failed(error_message為deployment failed),Worker 回到active(若有已部署版本)或draft,並以該reason_code稽核deployment.failed。現在上傳會讓已刪除部門的 Worker 復活;見退役。 - 先做完整性驗證 — manifest 完整性、持久化的 module tuple、全部 7 個 64-hex 摘要、回執鍵集合恰為
{builderPolicy, bundleDigest, dependencyDigest, lockDigest, manifestDigest, projectDigest, releaseGeneration, receiptVersion, sbom, sbomDigest, staticData, staticDataDigest}且receiptVersion="1"、重新計算releaseGeneration,然後從 R2 讀回兩個產物做大小 + SHA-256 檢查(project ≤ 2 MiB、bundle ≤ 5 MiB)。部署永不重建、永不讀 npm。 - 上 provider 前的圍欄:單一交易 — 重新驗證租約 + bundle 身分 +
state == "deploy_pending",然後version.state = "deploying"且同交易worker.status = "paused"。暫停會在 provider 可能觀察到混合狀態之前擋掉新的呼叫准入;已被准入的舊 generation 請求仍可打到對應的舊 bundle 完成。 - 舊版部署的機密清除,在上述規則適用時執行。清除未獲確認,或 Worker 的身分在此期間改變,部署以
failed結束,帶artifact_integrity_failed;閘道錯誤則以failed結束,帶deployment_internal_error。兩種情況下都尚未觸及 provider。 - provider 上傳(雲端上是 Cloudflare PUT)
.../workers/dispatch/namespaces/{ns}/scripts/{script_name}— multipart metadata 釘死main_module: "index.mjs"、compatibility date、恰好一個 Durable Object binding(TEAMSYNC_DEPARTMENT_STORAGE→TeamSyncDepartmentStorage)、宣告式 SQLite DO export,以及一個 tagrelease-<provider_release_digest>。每次都送完整期望狀態;不用keep_bindings。 - PUT 成功不等於結算。 回讀同時比對
GET .../settings(binding 的權威)與 Worker Details(持久化的 live exports):compatibility date 相等、compatibility flags 為空、恰好一個精確 binding、恰好一個 SQLite DO export(Cloudflare 可選的state欄位:缺席與"created"等價,其他都是漂移)、兩端各恰好一個相同的release-…tag。 - 回讀吻合也還不等於結算 — release 必須能回應。 任務透過簽章控制通道對上傳的 release 送出唯讀的
access_policy.get指令(精確的 release generation,斷言 subject 為system:edge-worker-deploy),依 600 秒輪詢截止點重試(後端 5.22.0 之前為 60 秒)、退避 1 → 2 → 4 → 5 秒(之後維持 5 秒),期間租約心跳持續運作;每次探測之前與之後,它還會重新驗證自己的 DB 租約,若另一次投遞現在持有該票證就立刻停止。截止點之後不會再啟動任何探測,每次探測在 40 秒(閘道的 5 秒連線加 35 秒讀取預算)或到達截止點時被中斷,以先到者為準,因此這段等待大約在回讀吻合後 600 秒結束。等待期間票證讀作leased或deploying,版本為deploying,Worker 維持paused(閘道通道的呼叫得到 404Department Worker not found);在這段等待內回應的 release,不論花了幾分鐘,都會結算為completed,更慢的仍以uncertain收場。只有某次探測取得政策回應才會:前一個 current 版本 →superseded、本版本 →deployed、更新worker.current_version_id、worker.status = "active"、票證completed。輪詢用盡截止點後仍始終沒有回應的 release 以worker_readiness_timeout進入uncertain(Worker 維持暫停),永遠不會回報completed。這道關卡涵蓋所有可能結算completed的路徑:全新上傳、模糊上傳後的 GET-only 回讀,以及 GET-only 調解。
provider_release_digest = sha256("teamsync-edge-worker-release\0" + canonical JSON{bindings, bundle_digest, compatibility_date, exports, main_module, version: 2})。
重試、終態、不確定 — 精確規則
- 只有 429 會撤回圍欄。 PUT 收到 Cloudflare 429 是被證明的拒絕:在鎖定的租約下,任務把版本退回
deploy_pending、恢復 worker 上次結算的狀態,允許第二次 PUT 且不必先做 settings GET。租約取得總數上限 2 次(_MAX_DEPLOY_ATTEMPTS = 2);第二次 429 安全地失敗。 - 408、409、3xx、5xx、逾時、格式錯誤的確認都是 uncertain,不是可越過圍欄的重試 — 「conflict」可能在 provider 已開始處理上傳之後才回來。任務接著嘗試 GET-only 回讀:完全吻合 → readiness 檢查,然後 completed;可證明的不吻合 →
uncertain(provider_bundle_mismatch);讀不到 →uncertain(provider_settlement_lost/cloudflare_upload_uncertain)。 - uncertain 一律暫停 Worker 並保留准入圍欄。復原嘗試是 GET-only(任務看到
state == "deploying"就永不重新 PUT)。 - 上 provider 前的確定失敗(完整性、設定、非 408/409 的 4xx、
department_deleted、edge_workers_provider_invalid,以及自後端 5.22.0 起的legacy_serving_target_unavailable、deployment_target_unavailable、deployment_target_release_changed)是終態failed;worker 回到active(若有已部署版本)或draft。 - 僅地端:edge host 回覆「backup fence active」是延後,不是失敗。版本回到
deploy_pending,outbox 列回到pending且last_error_code="edge_host_fenced"、不延遲,該次嘗試不計入,票證讀作pending且error_code: "edge_host_fenced"。地端唯一會公開文字的終態失敗是edge_host_capacity_exhausted:它固定的指引文字被存為版本的error_message(其他每種終態失敗存的是deployment failed)。其他edge_host_*代碼:地端。 - 部署重試零退避(
next_attempt_at = NULL);節奏來自 transport nack/重投遞加上回收迴圈的 5 分鐘寬限。 - 在 AWS Batch 下,只有同一個
AWS_BATCH_JOB_ID的第 2 次嘗試可以回收自己還活著的租約;不同 job 不行。
回收與修復
每個 API replica 跑一個 60 秒迴圈,依序:重發可回收的部署(pending 停滯 > 5 分鐘或租約過期 — 每次發佈前重寫必要的 submitting 狀態,用秒精度的 next_attempt_at compare-and-set 圍欄)、調解控制面稽核、調解產物清理。批次上限各 50、FOR UPDATE SKIP LOCKED、沒有環境變數開關。另有兩點:
- Worker 資料列已不存在(公司被硬刪除)的 pending 或租約過期的部署列,會以
last_error_code="worker_deleted"結算為failed,而不是被永遠重發。不寫入稽核列,因為 Worker 與版本資料列都已不在。 - 每輪之前,迴圈會驗證
EDGE_WORKERS_PROVIDER;值不合法時記錄Edge Worker recovery skipped: edge_workers_provider_invalid,整輪(部署、控制面稽核與清理的回收)略過 60 秒。
第二條 15 秒迴圈重試已刪除部門的退役;它從不碰部署(見退役)。
uncertain 部署需要人:公司管理者呼叫 POST /company/departments/{department_id}/repairs,帶精確標記 {action: "reconcile_uncertain_deployment", ticket_id, observed_worker_revision}。過期的修訂版得到 409 “Worker revision is stale; refresh department state”。修復把 outbox 列重設為 pending 並設定 version.state = "deploying" — 這正是強制下一次任務執行走 GET-only 調解、絕不盲目重新 PUT 的機制。那次執行只有在回讀吻合且 release 通過 readiness 檢查時才結算 completed;因此 worker_readiness_timeout 在 release 能回應之後即可透過修復結案。修復路由和部署路由一樣會回 503 Edge Worker provider is not configured,在雲端也會回 409 worker_serving_target_unavailable;地端在閘門關閉期間則回同樣的 409 edge_workers_maintenance/edge_workers_restore_pending。細節:公司管理員。
票證輪詢者看到什麼
GET /worker/deployments/{ticket_id} 回傳 {ticket_id, operation: "deploy", company_id, department_id, status, worker_id, version_id, error_code}。
- 狀態字彙:
submitting、pending、leased、deploying、completed、failed、cancelled、uncertain。submitting(202 body/發佈前的 Redis 寫入)與deploying(版本圍欄的 Redis 鏡像)只存在於 API 層,永遠不是資料庫狀態。 - 終態:
completed、failed、cancelled。uncertain不是終態 — 意思是「已暫停,等公司修復」。 - 上傳之後,票證在檢查 release 的期間(上面第 8 步)最多可能維持
leased或deploying約 10 分鐘;請持續輪詢直到終態。 - Redis 補充只在資料庫列為
leased時生效,且五個身分欄位都吻合、快取狀態屬於leased/deploying才採用 — 7 天前的過期快取永遠無法讓終態票證倒退。 - 輪詢者可能遇到、以前不存在的
error_code值:department_deleted(雲端,failed)、edge_workers_provider_invalid(failed,調解中則為uncertain)、自後端 5.22.0 起的legacy_serving_target_unavailable、deployment_target_unavailable與deployment_target_release_changed(雲端,failed;後兩者在調解中為uncertain),以及地端的edge_host_fenced(pending)與edge_host_*失敗。完整列表見錯誤。