Skip to content

Latest commit

 

History

History
145 lines (101 loc) · 9 KB

File metadata and controls

145 lines (101 loc) · 9 KB

Device Test Runner Process Lifecycle

Version: v1.6.2

Purpose

本文件說明一次 attempt 如何啟動、停止與收尾,以及清理和 retry 的關係。閱讀順序是「程序 → 群組 → 輸出 → 下一次 attempt」。類別介面與 UML 見 Architecture,本版測試案例見 Test Matrix。

Process model

Executor 用 Popen(shell=True, start_new_session=True) 執行命令,讓每次 attempt 有自己的 session 與 process group。沒有自行脫離群組的 child/grandchild,可以一起接收終止訊號。

Device Test Runner
└── Attempt process group
    └── shell / test script(parent)
        └── child
            └── grandchild

Parent/child 是誰啟動誰的關係;process group 是一起接收 signal 的範圍。killpg 操作的是群組,不是沿著 parent/child 關係搜尋所有後代。

元件 責任
DeviceTestRunner 安排 lifecycle、判斷 retry、驗證 artifacts 與寫入報告
SubprocessExecutor 啟動一次 attempt,監看完成/取消/timeout,收集輸出
ProcessTerminator 對該 attempt 的程序群組送訊號,等待群組退出
stdout/stderr reader threads 讀取外部程序的兩條輸出串流,保存到結果與 log

Normal completion, timeout and cancellation

Executor 每輪依序檢查 process 完成、token 取消、step timeout。正常 polling 間隔為 0.1 秒。

情境 Executor 行為 後續結果
直接 process 已完成 離開監看迴圈,收尾 reader;不主動清理群組 依 exit code 分類;runner 再處理 artifact validation 與 retry policy
程序仍執行且 token 已取消 呼叫群組清理,再收尾 reader 清理成功時為 CANCELLED,不 retry
程序仍執行且已超過 timeout 呼叫群組清理,再收尾 reader 清理成功時為 TIMEOUT,符合 retry policy 才重試

已觀察到完成的程序優先。如果程序仍執行中,取消與 timeout 同時可見,取消優先。以下流程描述清理成功的路徑;清理例外見後面的錯誤處理說明。

Run-level timeout

YAML 的 run_timeout_seconds 控制整次 run 的 deadline,省略時不啟動 watchdog。Watchdog 在 global_setup 前開始,與 steps、retry delay 共用同一截止時間;逾時以 RUN_TIMEOUT 取消 token。第一個取消原因不會被覆寫。

被中斷的 attempt 記錄 CANCELLED,run 記錄 TIMED_OUT,metadata.run_timed_out 為 true。Step timeout 仍記錄 TIMEOUT。Retry delay 期間逾時不新增 attempt,也不改寫前一次失敗。

Cleanup 使用新 token 與自己的 step timeout。Watchdog 到 cleanup 與 final validation 後才停止;report 在停止後建立。這不保證整個程序於 deadline 內退出,非預期例外也不保證 cleanup 或 report 完成。

Cleanup sequence

flowchart TD
    A[觀察到 cancel 或 timeout] --> B{清理開始時直接 process 已退出?}
    B -- Yes --> P[以原 PID 探測 process group]
    P --> Q{群組仍存在?}
    Q -- No --> C[返回,無需終止]
    Q -- Yes --> D[對群組送 SIGTERM]
    B -- No --> O[取得 PGID]
    O --> D
    D --> E[回收直接子程序並檢查群組]
    E --> F{寬限時間內群組消失?}
    F -- Yes --> G[Terminator 返回]
    F -- No --> H[對群組送 SIGKILL,再等最多 2 秒]
    H --> I{群組已消失?}
    I -- Yes --> G
    I -- No --> J[拋出 RuntimeError]
    C --> K[Executor 收尾 stdout 與 stderr readers]
    G --> K
    K --> L[返回 attempt 結果,Runner 判斷後續流程]
Loading

getpgid 找不到 direct child 時,改用原 PID 探測 group;此回退依賴 start_new_session=True。群組不存在才直接返回。送出 signal 時也處理 ProcessLookupError;不要求每次檢查之間程序狀態保持不變。

為什麼不能只看 parent?

例如 parent 收到 SIGTERM 就結束,但 child 故意忽略 SIGTERM。此時 process.poll() 已經回傳 exit code,child 卻仍可能工作。

先前清理只等待直接 process 結束,會過早返回。現在進入群組終止流程後,會持續用 os.killpg(PGID, 0) 檢查群組,必要時升級 SIGKILL。這是本版的重要修正。

signal 0 只檢查存在與權限,不會終止程序。ProcessLookupError 表示群組不存在;探測遇到 PermissionError 則不能宣告清理完成,會繼續等待到 deadline。這項處理僅針對探測,不表示送出 SIGTERM/SIGKILL 的權限錯誤也會被忽略。

