<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Root-Cause-Analysis on Tarragon</title><link>https://tarrragon.github.io/blog/tags/root-cause-analysis/</link><description>Recent content in Root-Cause-Analysis on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 28 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/root-cause-analysis/index.xml" rel="self" type="application/rss+xml"/><item><title>CI step silent hang：時間真空才是訊號、happy log 反而是 anti-signal</title><link>https://tarrragon.github.io/blog/work-log/ci-step-silent-hang%E6%99%82%E9%96%93%E7%9C%9F%E7%A9%BA%E6%89%8D%E6%98%AF%E8%A8%8A%E8%99%9Fhappy-log-%E5%8F%8D%E8%80%8C%E6%98%AF-anti-signal/</link><pubDate>Thu, 28 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/ci-step-silent-hang%E6%99%82%E9%96%93%E7%9C%9F%E7%A9%BA%E6%89%8D%E6%98%AF%E8%A8%8A%E8%99%9Fhappy-log-%E5%8F%8D%E8%80%8C%E6%98%AF-anti-signal/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心議題&lt;/strong>：CI step 看起來「跑了很久才 timeout」時，要分辨「真的時間不夠」跟「silent hang 占滿時間」 — 兩者修法完全不同。Silent hang 的訊號是「最後一行 happy log 到 cancel 之間有大段時間真空」、不是「最後一行錯誤訊息」。第一次歸因錯誤後、第二次 fail 不該再加 timeout、該停下來重看 detailed log。
&lt;strong>案例骨幹&lt;/strong>：本 blog 的 Playwright CI 一直 timeout、初診「cache 缺失 + timeout 太緊」加了 cache + bump timeout、仍 timeout。重看 detailed log 發現 chromium 下載 2 秒完成、之後 24 分 31 秒&lt;strong>完全沒任何 log&lt;/strong> 才被 cancel — Playwright 1.59 在 Node.js 24.16.0 的 extract-zip regression（&lt;a href="https://github.com/microsoft/playwright/issues/41000">microsoft/playwright#41000&lt;/a>、上游 &lt;a href="https://github.com/nodejs/node/issues/63487">nodejs/node#63487&lt;/a>）。升 Playwright 1.60.0 後該 step 從 25 分鐘卡死降到 22 秒。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="1-silent-hang-是-happy-log-的-anti-signal">1. Silent hang 是 happy log 的 anti-signal&lt;/h2>
&lt;p>CI step timeout 時、第一個本能是看「step 跑了多久」。15 分鐘 timeout 然後被砍、直覺判斷是「時間不夠、bump timeout」。這個直覺對應的失敗模式是「step 真的需要 16 分鐘才能跑完」。&lt;/p>
&lt;p>但有另一種失敗模式長得很像、修法完全不同：&lt;strong>silent hang&lt;/strong> — step 在某個點之後就不再輸出任何 log、process 仍在執行（沒有 crash）、直到外部 timeout 才被砍。表面看跟「時間不夠」一樣（step 跑很久才被 cancel）、但根因是 process 本身卡死、給多少時間都跑不完。&lt;/p>
&lt;p>辨識 silent hang 的關鍵訊號是「最後一行 happy log 到 cancel 訊息之間有大段時間真空」。&lt;strong>「Happy log」指的是看起來成功的訊息&lt;/strong>（例：下載 100% 完成、build succeeded、X tests passed）— 這類訊息特別會誤導判斷、因為它讓人以為任務在進展。Silent hang 開始之前的最後一行通常正是這種 happy log、是正常結束訊號的反面。&lt;/p>
&lt;h3 id="三類-timeout-模式的對照">三類 timeout 模式的對照&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>可能根因&lt;/th>
 &lt;th>修法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>整個 step 進度持續、最後階段加速到 timeout&lt;/td>
 &lt;td>時間真的不夠&lt;/td>
 &lt;td>bump timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>有失敗訊息（exception / non-zero exit）之後 timeout&lt;/td>
 &lt;td>code 邏輯錯&lt;/td>
 &lt;td>看訊息修&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>最後一行 log 之後有大段時間真空、然後 cancel&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>Silent hang&lt;/strong>、可能 upstream bug&lt;/td>
 &lt;td>&lt;strong>查 upstream issue tracker、不是加 timeout&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第三種最容易誤判、因為「log 之間沒輸出」沒被當成訊號 — 但&lt;strong>訊息真空本身就是訊號&lt;/strong>。寫 debug log 的人會記得補 error 訊息、但 silent hang 通常發生在工具內部的某個沒輸出 log 的等待點、所以沒有 error 訊息可看。&lt;/p>
