読者です 読者をやめる 読者になる 読者になる

非同期プロセスと sleep-forに関するメモ

emacs elisp test

emacs - Elisp: sleep-for doesn't block when running a test in ert - Stack Overflow


でやり取りを行ったことについてのメモ。

問題

通常であれば非同期プロセスを使う際、sleepする必要なんてないと
思うんですが、テストを書く場合は完了を待ちたいという場合がある
かと思います。で、start-processでプロセスを起動、その後すぐに
sleep-forをすると指定した時間 sleepしてくれないという問題が
ありました。ドキュメントには "guarantee a delay"と書いており、
なんで、っていうのがスタート地点でした。

(ert-deftest timetest ()
  (let ((now (cadr (current-time))))
    (start-process "echo" "*echo*" "echo" "hello world")
    (sleep-for 5)
    (should (< now (- (cadr (current-time)) 3)))))

このようなテストを行うと、ほとんどの場合テストが失敗します。

原因

原因はプロセスが終了するとき、sleep-forが中断してしまうためです。
ほとんどの場合というのは、sleep-forの前にプロセスが終了すると、
sleepが中断されないためです。


なぜこのような問題が起こるのかは不明なのです。
sleep-forの実装を見たところ複雑で、すぐにわかるという感じでは
ありませんでした。プロセス終了時に何かしらの eventが飛んできて、
叩き起こされるというイメージでしょうか。

解決

上記のようなテストケースでは以下のように対応することが
できます。叩き起こされていもいいように dummyの sleepを追加しました。

(ert-deftest timetest ()
  (let ((now (cadr (current-time))))
    (start-process "echo" "*echo*" "echo" "hello world")
    (sleep-for 1) ;; この sleepはおそらく叩き起こされる
    (sleep-for 5) ;; もう叩き起こされることはないので 5秒 sleepできる
    (should (< now (- (cadr (current-time)) 3)))))

ただこれは脆いです。n個の非同期プロセスの終了が発生してしまうと
対応出来ません。そのため現状は以下のようにしておくのが無難なのでは
ないかという結論にいたりました。

(ert-deftest timetest ()
  (let ((now (float-time))  ;; current-timeより float-timeの方がベター
        (process-connection-type nil))
    (start-process "tmp" "*tmp*" "bash" "-c" "sleep 1; echo hi")
    (while (< (- (float-time) now) 5) ;; 定期的に時間を確認
      (sleep-for 1))
    (should (< now (- (float-time) 3)))))

定期的に時間を確認し、期待する時間に達していなかったら再度 sleep
し、また時間を確認ということを繰り返します。これだと何度叩き起こされても
期待する時間まで眠れます。


process-connection-typeについてはよくわかっていないのですが、
このようなケースでは nilにしておくのがよさそうな感じです。
擬似端末使うか、PIPE使うかの選択みたいなんですが、擬似端末だと
一定量書くと、プロセス終了と同様の問題が起きるっぽいです。
(こちらについては詳しく見ていません。)

おわりに

正式な解決方法をご存知の方がいたら、教えていただければと思います。