「跟外部系統對接的程式碼,逾時控制不是一開始就該設計好嗎?」
理論上是這樣沒錯。但今天要老實講一個真實發生過的演進過程:這個系統跟外部系統對接的 SOAP Client,最初的版本完全沒有逾時控制——它能動、能拿到資料,但如果外部系統那端卡住不回應,這邊的請求會一直等下去。逾時防護是後來才補上的。這篇想講的不是「應該怎麼設計」,是「真實系統常常是這樣一步一步補齊的」。
這個系統跟外部系統對接用的 SOAP Client Factory,最早的版本簽章大致是這樣:
public static function factory(string $wsdl): DataGatewayClient
{
$engine = DefaultEngineFactory::create(
ExtSoapOptions::defaults($wsdl, [])
->withClassMap(DataGatewayClassmap::getCollection())
);
// ...
}
只吃一個 WSDL 位址,內部組出一個沒有特別設定逾時的 SOAP engine。這個版本能正常運作——多數時候,外部系統回應得夠快,這支 Client 完全感覺不出問題。
實際的 commit 訊息只留下簡短的一句「soap timeout throw error」,但改動內容清楚說明了發生了什麼事——這支 Factory 的簽章加了一個新參數:
public static function factory(Psr18Transport $transport, string $wsdl): DataGatewayClient
{
$extSoapOptions = ExtSoapOptions::defaults($wsdl, [
'connection_timeout' => 5,
])->withClassMap(DataGatewayClassmap::getCollection());
$engine = DefaultEngineFactory::create($extSoapOptions, $transport);
// ...
}
多了一個 Psr18Transport $transport 參數,讓呼叫端可以控制底層的傳輸層設定(就是 Day 17 看到的、Guzzle Client 帶著 CONNECT_TIMEOUT/TIMEOUT 設定的那個傳輸層),同時 SOAP 層自己也加上了 connection_timeout => 5。
沒有逾時控制的版本,在多數情況下運作正常——這正是這類問題最容易被拖延處理的原因:外部系統平常回應夠快,逾時風險不會在日常開發、日常使用時顯現出來。只有在外部系統真的卡住、網路異常、或者對方系統本身出問題時,這個缺口才會變成真正的痛。
這是一個很真實的開發節奏:先讓功能能動起來、能拿到資料、能通過驗收,逾時防護這種「平常不會出事、一旦出事影響很大」的邊角情況,往往要等到真的踩到(或者有意識地回頭補強)才會補上。commit 訊息裡的「throw error」暗示著,這次改動很可能就是因為真的遇到了外部系統卡住、導致請求無限期等待的情況,才回頭補上這一層防護。
這不是說「一開始就該設計完善」是錯的建議——如果條件允許,當然應該一開始就把逾時考慮進去。但真實專案的節奏經常是:先解決「能不能動」,再回頭處理「會不會出事」,這篇想誠實記錄的正是這個過程,而不是假裝這個系統從第一天就設計得完美無缺。
如果一支負責跟外部系統溝通的程式碼完全沒有逾時控制,最壞的情況是:外部系統卡住不回應,這邊的請求會一直掛著等待,佔用一個執行緒/連線資源不釋放。如果這種呼叫發生在使用者的請求週期裡(不是背景 Job),使用者會看到頁面一直轉圈圈,甚至觸發伺服器層級更嚴重的資源耗盡問題。如果是背景 Job 裡呼叫,佇列裡的這個工作會一直卡著不結束,後面排隊的其他工作全部被拖住。
你的專案裡有沒有呼叫外部系統的程式碼,逾時設定是明確控制的,還是依賴框架/套件的預設值(甚至完全沒設)?如果外部系統今天卡住不回應,你的系統會怎麼反應——是優雅地在幾秒內失敗,還是無限期等下去?
Psr18Transport 參數 + connection_timeout 設定,讓逾時變得可控明天要看這個系統怎麼處理外部系統回傳的不乾淨資料——一段用來清理非法字元的防禦函式,作者自己誠實標註「這不是我自己想出來的,是抄一篇公開文章的解法」。