等待時間

設定/階段 目前值 說明
Step timeout YAML 的 timeout_second 命令執行逾時門檻,不包含完整清理完成的承諾
Executor polling 0.1 秒 檢查完成、取消與 timeout 的間隔
SIGTERM grace period 預設 2 秒 可由 ProcessTerminator(grace_period_seconds=...) 調整
群組探測間隔 預設 0.05 秒 可由 poll_interval_seconds 調整
SIGKILL 後等待 最多 2 秒 目前寫在實作中,不是 YAML 設定
Reader join 每個最多 2 秒 依序等 stdout、stderr,並非兩個合計 2 秒

這些是分段等待,不是整次 run 的總 deadline。OS 排程、清理例外、lifecycle cleanup 與報告寫入也會影響完成時間。

Output readers and retry

外部程序透過 pipe 把 stdout/stderr 交給 runner。Reader 等到資料讀完、輸出端關閉後才會結束。Child 若仍持有 pipe,parent 即使已退出,reader 仍可能等待。

因此成功清理路徑的順序是:

  1. 停止群組並確認它消失。
  2. 等 stdout/stderr reader 結束,保留已讀取內容。
  3. Executor 返回 attempt 結果。
  4. Runner 判斷是否重試,處理 required artifact targets 與 retry delay。
  5. 符合條件才開始下一次 attempt。

真實 retry 測試在 Attempt 2 一開始就確認前次 parent 與 child PID 不存在,不會先 sleep 等它們消失。三層程序測試另外確認兩個 reader 已停止,且 stdout/stderr log 與結果一致。

程序清理與 lifecycle 的 teardown 是不同層級。前者停止一次 attempt 的 OS 程序;後者執行使用者設定的清理命令,例如還原裝置狀態。取消後仍依 runner 的 lifecycle 路由執行可到達的 teardown/global_teardown,各 cleanup attempt 使用新的 token。

Cleanup errors

失敗位置 目前行為
SIGKILL 後群組仍未消失 Terminator 拋出 RuntimeError("Process group did not exit after SIGKILL.")
Reader join 後仍存活 Executor 拋出 RuntimeError("Output reader thread did not stop.")
Executor 捕捉到 OSError/RuntimeError 記錄錯誤,嘗試終止仍存活的直接 process 所屬群組並 join readers;補救成功後回傳 PROCESS_ERROR
補救流程再次拋出例外 例外可能離開 executor,不能保證後續 teardown 或 JSON 報告完成

因此 timeout/cancel 觸發清理,不代表最終一定保留 TIMEOUT/CANCELLED 分類;清理失敗可能變成 PROCESS_ERROR,或直接以例外中斷流程。

Ctrl+C and cancellation

第一次 Ctrl+C/SIGINT 呼叫 token.cancel,讓 executor 與 runner 走受控取消流程。第二次 handler 呼叫拋出 KeyboardInterrupt,可能中斷正在進行的清理與報告寫入。

main.py 在 run 的 try 區塊捕捉 KeyboardInterrupt 後回傳 130,並在 finally 還原原本 SIGINT handler。一般結果的 CLI exit code 為 PASSED → 0、FAILED → 1、CANCELLED → 130。TIMED_OUT 目前落入回傳 0 的分支,是待修正缺口。

要區分兩個方向:Terminator 會送 SIGTERM 給 attempt 群組;但外部送 SIGTERM 給 runner 本身,目前沒有接成 token cancellation。

Evidence and limits

v1.6.1 Test Matrix 保留 parent/child/grandchild、忽略 SIGTERM、reader 收尾、log 保存與 retry 前清理的證據。本版新增 run timeout 與 orphan process 證據見 v1.6.2 Test Matrix。Fixture、monkeypatch 與模擬方式見 Test Guide。

目前限制:

  • 使用 POSIX process-group API;已有本機 macOS 測試紀錄,不能推論 Windows 支援或 Linux CI 已通過。
  • 自行建立新 session/脫離群組的後代不在清理範圍。
  • Direct child 已退出時仍可清理原 group;但需要呼叫端實際進入 terminator,不代表所有正常完成路徑皆會清理。
  • 正常完成後留下背景後代的情境,沒有主動清理保證。
  • 已退出但尚未回收的 zombie 可能仍保有 PID;群組消失時間受 OS 回收行為影響。
  • SIGINT 測試直接呼叫 handler,尚未驗證真實終端訊號、handler 還原與 CLI exit code 的完整流程。
  • 第二次 Ctrl+C、未處理例外與重複清理失敗,不保證完成 teardown 或保存完整報告。

本文件描述實作與既有測試證據,不新增測試執行紀錄;實際命令與結果見 Definition of Done。