WindowsのRust notify watcher callback内で無限にdrainしない
file watcherのcallbackは、つい仕事をしたくなる場所です。fileが変わったので、新しいdataを読み、parseし、stateを更新し、bufferが空になるまでloopしたくなります。
Windowsでは、それで notify watcherが固まったように見えることがあります。callback pathはevent delivery pathであり、無制限のparseをする場所ではありません。
anti-pattern
危険な形です。
let mut watcher = notify::recommended_watcher(move |event| {
if event.is_err() {
return;
}
loop {
match read_next_complete_chunk() {
Some(chunk) => process_chunk(chunk),
None => break,
}
}
})?;
process_chunk が遅い、または read_next_complete_chunk がbuffered dataを見つけ続けると、callbackが返りません。その間、新しいfile-system eventは後ろで待ちます。
watcherがeventを受け取れなくなったのではなく、callbackが戻らなくなっています。
callbackごとの処理をboundedにする
1 eventにつき1 chunkにします。
let mut watcher = notify::recommended_watcher(move |event| {
if event.is_err() {
return;
}
if let Some(chunk) = read_next_complete_chunk() {
process_chunk(chunk);
}
})?;
これでwatcherへすぐ制御を返せます。追加writeが来れば、次のeventで次のchunkを処理できます。
重い処理はthreadやasync taskへ逃がす
重いparseはqueueへ送ります。
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
std::thread::spawn(move || {
while let Ok(chunk) = rx.recv() {
process_chunk(chunk);
}
});
let mut watcher = notify::recommended_watcher(move |event| {
if event.is_err() {
return;
}
if let Some(chunk) = read_next_complete_chunk() {
let _ = tx.send(chunk);
}
})?;
callbackは小さく保ち、worker側でparse、batch、debounce、state updateを行います。
trailing dataが待つtrade-off
1 callback 1 chunkにすると、最後のwriteの後に次eventが来ない場合、buffered dataが待つことがあります。
timerやflush pathで補います。
watcher callback
-> one chunkを送る
periodic timer
-> final complete chunkを確認する
shutdown
-> 残りcomplete dataをdrainする
重要なのはcallback自体をboundedに保つことです。
debounceとは別問題
debounceはevent burstをまとめます。bounded callbackはevent delivery pathを占有し続けないためのものです。
両方必要なことがあります。
notify callback
-> small signalをすぐenqueue
-> callback外でdebounce/batch
-> workerでparse/process
callback内でsleepしてdebounceしてはいけません。それもevent deliveryを塞ぎます。
検証チェック
watcher callbackを確認します。
- strict limitのない
loop - callback内のlarge file parse
- blocking network/database call
- callback内のsleepや長いdebounce
- heavy recomputationを起こすUI state update
- file read/parse中にlockを保持
load testもします。
- 小さなupdateを高速に多数書く
- 大きなupdateを書く
- 大きなupdate中もeventが続くことを確認する
- shutdown時に残りdataをdrainまたは保持できることを確認する