&lt;hr>
&lt;h2 id="2-為什麼cache-缺失--bump-timeout的初診是-false-positive">2. 為什麼「cache 缺失 + bump timeout」的初診是 false positive&lt;/h2>
&lt;p>第一次看 CI fail log 時、有三件容易抓到的事：&lt;/p>
&lt;ol>
&lt;li>workflow YAML 裡的 &lt;code>timeout-minutes: 15&lt;/code>&lt;/li>
&lt;li>step 跑了 &lt;code>15m 6s&lt;/code>（幾乎等於 timeout 上限）&lt;/li>
&lt;li>step 名稱是 &lt;code>Install Playwright browsers&lt;/code>（要下載 170 MiB）&lt;/li>
&lt;/ol>
&lt;p>直覺合成的結論：「cache 缺失 + timeout 太緊」。這結論看起來「應該對」 — 因為這兩個都是「Install Playwright browsers」眾所周知的優化點。修法：加 &lt;code>actions/cache&lt;/code> + bump timeout 25 min。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心議題</strong>：CI step 看起來「跑了很久才 timeout」時，要分辨「真的時間不夠」跟「silent hang 占滿時間」 — 兩者修法完全不同。Silent hang 的訊號是「最後一行 happy log 到 cancel 之間有大段時間真空」、不是「最後一行錯誤訊息」。第一次歸因錯誤後、第二次 fail 不該再加 timeout、該停下來重看 detailed log。
<strong>案例骨幹</strong>：本 blog 的 Playwright CI 一直 timeout、初診「cache 缺失 + timeout 太緊」加了 cache + bump timeout、仍 timeout。重看 detailed log 發現 chromium 下載 2 秒完成、之後 24 分 31 秒<strong>完全沒任何 log</strong> 才被 cancel — Playwright 1.59 在 Node.js 24.16.0 的 extract-zip regression（<a href="https://github.com/microsoft/playwright/issues/41000">microsoft/playwright#41000</a>、上游 <a href="https://github.com/nodejs/node/issues/63487">nodejs/node#63487</a>）。升 Playwright 1.60.0 後該 step 從 25 分鐘卡死降到 22 秒。</p></blockquote>
<hr>
<h2 id="1-silent-hang-是-happy-log-的-anti-signal">1. Silent hang 是 happy log 的 anti-signal</h2>
<p>CI step timeout 時、第一個本能是看「step 跑了多久」。15 分鐘 timeout 然後被砍、直覺判斷是「時間不夠、bump timeout」。這個直覺對應的失敗模式是「step 真的需要 16 分鐘才能跑完」。</p>
<p>但有另一種失敗模式長得很像、修法完全不同：<strong>silent hang</strong> — step 在某個點之後就不再輸出任何 log、process 仍在執行（沒有 crash）、直到外部 timeout 才被砍。表面看跟「時間不夠」一樣（step 跑很久才被 cancel）、但根因是 process 本身卡死、給多少時間都跑不完。</p>
<p>辨識 silent hang 的關鍵訊號是「最後一行 happy log 到 cancel 訊息之間有大段時間真空」。<strong>「Happy log」指的是看起來成功的訊息</strong>（例：下載 100% 完成、build succeeded、X tests passed）— 這類訊息特別會誤導判斷、因為它讓人以為任務在進展。Silent hang 開始之前的最後一行通常正是這種 happy log、是正常結束訊號的反面。</p>
<h3 id="三類-timeout-模式的對照">三類 timeout 模式的對照</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能根因</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>整個 step 進度持續、最後階段加速到 timeout</td>
          <td>時間真的不夠</td>
          <td>bump timeout</td>
      </tr>
      <tr>
          <td>有失敗訊息（exception / non-zero exit）之後 timeout</td>
          <td>code 邏輯錯</td>
          <td>看訊息修</td>
      </tr>
      <tr>
          <td><strong>最後一行 log 之後有大段時間真空、然後 cancel</strong></td>
          <td><strong>Silent hang</strong>、可能 upstream bug</td>
          <td><strong>查 upstream issue tracker、不是加 timeout</strong></td>
      </tr>
  </tbody>
