產物與清理
一個獨立範圍的產物儲存
Edge Worker 產物(提交的專案壓縮檔與建出的 bundle)存放在由部署環境的 Edge Worker provider(EDGE_WORKERS_PROVIDER,預設 cloudflare)選定的儲存裡。兩種儲存都只屬於這個模組,所以它的佈署永遠不可能覆寫或擴大對一般應用 blob 的存取。
- 雲端 provider(Cloudflare R2)。 產物使用自己的 bucket 與憑證對(
EDGE_WORKERS_R2_BUCKET、EDGE_WORKERS_R2_ACCESS_KEY_ID/_SECRET_ACCESS_KEY)。沒有退回應用憑證或AWS_S3_BUCKET的路徑;S3_PROVIDER必須是r2,endpoint 必須恰為https://<EDGE_WORKERS_CLOUDFLARE_ACCOUNT_ID>.r2.cloudflarestorage.com(無 userinfo/port/path/query/fragment;S3_ENDPOINT_URL只有在正是同帳號的那個正規 endpoint 時才被接受)。boto3 釘region_name="auto"、SigV4。 - 地端 provider(
EDGE_WORKERS_PROVIDER=onprem)。 產物存進地端套件專用的 S3 相容產物服務,由EDGE_WORKERS_ARTIFACTS_ENDPOINT_URL(必須是套件固定的內部 origin)、EDGE_WORKERS_ARTIFACTS_BUCKET、EDGE_WORKERS_ARTIFACTS_ACCESS_KEY_ID與EDGE_WORKERS_ARTIFACTS_SECRET_ACCESS_KEY設定,使用 path-style 定址與區域us-east-1。上面的 R2 規則一律不適用於它。見地端 Edge Workers。
選定的儲存若未設定,提交會回應 503 Edge Worker artifact storage is unavailable,原始碼下載會回應 503 edge_worker_artifact_unavailable(錯誤);provider 本身設定有誤時,提交一開始就會被更早拒絕,回應 503 Edge Worker provider is not configured。
有時限的用戶端。 不論哪種 provider,每次產物呼叫都經過每個(endpoint、bucket、region、選項、憑證)組合共用的一個 S3 用戶端,每個行程只建一次(最多快取 8 個)。這些上限是程式碼常數,不是環境設定:連線逾時 5 秒、讀取逾時 20 秒、重試模式 standard 搭配 2 次重試(每次呼叫最多 3 次嘗試)。因此卡住的儲存會在有限時間內讓請求失敗,而不是讓它一直懸著。
物件佈局
edge-workers/{tenant_prefix}/{kind}/{digest}-{artifact_id}.{ext}tenant_prefix=sha256("edge-artifacts-v2\0" + company_id + "\0" + department_id)[:32]— 原始租戶 ID 永遠不出現在 key 裡。- 種類:
project(.tar.gz、application/gzip、1 位元組–2 MiB)與bundle(.mjs、application/javascript+module、1 位元組–5 MiB)。 - 每次 PUT 都帶
Metadata: {sha256digest: <digest>},且永不帶 objectTagging參數(測試強制)。 - 讀取與刪除都通過所有權圍欄:key 必須以本租戶的
edge-workers/{prefix}/{kind}/開頭、其後不得再有/— 跨租戶存取與前綴穿越在結構上就被擋掉。get_bytes雙重限界(宣告的ContentLength+ 實際位元組),部署任務每次讀取都重驗大小 + SHA-256。
產物是不可變證據:儲存裡放著管理者提交的精確壓縮檔與平台建出的精確 bundle;部署任務把那個儲存的 bundle 讀回來並逐位元組部署。部門管理者可以用 GET /worker/versions/{version_id}/source 再次下載儲存的壓縮檔(範本與原始碼下載):該 route 透過同一個有上限的 get_bytes 讀取 project 物件(最多 2 MiB),在送出任何位元組之前先比對其 SHA-256 與該版本的 project_digest,並且在任何儲存呼叫之前就拒絕物件 key 不在該部門自己前綴之下的版本列。被取代的版本保留其資料列與產物,所以它們的壓縮檔仍可下載;下方的調節器只會刪除沒有任何版本列引用的物件。
清理 outbox:先寫意圖、再寫物件
設計拒絕允許的失敗模式:請求崩潰或提交確認遺失,留下無主的計費物件、或刪掉已提交的產物。機制:
- 在任何一個產物寫入之前,提交路由先提交一筆
artifact_cleanupoutbox 意圖,指名兩個精確 key + 摘要(dedupe keyartifact-cleanup:{artifact_id}),15 分鐘寬限後到期。 - 兩個產物上傳。
- 版本提交交易**原子性收養(取消)**該意圖 — 就是讓版本落地的同一個交易。若提交確認遺失,路由會用身分釘選欄位(租戶、建立者、摘要、物件 key、專案大小、builder 政策版本)讀回租戶擁有的版本、用
artifact_id讀回意圖再決定;未提交成功則把意圖立即設為到期(submission_not_committed)。
調節器
每個 API replica 的 60 秒迴圈租約到期的意圖(FOR UPDATE SKIP LOCKED、批次 50、租約 300 秒),逐筆:
- 對同租戶的
edge_worker_versions重新檢查每個 key。兩個都被引用 → 該列以cancelled結算並指向擁有版本,什麼都不刪(即使資料損毀讓兩個 key 指向不同版本也 fail-closed)。只有一個被引用 → 只刪孤兒的那一個。 - 刪除是冪等的(刪不存在的 key 也算成功),所以刪除與結算之間的崩潰由租約過期+無害的重複刪除復原。
- 失敗保持耐久:該列帶指數退避(15 秒 → 上限 3600 秒)回到
pending;產物儲存中斷讓整批失敗並重試 — 刪除失敗永遠不會默默丟失意圖證據。
結果:每個儲存的產物物件在任何時刻都被一筆存活的版本列或一筆待處理的清理意圖覆蓋。精確的認領/租約機制見背景任務。Worker 本身在其部門或公司被刪除時會發生什麼事(鎖定、墓碑、孤兒腳本)說明在退役。