工作時碰到了某個以 Node.js 撰寫的 scheduled job(排程工作)一直保持在 running 的狀態,永遠不會進到 complete。這時一般首先想到的是,程式可能在某個地方卡住了,進入無窮等待(像是進入無限迴圈一樣)。
一開始我試著使用懶人 debug 方法:加入幾個 console.log
看看 code 是跑到哪裡停住了。
結果,到程式碼最後一個 console.log
也有被執行到。
但程式依然沒有結束。
如果你是個對 JavaScript 這個語言的運行機制很熟的人,馬上就知道這問題在哪了。
但因為我在這之前我寫的都是其他語言,JavaScript 頂多看看語法差異就上陣了,也因此忽略了 JavaScript 一個很重要的特性:
Event Loop
Event Loop 其實就是 JavaScript 執行 Asynchronous 程式碼片段的方式:
當 Node.js 執行程式時,會把主程式先執行完。這之間碰到 async code 的話,會將 async code 交給另外的 worker 處理,並在 queue 註冊 callback,主程式則繼續往下進行。
之後就會進入 Event Loop 不斷的監聽 callback,若有 worker 完成,則會將 callback 交給主程式執行。
也因此,Node.js 的 async functions 不會 block 主程式進行,而會丟給背後的 worker 來跑。Worker 和主程式不一樣,可以是多個 Threads,可以達到平行化,也因此 Node.js 鼓勵使用 asynchronous 的方式撰寫。
註:這是我個人簡化後的版本,若有觀念上的錯誤歡迎指正。
Quick Example
這算是蠻經典的例子:
// index.js
setTimeout(() => console.log('callback'), 0)
console.log('end of code')
❯ node index.js
end of code
callback
因為 setTimeout
是 async,會註冊 callback,即便等待時間是 0,也會在主程式跑完後才執行。這也是我一開始 debug 的方法沒成功的原因。
回到正題:我的 Node.js Process 怎麼不會結束?
就是有 worker 還在工作,導致 event loop 一直在等 callback 可以被放進 call stack。
找出兇手
既然知道原因了,下一步就是找出是哪個 async code 沒有結束一直在等待。
Node.js 有兩個一直沒有被官方列入文件的方法,可以快速查看現在還在 queue 等待的 work 有哪些:
console.log(process._getActiveHandles())
console.log(process._getActiveRequests())
這兩個底線開頭的方法直到最近還有 issue 在討論要不要加入文件,但看起來就是無限延長,畢竟這個方法印出的結果也是蠻冗長的,包含了很多給人類 debug 時不需要的資訊。
因此我找到了一個 package:
Why Is Node Running
why-is-node-running 是一個利用 Node.js 實驗中的 Async Hooks API 撰寫的 library,可以幫你調查你的 Node.js 程式為什麼不會結束。
原理是在 require
時,利用 side effects 註冊一些 Async Hooks,這樣當 async code 被執行時,這個 module 就會知道,也可以因此監聽有哪些 Code 沒結束。
用法也蠻簡單的:
const log = require('why-is-node-running') // 一定要一開始就 require,讓 side effects 優先執行
setInterval(function () {}, 1000)
log() // 印出目前還沒有結束的 work
❯ node index.js
There are 1 handle(s) keeping the process running
# Timeout
/Users/davidye/Projects/node_running/index.js:3 - setInterval(function () {}, 1000)
參考資料
實際上關於 Event Loop 還有很多細節,也有很多已經寫得不錯的文章,列在下方給大家做為參考。
官方關於 Event Loop 的說明文件
- The Node.js Event Loop, Timers, and process.nextTick() | Node.js
- Don’t Block the Event Loop (or the Worker Pool)
- The Node.js Event Loop
Event Loop 好文
- JavaScript 中的同步與非同步(上):先成為 callback 大師吧!
- 完整圖解 Node.js 的 Event Loop (事件迴圈)
- Event Loop 運行機制解析 - Node.js 篇
- 非同步程式碼之霧:Node.js 的事件迴圈與 EventEmitter
其他參考資料
- How does a node.js process know when to stop?
- How can I get a list of callbacks in the Node work queue? (or, Why won’t Node exit?)