雲端 Mac 上的 iOS 相依圖驗收:穩定拓撲排序與循環偵測

雲端 Mac 上的 iOS 相依圖驗收:穩定拓撲排序與循環偵測

雲端 Mac 上的 iOS 相依圖驗收:穩定拓撲排序與循環偵測

同一批資源處理工作,在本機執行時順序正確,搬到雲端 Mac 的流水線後,卻偶爾先產生索引,再產生資源。問題不一定出在機器:如果工作順序取決於字典的走訪順序,或把「已排入佇列」誤認為「相依工作已完成」,只要換一個程序執行,就可能暴露錯誤。以下使用獨立的 Swift 指令稿驗收相依圖,不必先啟動模擬器。

先定義邊的方向與失敗語意

手動指定順序,適合固定且很短的流程;相依圖則適合工作會增減,或多個步驟共用前置工作的流程。如果所有工作本來就是循序執行,引入圖只會增加維護成本。

本文以「工作名稱 → 前置工作集合」表示輸入。例如,index 相依於 assets,輸出順序就必須讓後者先出現。工作名稱是穩定的識別碼,不隨顯示語言改變;互不相依的工作,則依識別碼的字典序選取。

輸入還必須約定三件事:重複的邊合併處理、遇到未知相依立即失敗、自我相依視為循環。空圖是合法輸入,應回傳空陣列。不要默默建立未知節點,否則名稱拼錯也會被當成成功。

排序只能證明相依關係是否能排成合法序列,不能證明工作已完成,更不能證明產出正確。執行狀態與圖結構應分開驗收。

實作可執行的穩定排序器

在節點的工作目錄中,將以下內容儲存為 graph-check.swift。程式碼只依賴 Swift 標準函式庫,使用目前選定的工具鏈執行即可,不需要安裝額外套件。演算法會記錄每個工作尚未滿足的相依數量,每一輪都選取名稱排序最前的就緒工作。

enum GraphError: Error, Equatable {
    case unknown(task: String, dependency: String)
    case blocked([String])
}

func ordered(_ graph: [String: [String]]) throws -> [String] {
    let names = graph.keys.sorted()
    let known = Set(names)
    var remaining: [String: Int] = [:]
    var successors: [String: [String]] = [:]

    for task in names {
        let dependencies = Set(graph[task]!)
        for dependency in dependencies.sorted() {
            guard known.contains(dependency) else {
                throw GraphError.unknown(
                    task: task, dependency: dependency
                )
            }
            successors[dependency, default: []].append(task)
        }
        remaining[task] = dependencies.count
    }

    var ready = names.filter { remaining[$0] == 0 }
    var result: [String] = []

    while !ready.isEmpty {
        let task = ready.removeFirst()
        result.append(task)
        for next in successors[task, default: []] {
            remaining[next]! -= 1
            if remaining[next] == 0 {
                ready.append(next)
            }
        }
        ready.sort()
    }

    guard result.count == names.count else {
        throw GraphError.blocked(
            names.filter { remaining[$0]! > 0 }
        )
    }
    return result
}

func check() throws {
    let graph = [
        "assets": ["fetch", "fetch"],
        "fetch": [],
        "index": ["assets"],
        "lint": []
    ]
    let result = try ordered(graph)
    precondition(result == ["fetch", "assets", "index", "lint"])

    let position = Dictionary(
        uniqueKeysWithValues: result.enumerated().map {
            ($0.element, $0.offset)
        }
    )
    precondition(result.count == graph.count)
    precondition(Set(result) == Set(graph.keys))
    for (task, dependencies) in graph {
        for dependency in dependencies {
            precondition(position[dependency]! < position[task]!)
        }
    }

    let empty = try ordered([:])
    precondition(empty.isEmpty)

    do {
        _ = try ordered(["export": ["missing"]])
        preconditionFailure("Unknown dependency was accepted")
    } catch let error as GraphError {
        precondition(error == .unknown(
            task: "export", dependency: "missing"
        ))
    }

    do {
        _ = try ordered([
            "a": ["b"], "b": ["a"], "report": ["b"]
        ])
        preconditionFailure("Cycle was accepted")
    } catch let error as GraphError {
        precondition(error == .blocked(["a", "b", "report"]))
    }
}

try check()
xcrun swift graph-check.swift

驗收通過時不會輸出任何內容;失敗時,程序會回傳非零結束代碼。執行這份驗收指令稿時,不要使用會停用前置條件檢查的最佳化模式。整合到專案後,可以將相同的斷言移至正式測試目標,避免業務程式的建置設定影響檢查行為。

用性質斷言補足範例的盲點

固定的預期結果能找出順序漂移,卻也可能連錯誤實作一起固定下來。因此,程式碼同時驗證輸出數量、節點集合,以及每條邊的先後關係。這三類檢查分別用來防止遺漏、重複與相依順序顛倒。

接著加入菱形圖:兩個工作相依於同一個根節點,最後一個工作再相依於這兩個工作。根節點只能輸出一次,匯合工作必須最後出現。再加入多個獨立子圖、單一節點、自我相依,以及所有節點都位於循環中的輸入。

對同一張圖,以不同順序將項目插入字典,再反轉各個相依陣列,結果都應完全一致。這裡的確定性來自明確排序,而不是反覆執行後「看起來都一樣」。不要把一次碰巧正確的字典走訪順序當成介面保證。

不要把阻塞集合誤報為循環成員

範例中的 a 與 b 互相相依,report 只是相依於 b,卻同樣無法解除阻塞。因此,錯誤名稱使用 blocked,而不是宣稱所有殘留工作都位於循環中。

排查問題時應保留哪些證據

失敗報告只需保留原始圖、經穩定排序的阻塞集合、提交識別碼與工具鏈版本。若需要在介面上精確標紅循環成員,再加入強連通分量分析;不要只憑入度仍大於零,就畫出「循環路徑」。

存在循環時,即使部分獨立工作已排出順序,整體規劃仍應失敗。除非產品明確支援部分執行,否則不能把前半段結果當成完整計畫交給執行器。

整合流水線時,守住排程邊界

先在雲端 Mac 上執行圖檢查,再進入耗時的建置步驟。保留非零結束代碼,讓相依設定錯誤在耗時數分鐘的工作啟動前就被發現,而不是等執行器卡住後才察覺。

這份實作每一輪都會重新排序就緒陣列,適合規模有限的應用程式工作圖;節點數量增加後,可以改用最小堆積,但必須維持相同的字典序契約,並沿用現有測試比對結果。

真正平行執行時,只有在前置工作成功完成後,才能減少後續工作的未完成相依計數。失敗、取消與重試都需要獨立的狀態,不能從拓撲順序的索引推導。最終驗收應同時滿足:異常輸入有明確說明、合法圖的順序穩定、執行器不會提早將相依條件視為已滿足。這樣即使換到另一個建置節點,也不會把偶然的順序帶進發布流程。

常見問題

排序失敗後,殘留節點都是循環成員嗎?

不是。依賴循環的下游工作也可能殘留,應先標為無法排程。若要精確找出循環成員,需再做強連通分量分析。

穩定排序會限制工作只能循序執行嗎?

不會。它只固定輸出順序;並行排程仍須維護就緒集合,等前置工作真正完成後才釋放後續工作。

用你的建置工作測試獨享節點

整台實體 Mac mini 僅供一位客戶使用,並非虛擬機器。先依專案規模挑選規格,再按天測試自己的相依套件、快取與打包流程。