</table>
<p>第三種最容易誤判、因為「log 之間沒輸出」沒被當成訊號 — 但<strong>訊息真空本身就是訊號</strong>。寫 debug log 的人會記得補 error 訊息、但 silent hang 通常發生在工具內部的某個沒輸出 log 的等待點、所以沒有 error 訊息可看。</p>
<hr>
<h2 id="2-為什麼cache-缺失--bump-timeout的初診是-false-positive">2. 為什麼「cache 缺失 + bump timeout」的初診是 false positive</h2>
<p>第一次看 CI fail log 時、有三件容易抓到的事：</p>
<ol>
<li>workflow YAML 裡的 <code>timeout-minutes: 15</code></li>
<li>step 跑了 <code>15m 6s</code>（幾乎等於 timeout 上限）</li>
<li>step 名稱是 <code>Install Playwright browsers</code>（要下載 170 MiB）</li>
</ol>
<p>直覺合成的結論：「cache 缺失 + timeout 太緊」。這結論看起來「應該對」 — 因為這兩個都是「Install Playwright browsers」眾所周知的優化點。修法：加 <code>actions/cache</code> + bump timeout 25 min。</p>
<p>修完仍 timeout、但這次跑 <code>25m 6s</code>（一樣頂到上限）。</p>
<p><strong>這時的訊號應該是「同樣的 step 在 1.67 倍的 timeout 下仍頂到上限」</strong> — 如果是時間不夠、bump 之後該往中間靠（譬如完成在 18-20 min）；如果一直頂到上限、意思是 step 不會自己結束、是 hang。</p>
<p>但初診時很容易略過這個訊號、轉而繼續想「是不是 cache step 設定有問題？」。這個歸因方向是錯的、因為前置假設「cache 是瓶頸」本身就沒驗證過。</p>
<h3 id="一輪-false-positive-的-anatomy">一輪 false positive 的 anatomy</h3>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>容易做的</th>
          <th>該做的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>看到 timeout</td>
          <td>假設「時間不夠」</td>
          <td>先區分「時間不夠」vs「silent hang」</td>
      </tr>
      <tr>
          <td>看 high-level log</td>
          <td>假設「下載慢」</td>
          <td>應該看下載前後 timestamp 比對</td>
      </tr>
      <tr>
          <td>提解法</td>
          <td>加 cache + bump timeout</td>
          <td>應該先確認瓶頸真的在下載</td>
      </tr>
      <tr>
          <td>解法仍 fail</td>
          <td>假設「cache 沒 hit」</td>
          <td>應該意識到「同個 step 又頂到上限」是 hang 訊號</td>
      </tr>
  </tbody>
