Skip to Content
概念版本與部署

版本與部署

本頁描述雲端 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。

路由,依序:

  1. provider 檢查:EDGE_WORKERS_PROVIDER 不合法(或地端 provider 未安裝)→ 503 Edge Worker provider is not configured。同一個檢查也把關 POST /worker/versions 與公司修復路由。
  2. 先查完全相同的重試,再暫存任何東西:同一個 (version_id, base_revision) 與同一個設定摘要的票證若已存在,會直接回傳(202),不動設定;見下方 重試與設定摘要。對於新的請求,路由接著會檢查能否驗證 Worker 的服務位置,否則 409 {"code":"worker_serving_target_unavailable","message":"Worker deployment target cannot be verified"},且不暫存任何東西。
  3. 設定鎖:每個部門一把 Redis 鎖(TTL 600 秒),讓部署與操作設定寫入序列化。被另一個請求持有 → 409 {"code":"deployment_in_progress","message":"Another Worker settings mutation or deployment is in progress"};Redis 無法使用 → 503 Operation-settings mutation lock is unavailable。
  4. 在途票證檢查:該部門任何不是 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,不要比對舊字串。
  5. 操作設定暫存,涉及設定時才執行:見下方 部署中的操作設定。
  6. 部署 outbox。

部署 outbox:

  1. 僅地端:維護閘門。resync epoch 進行中(或維護資料列不存在)→ 409 edge_workers_maintenance;還原保護開啟中 → 409 edge_workers_restore_pending。雲端的閘門永遠開啟,所以這兩個 409 在雲端絕不會出現。
  2. FOR UPDATE 鎖定 Worker(並在鎖定下重新讀取)。服務位置必須仍可驗證(否則 409 “Worker deployment target cannot be verified”),且 Worker 不得有尚未結束的搬移(否則 409 worker_migration_in_progress,搬移 Worker);接著要求 base_revision == worker.latest_revision,否則 409 “Worker revision changed; refresh before deploying”。
  3. 查詢同一個 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”)。
  4. Worker 的任何修訂版處於 deploy_pending/deploying/uncertain → 409 “a deployment is already active for this department Worker”;目標版本不是 ready → 409 Worker version cannot deploy from state '<state>';任何尚未釋放的 outbox 部署列 → 同一條「already active」字串。
  5. 配發 worker.deployment_sequence + 1 並複製到 outbox 列的 admission_sequence(在 BigInteger 上限耗盡 → 409)。
  6. 插入 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 的位置(以及它是從哪個位置部署而來),沒有任何路由會回傳它。
  7. 設定 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 = 清除);無法使用的帳號為 422 service_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.8operation_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.9service_account_credentials_require_supported_release — Unbind every service account (PUT operation-settings) before deploying a release without service-account support
最終得到的設定表(列出的或帶過去的)引用了服務帳號,但目標或上線中的 release 早於 v2.9service_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 機密。

部署任務實際做的事

部署任務(常數與完整分支見背景任務):

  1. Redis 投遞認領(諮詢性、30 秒、owner-fenced、有心跳)合併重複的 transport 投遞;耐久權威是 DB 租約(90 秒,每 30 秒心跳,owner token owner:uuid4)。provider 在取得租約之前就已解析;若其設定不合法,任務一取得租約就結算票證:failed,帶 edge_workers_provider_invalid(版本已是 deploying 時為 uncertain)。僅地端:執行閘門關閉時,任務延後,且不結算任何東西。
  2. 部門檢查(僅雲端,GET-only 調解中不做):在取得租約之後、讀取或上傳任何東西之前,部門資料列不存在或已軟刪除,就把票證結算為 failed,error_code: "department_deleted" — 版本變成 failed(error_message 為 deployment failed),Worker 回到 active(若有已部署版本)或 draft,並以該 reason_code 稽核 deployment.failed。現在上傳會讓已刪除部門的 Worker 復活;見退役。
  3. 先做完整性驗證 — 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。
  4. 上 provider 前的圍欄:單一交易 — 重新驗證租約 + bundle 身分 + state == "deploy_pending",然後 version.state = "deploying" 且同交易 worker.status = "paused"。暫停會在 provider 可能觀察到混合狀態之前擋掉新的呼叫准入;已被准入的舊 generation 請求仍可打到對應的舊 bundle 完成。
  5. 舊版部署的機密清除,在上述規則適用時執行。清除未獲確認,或 Worker 的身分在此期間改變,部署以 failed 結束,帶 artifact_integrity_failed;閘道錯誤則以 failed 結束,帶 deployment_internal_error。兩種情況下都尚未觸及 provider。
  6. 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,以及一個 tag release-<provider_release_digest>。每次都送完整期望狀態;不用 keep_bindings。
  7. PUT 成功不等於結算。 回讀同時比對 GET .../settings(binding 的權威)與 Worker Details(持久化的 live exports):compatibility date 相等、compatibility flags 為空、恰好一個精確 binding、恰好一個 SQLite DO export(Cloudflare 可選的 state 欄位:缺席與 "created" 等價,其他都是漂移)、兩端各恰好一個相同的 release-… tag。
  8. 回讀吻合也還不等於結算 — 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(閘道通道的呼叫得到 404 Department 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_* 失敗。完整列表見錯誤。
Last updated on