Haskellのブラケット関数が実行可能ファイルで機能するのに、テストでクリーンアップできないのはなぜですか?


10

Haskellのbracket関数が、stack runまたはstack testが使用されているかどうかによって異なる動作をするという非常に奇妙な動作を目にしています。

次のコードを検討してください。ネストされた2つのブラケットがDockerコンテナの作成とクリーンアップに使用されています。

module Main where

import Control.Concurrent
import Control.Exception
import System.Process

main :: IO ()
main = do
  bracket (callProcess "docker" ["run", "-d", "--name", "container1", "registry:2"])
          (\() -> do
              putStrLn "Outer release"
              callProcess "docker" ["rm", "-f", "container1"]
              putStrLn "Done with outer release"
          )
          (\() -> do
             bracket (callProcess "docker" ["run", "-d", "--name", "container2", "registry:2"])
                     (\() -> do
                         putStrLn "Inner release"
                         callProcess "docker" ["rm", "-f", "container2"]
                         putStrLn "Done with inner release"
                     )
                     (\() -> do
                         putStrLn "Inside both brackets, sleeping!"
                         threadDelay 300000000
                     )
          )

これをで実行しstack run、で中断するとCtrl+C、予期した出力が得られます。

Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release

そして、両方のDockerコンテナーが作成されてから削除されていることを確認できます。

ただし、このまったく同じコードをテストに貼り付けて実行するstack testと、最初のクリーンアップ(の一部)のみが発生します。

Inside both brackets, sleeping!
^CInner release
container2

これにより、Dockerコンテナーが私のマシンで実行されたままになります。どうしたの?


スタックテストはスレッドを使用しますか?
カール・

1
よく分かりません。私は興味深い事実に気づきました。実際にコンパイルされたテスト実行可能ファイルを掘り下げ.stack-workて直接実行した場合、問題は発生しません。で実行してstack testいるときにのみ発生します。
トム

何が起こっているのかは推測できますが、スタックはまったく使用していません。行動に基づく推測にすぎません。1)stack testテストを処理するためにワーカースレッドを開始します。2)SIGINTハンドラーがメインスレッドを強制終了します。3)メインスレッドが終了すると、Haskellプログラムは終了し、追加のスレッドは無視されます。2は、GHCによってコンパイルされたプログラムのSIGINTでのデフォルトの動作です。3は、Haskellでスレッドが機能する方法です。1は完全な推測です。
カール・

回答:


6

を使用するとstack run、Stackはexecシステムコールを効果的に使用して実行可能ファイルに制御を移すため、シェルから直接実行可能ファイルを実行する場合と同じように、新しい実行可能ファイルのプロセスが実行中のStackプロセスを置き換えます。プロセスツリーは次のようになりstack runます。特に、実行可能ファイルはBashシェルの直接の子であることに注意してください。さらに重要なことに、端末のフォアグラウンドプロセスグループ(TPGID)は17996であり、そのプロセスグループ(PGID)内の唯一のプロセスはbracket-test-exeプロセスです。

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13816 13831 13831 13831 pts/3    17996 Ss    2001   0:00  |       \_ /bin/bash --noediting -i
13831 17996 17996 13831 pts/3    17996 Sl+   2001   0:00  |       |   \_ .../.stack-work/.../bracket-test-exe

その結果、Ctrl-Cを押しstack runてシェルの下またはシェルから直接実行中のプロセスを中断すると、SIGINTシグナルはbracket-test-exeプロセスにのみ配信されます。これにより、非同期UserInterrupt例外が発生します。方法bracketは、次の場合に機能します。

bracket
  acquire
  (\() -> release)
  (\() -> body)

は、処理中に非同期例外を受け取り、body実行してreleaseから、例外を再度発生させます。ネストされたbracket呼び出しでは、これは内部ボディを中断し、内部リリースを処理し、例外を再発生させて外部ボディを中断し、最後に例外を再発生させてプログラムを終了するという効果があります。(関数の外側bracketに続いてさらにアクションがある場合main、それらは実行されません。)

一方、を使用するとstack test、StackはwithProcessWaitをして、実行可能ファイルをプロセスの子プロセスとして起動しstack testます。次のプロセスツリーでbracket-test-testは、がの子プロセスであることに注意してくださいstack test。重要なのは、ターミナルのフォアグラウンドプロセスグループは18050であり、そのプロセスグループにはstack testプロセスとプロセスの両方が含まれているbracket-test-testことです。

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13816 13831 13831 13831 pts/3    18050 Ss    2001   0:00  |       \_ /bin/bash --noediting -i
13831 18050 18050 13831 pts/3    18050 Sl+   2001   0:00  |       |   \_ stack test
18050 18060 18050 13831 pts/3    18050 Sl+   2001   0:00  |       |       \_ .../.stack-work/.../bracket-test-test

端末でCtrl-Cを押すと、端末のフォアグラウンドプロセスグループのすべてのプロセスにSIGINTシグナルが送信されるため、両方のstack testbracket-test-testのシグナルがシグナルを取得します。 bracket-test-test上記のように信号の処理とファイナライザの実行を開始します。ただし、ここで競合状態が発生します。これstack testは、中断されると、途中でwithProcessWait次のように定義されるためです。

withProcessWait config f =
  bracket
    (startProcess config)
    stopProcess
    (\p -> f p <* waitExitCode p)

そのため、bracket割り込みをstopProcess受けると、SIGTERMシグナルを送信して子プロセスを終了するを呼び出します。と対照的にSIGINT、これは非同期例外を発生させません。通常はファイナライザの実行が完了する前に、子をただちに終了させます。

これを回避する特に簡単な方法は考えられません。1つの方法は、の機能を使用してSystem.Posix、プロセスを独自のプロセスグループに入れることです。

main :: IO ()
main = do
  -- save old terminal foreground process group
  oldpgid <- getTerminalProcessGroupID (Fd 2)
  -- get our PID
  mypid <- getProcessID
  let -- put us in our own foreground process group
      handleInt  = setTerminalProcessGroupID (Fd 2) mypid >> createProcessGroupFor mypid
      -- restore the old foreground process gorup
      releaseInt = setTerminalProcessGroupID (Fd 2) oldpgid
  bracket
    (handleInt >> putStrLn "acquire")
    (\() -> threadDelay 1000000 >> putStrLn "release" >> releaseInt)
    (\() -> putStrLn "between" >> threadDelay 60000000)
  putStrLn "finished"

Ctrl-Cを押すと、SIGINTが bracket-test-testプロセスにます。クリーンアップし、元のフォアグラウンドプロセスグループを復元してプロセスをポイントし、stack test終了します。これにより、テストが失敗し、stack test実行が継続されます。

SIGTERMstack test方法としては、プロセスが終了した後でも、を処理して子プロセスを実行し続け、クリーンアップを実行することもできます。シェルプロンプトを見ている間、プロセスがバックグラウンドでクリーンアップされるので、これは一種の見苦しいものです。


詳しい回答ありがとうございます!参考までに、私はこれについてStackバグをgithub.com/commercialhaskell/stack/issues/5144に提出しました。本当の修正はstack testdelegate_ctlcからのオプションSystem.Process(または類似のもの)でプロセスを起動することです。
トム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.