</table>
<p>每一步單看都合理、合起來就是把 false positive 越雕越精緻。這個 anatomy 對任何「初診沒驗證就改」的場景都適用、不限 CI。</p>
<hr>
<h2 id="3-wrap-的-r-在第二次-fail-時是-stop-訊號">3. WRAP 的 R 在第二次 fail 時是 stop 訊號</h2>
<p>WRAP 決策框架的 R（Reality Test）原則是「需要什麼事證才能證明這個方法可行？」。它不只是決策前的檢查、更是<strong>連續失敗後的 stop 訊號</strong>。</p>
<p>第二次 fail 時、繼續同方向加 timeout 是自動駕駛模式。WRAP 在這個位置該提醒的事：</p>
<ul>
<li>「兩次同類修法都沒解、是不是前置假設錯了？」</li>
<li>「我有沒有資料去判斷真正卡哪？」（資料充足度閘門）</li>
<li>「同類問題的 base rate 是什麼？」（基本率思考）</li>
</ul>
<p><strong>Stop 訊號的觸發條件是「同方向修法連續 fail 2 次」、不是「fail 3 次」</strong>。第二次就該回到資料層；第三次已經是浪費 cycle 而且強化錯誤假設。</p>
<p>實際上第二次 fail 後做的對的事是停下來、grep detailed log 的 timestamp 序列、發現「下載完成」跟「cancel」之間有 24 分鐘空白 — 這時才確認是 silent hang。如果第二次沒做這個轉折、第三次大概率是「換更大的 timeout」或「換不同的 cache key」、仍 fail。</p>
<hr>
<h2 id="4-detailed-log-的關鍵讀法找沒輸出的時間段">4. Detailed log 的關鍵讀法：找「沒輸出的時間段」</h2>
<p>CI 平台的 step log 通常很長、人眼掃容易跳過。看 silent hang 嫌疑時、讀法不是順序讀、是抓四個 timestamp：</p>
<ol>
<li><strong>Step 開始的 timestamp</strong>（log header 通常有）</li>
<li><strong>Step 結束（cancel / fail）的 timestamp</strong></li>
<li><strong>最後一行有意義輸出的 timestamp</strong></li>
<li>計算 #3 到 #2 之間的時間真空</li>
</ol>
<p>真空夠大（&gt; 1 分鐘）+ #3 是 happy log = silent hang 嫌疑高。</p>
<p>GitHub Actions 用 <code>gh</code> CLI 的具體做法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 取某個 step 的所有 log（filter step 名稱）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gh run view &lt;run-id&gt; --log --job &lt;job-id&gt; <span class="p">|</span> rg <span class="s2">&#34;Install Playwright browsers&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 抓最後幾行看真空尾巴</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gh run view &lt;run-id&gt; --log --job &lt;job-id&gt; <span class="p">|</span> rg <span class="s2">&#34;Install Playwright browsers&#34;</span> <span class="p">|</span> tail -3</span></span></code></pre></div><p>本案例的最後 3 行（簡化過）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">2026-05-27T09:59:44.110Z  | 100% of 170.4 MiB
</span></span><span class="line"><span class="ln">2</span><span class="cl">2026-05-27T10:24:15.201Z  ##[error]The operation was canceled.</span></span></code></pre></div><p>24 分 31 秒真空、最後一行 happy log 是「下載 100% 完成」 — silent hang 確認。</p>
<p>這個讀法的核心是「<strong>時間真空優先於訊息內容</strong>」。技術人員習慣讀訊息內容找 error keyword、但 silent hang 沒有 error keyword 可找、只有時間真空。轉個訊號類型才看得到。</p>
<hr>
<h2 id="5-upstream-issue-搜尋的優先序">5. Upstream issue 搜尋的優先序</h2>
<p>Silent hang 確認後、下一步通常<strong>不是繼續 reason 根因</strong>、是去查 upstream issue tracker。Silent hang 多半是工具 / 依賴的 bug、而非自己 config 錯 — 因為 config 錯通常有 error message、不會 silent。</p>
<p>查詢策略：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">gh api <span class="s1">&#39;search/issues?q=repo:&lt;upstream&gt;/&lt;repo&gt;+&lt;symptom keywords&gt;+is:issue&amp;per_page=10&amp;sort=updated&#39;</span></span></span></code></pre></div><p>關鍵是 <strong>keyword 選擇用「症狀詞」而不是「猜測詞」</strong>。症狀詞描述讀者實際觀察到的現象（<code>hangs after download</code>、<code>stuck during extract</code>），猜測詞描述讀者推測的根因（<code>slow</code>、<code>timeout</code>、<code>network issue</code>）。猜測詞會找到大量無關 issue；症狀詞通常直接命中。</p>
<p>本案例查詢 <code>playwright install hangs chromium</code> 第二筆結果就是 issue #41000、標題完全匹配「<code>playwright install chromium</code> hangs after download completes on Node.js 24.16.0 (extract-zip)」。Issue 詳情指向上游 <a href="https://github.com/nodejs/node/issues/63487">nodejs/node#63487</a>、給出兩個 workaround（升 Playwright 1.60.0 或 pin Node 24.15.0）。從查詢到確認根因、全程不到 5 分鐘。</p>
<h3 id="為什麼-issue-tracker-該優先於-self-reasoning">為什麼 issue tracker 該優先於 self-reasoning</h3>
<p>技術人員的 instinct 是「自己想出根因」。但 CI silent hang 這類問題、根因通常在工具版本、runtime 版本、OS、container image 的微妙交互、不在自己的 codebase。<strong>Reasoning 找不到的東西、社群 issue tracker 經常已經有人回報過</strong>。</p>
<p>「先 reason 再查」跟「先查再 reason」的取捨：</p>
<table>
  <thead>
      <tr>
          <th>問題範圍</th>
          <th>哪個優先</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自己 codebase 內的邏輯 bug</td>
          <td>reason</td>
          <td>自己最熟、reasoning 通常較快</td>
      </tr>
      <tr>
          <td>Upstream tool / runtime / OS / container 範圍</td>
          <td>查 issue</td>
          <td>自己沒上游知識、reasoning 容易卡在錯誤前置假設</td>
      </tr>
      <tr>
          <td>兩者交界（自己 config 觸發 upstream bug）</td>
          <td>並行</td>
          <td>先查找 known issue、同時 reason 自己 config</td>
      </tr>
  </tbody>
