Files
Arcrun/registry/components/wait/main.go
uncle6me-web f1370e2275 fix(engine): 等待搬回引擎——WASI 沙箱裡沒有「不花 CPU 地等」這種東西(Arcrun#101)
leo 在 youlin stage 實測(只有 input >> wait 兩個節點):
  ms=3000 → 38.9s 後 503(1102) / ms=20000 → 34.0s / ms=30000 → 34.9s / 寫死 3000 → 34.8s
四個值同一種死法、與 ms 無關 ⇒ 病不是「等待很貴」,是「等待從來沒成功過」。

修法:wait 移進 BUILTIN_COMPONENTS,由引擎 await 一個 timer。
只花 wall-clock、不記 CPU ⇒ 等 30 秒與等 3 秒同價(皆 ≈0)。
I/O 契約沿用 component.contract.yaml,既有 workflow 的 wait 節點定義不必改。

🔴 誠實標明:原本註解斷言「Workers 時鐘在同步執行期間凍結,所以自旋永不結束」。
寫測試去證,反而被打臉——workerd 裡自旋 2553 圈後 Date.now() 就前進了。
那條假斷言已刪除(不是改鬆),完整機制降級為推測。修法不依賴它:
純 WASI 沙箱本來就沒有睡覺這個手段,會等的只有宿主。

實測:
  npx vitest run tests/wait-builtin.test.ts  → 12 passed (12)
  npx vitest run(全套)                      → 386 passed / 14 failed
                                              (14 = 動工前的既有紅燈數,未新增)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:15:08 +08:00

71 lines
2.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// wait — 等待指定毫秒數後繼續(最多 30 秒)
//
// ⚠️ 已由引擎接手,這份 WASM 在 Cloudflare Workers 上跑不動(Arcrun#1012026-08-12)。
// 現行實作在 cypher-executor/src/lib/constants.ts 的 BUILTIN_COMPONENTS['wait']
// component-loader step 1 先命中,這顆 wasm 不會再被工作流呼叫到。
//
// 為什麼跑不動(不是「比較慢」,是「永遠不會結束」):
// 下面的 time.Sleep 在 TinyGo 走 WASI poll_oneoff,而 component worker 的 WASI shim
// 把 poll_oneoff 實作成 ENOSYS ⇒ TinyGo 排程器退化成迴圈重讀 clock_time_get 自旋;
// Workers 的時鐘在無 I/O 的同步執行期間是凍結的 ⇒ 結束條件永遠不成立 ⇒ 一路燒到
// CPU 上限被砍(error 1102)。leo 實測 ms=3000/20000/30000 全在 ~35 秒後 503
// 死法與 ms 無關 —— 這正是「迴圈沒結束」而非「等待很貴」的證據。
//
// 原本的舊註解寫「改用 busy-wait 模擬」是錯的:這個檔從來沒有 busy-wait,
// 一直是 time.Sleep。那句話誤導了後來每一個讀這個檔的人。
//
// 本次刻意不改行為、只改註解:手邊沒有 TinyGo 工具鏈,改了 main.go 卻沒重編,
// 會讓 repo 內已 commit 的 .component-builds/wait/component.wasm 與原始碼漂移
// rule 05「WASM 來源」:那份 wasm 是 self-host 用戶的部署來源)。
// 要退役這顆零件(刪目錄/下架 wait.arcrun.dev)是另一個決定,需人拍板。
package main
import (
"encoding/json"
"io"
"os"
"time"
)
type Input struct {
Ms int `json:"ms"`
Context map[string]interface{} `json:"context"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.Ms <= 0 {
writeError("ms 必須大於 0")
return
}
ms := input.Ms
if ms > 30000 {
ms = 30000
}
time.Sleep(time.Duration(ms) * time.Millisecond)
result := make(map[string]interface{})
for k, v := range input.Context {
result[k] = v
}
result["waited_ms"] = ms
out, _ := json.Marshal(map[string]interface{}{"success": true, "data": result})
os.Stdout.Write(out)
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}