Deno.addSignalListener を実装した話
Deno (ディノ) Advent Calendar 2021 24日目の記事です。
今日は Deno.addSignalListener()
を実装した話とこの API に至るまでの経緯の話をします。
概要
Deno.addSignalListener
は Deno でプログラムへの signal をハンドリングする為の API です。下のような使い方で、プログラムに signal が送信されたタイミングでハンドリングすることが出来ます。
const handler = () => {
console.log("SIGINT をキャッチ!");
};
Deno.addSignalListener("SIGINT", handler);
なお、ハンドリングを止める場合は、
Deno.removeSignalListener("SIGINT", handler);
と、なります。
元々は Deno.signal(): AsyncIterableIterator<void>
という API として実装されていましたが、議論を経て、AsyncIterable
を返すデザインは良くないという話になり、Deno v1.14.0 のタイミングで、自分が callback の形式のデザインに作り変えました。denoland/deno#12512
実装作業
Deno は、外部環境へ影響を及ぼす操作をしたい場合は、V8 の中から Op という命令の単位を発行して、Deno runtime へやりたい操作を要求するという仕組みを持っています。signal のハンドリングも、この「外部環境への影響」に該当するため、専用の Op を定義する事で機能が成り立っています。signal のハンドリングに関する Op は以下3つです。
-
op_signal_bind
signal 監視を開始する -
op_signal_poll
signal を待つ -
op_signal_unbind
signal 監視を終了する
上の3つの Op は以前の AsyncIterable 型 API を実装するために作られた Op ですが、今回はこれらを全て再利用できないかという方針で実装を開始しました。作業を始めてみると、これらの Op で callback 型の API に作り替えられることが分かり、上記の PR を提出することができました (したがって、この作業の中では Rust 側のコードは全く書き換えていません)。
デザインに関する議論は PR 作成前にほとんど完了していたため、大きなレビュー論点は無く、細かい点を調整した後に、無事マージする事が出来ました。なお、自分は Deno Company のメンバー人格のため、マージはレビュワーではなく、自分で行います。Deno Company では、メンバーになった場合は基本的にセルフマージするというルールがあります。なお、マージは基本的には Squash Merge を使う事などもルール化されています。
API のデザイン議論
PR の時点ではデザインの議論は既に終わっていましたが、そこに至るまでの議論は自分が 2021年6月に立てた issue (denoland/deno#11158) 上で行っていました。issue の中では以下のような幾つかの案が出ていました。
Deno.onSignal("SIGINT", callback)
Deno.addSignalListener("SIGINT", callback)
Deno.addEventListener("SIGINT", callback)
window.addEventListener("SIGINT", callback)
Deno.onSignal
は主に Ryan が推していました。Ryan は特に API 名が短い点が重要と考えているようでした。signal にバインドする場合はこの API が最も簡潔に見えるのですが、では、この API から signal イベントへのバインドを止める場合はどうするか、と考えた時に、誰にも分かりやすいであろうというデザインがないという点がネックになり、採用されませんでした。
Deno.addSignalListener
はコアメンバーの Bartek と Satya が独立に提案した案でした。最初にこの案を聞いた時は、個人的に、addEventListener の紛いもの感があって微妙という印象を持ったのですが、最初に Bartek が standup で提案した後に、それと独立で Satya も 1:1 で同じ案を提案し、独立して2人から同案が出てきた事で、結構妥当な案かもしれないという印象になりました。また、この案は、addEventListener
とのアナロジーから、ハンドリングを止める為の API が自動的に removeSignalListener
になるという点が Deno.onSignal
案よりも優れていました。
Deno.addEventListener
は自分が当初思いついた案でしたが、Deno は既に global object が addEventListener API を持っており、Deno.addEventListener
と window.addEventListener
の両方があると、「あのイベントはどっちの addEventListener だっけ?」という混乱が生じるという反対意見が出てきて、却下となりました。
window.addEventListener
案は主に Bert が提案していた案でした。既に EventTarget を実装している window (Deno の global object) に特殊なイベントである SIGINT
など signal を表すイベント名を追加するという案です。自分も上の Deno.addEventListener
案が却下された後はこの案が本命というイメージでした。この案は、Web Spec に詳しい Luca と Andreu の2人から主に反対されて却下されることになりました。この2人の主張は主に Web Spec の観点から、この addEventListener
の拡張はおかしいという主張で、EventTarget の Spec の中で、イベントにバインドする事で副作用が起こってはならないという記述がある事が反対の主な根拠になっていました。
つまり、どういう事かと言うと、SIGINT をプログラムが受け取った時のデフォルトの挙動は、プログラムの異常終了です。しかし、SIGINT の監視を始める事で、このデフォルト挙動は無くなってしまい、SIGINT でどうするかはプログラマーが決める事になります。すなわち、signal イベントにバインドするだけでプログラムの挙動が変わるという副作用が起きるという事です。そして、DOM の Spec ではそういう事が起こってはならないという規定があります (2.8. Observing event listeners)。
言われてみればそうだなと思うとともに、良くこんな観点がすぐに出てくるなという点に非常に感心した記憶があります。
上記の観点で、window.addEventListener
案が却下になると、消去法で Deno.addSignalListener
だけが残り、結局この案が採用される事になりました。
旧 Deno.signal API について
上のデザインになる前の、Deno.signal というデザインは、最終的には Core メンバーから相当批判を受けてしまう悪いデザインだったのですが、では、なぜ以前このデザインを選んでしまったのでしょうか?
元々の AsyncIterable を使う案は、tokio のモジュールの tokio::signal の API デザインから由来しています。tokio::singal が提供する Signal struct は Stream trait を実装しており、まさに AsyncIterable と同じような使い方の API です (なお、Deno は内部的にこの tokio の signal の API を使って signal 機能を実装しています)。実は当初から、この API デザインは間違いだという事を keroxp さんや zekth さんが強く主張していました。また、自分が Deno.signal を実装する前に、そのプロトタイプとなる Deno.sigaction を実装していた Kevin も callback 型の API を PR で出していました。しかし、当時2019年ごろは、おそらく Node との差別化を図りたいという意図も働いて、event handler 型の API はなるべく AsyncIterable に解釈し直してデザインするという傾向が強くあったように思います (最初の HTTP server も AsyncIterable でデザインされており、後に event handler に変わるという同様の経緯を辿りました)。そういう当時の雰囲気もあり、Ryan, Kitson などコアメンバーが event handler 型よりも AsyncIterable 型を推していたという経緯がありました。
この辺りの最初のバージョンの Deno.signal API を実装した話は、Deno Advent Calendar 2019年 20日目 Deno に signal handler の API を実装した話 でも詳しく解説しています。
まとめ
今日は Deno.addSignalListener
という API が出来るまでの経緯について紹介しました。
Discussion
個人的には EventTarget の拡張として実装してほしかったんで、謎が解けました。