</table>
<p>Silent hang 預設屬於第二類、應該優先查 issue tracker。</p>
<hr>
<h2 id="6-整合訊號--行動-mapping">6. 整合：訊號 → 行動 mapping</h2>
<p>把本案例的經驗整理成可重用的訊號表：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Step timeout 且最後一行是 happy log</td>
          <td>計算 timestamp 真空、確認是否 silent hang</td>
      </tr>
      <tr>
          <td>同方向修法 2 次都 fail</td>
          <td>停止、回到資料層、不再加 timeout / retry</td>
      </tr>
      <tr>
          <td>Silent hang 確認</td>
          <td>用症狀詞查 upstream issue tracker</td>
      </tr>
      <tr>
          <td>Issue 命中且有 workaround</td>
          <td>套 workaround、不要先 reason</td>
      </tr>
      <tr>
          <td>Issue 沒命中</td>
          <td>才回到 self-debug、加 verbose log（<code>DEBUG=</code> env）</td>
      </tr>
  </tbody>
</table>
<p>這張表的順序很重要：每一步的「該做的事」是下一步的「前置條件」。略過任一步、後面的判斷會建立在錯誤假設上。</p>
<hr>
<h2 id="適用範圍">適用範圍</h2>
<p>「Silent log 是 happy log 的 anti-signal」這個原則對所有非互動 process（CI、cron job、background worker、container init）都適用：</p>
<ul>
<li><strong>Docker build 卡住</strong>（特別是 RUN apt-get / npm install / pip install）— 同類 silent hang 模式</li>
<li><strong>CI cache restore 卡住</strong> — 大量小檔案的 cache 操作可能 silent hang</li>
<li><strong>Database migration 卡住</strong> — schema 變更 + 長 transaction 可能 silent hang</li>
<li><strong>任何 process 跑時間接近 timeout 上限被 cancel</strong> — 先檢查是否 silent hang 才提解法</li>
</ul>
<p>「WRAP R 在第二次 fail 時是 stop 訊號」這條原則不限 CI、適用所有「同方向修法重複 fail」的場景：debug、設定調校、效能優化。</p>
<hr>
<h2 id="參考資料">參考資料</h2>
<ul>
<li><a href="https://github.com/microsoft/playwright/issues/41000">microsoft/playwright issue #41000</a> — 本案例的 upstream issue（Playwright 1.57-1.59 在 Node 24.16.0 extract-zip hang）</li>
<li><a href="https://github.com/nodejs/node/issues/63487">nodejs/node issue #63487</a> — Node 24.16 extract-zip / yauzl regression 上游</li>
<li>同 blog 文章：<a href="/blog/skills/wrap-decision/" data-link-title="WRAP 決策框架 — 認知偏誤防護與決策品質" data-link-desc="WRAP 決策框架的 blog 好讀版：用錨點確認、資料充足度、選項擴增、現實檢驗、機會成本、行前預想與絆腳索防止自動駕駛式決策。">WRAP 決策框架的 R 階段操作</a> — Reality Test 詳細用法</li>
</ul>
]]></content:encoded></item></channel></rss>