← ./articles-ja

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もします。

  1. 小さなupdateを高速に多数書く
  2. 大きなupdateを書く
  3. 大きなupdate中もeventが続くことを確認する
  4. shutdown時に残りdataをdrainまたは保持できることを確認する

参考