Haskell의 대괄호 기능이 실행 파일에서 작동하지만 테스트에서 정리되지 않는 이유는 무엇입니까?


10

나는 하스켈의 아주 이상한 행동을보고 있어요 bracket함수가 다르게 행동 여부에 따라 있습니다 stack run또는 stack test사용됩니다.

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 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 testStack은 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 비동기 예외는 발생하지 않습니다. 일반적으로 종료자를 실행하기 전에 자식을 즉시 종료합니다.

이 문제를 해결하는 특히 쉬운 방법은 생각할 수 없습니다. 한 가지 방법은 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계속 실행됩니다.

대안은 프로세스가 종료 된 SIGTERM후에도 프로세스를 처리하고 하위 프로세스를 실행하여 정리를 수행하는 stack test것입니다. 쉘 프롬프트를 보면서 프로세스가 백그라운드에서 정리되기 때문에 이것은 추악합니다.


자세한 답변 감사합니다! 참고로 여기에 github.com/commercialhaskell/stack/issues/5144 에 대한 스택 버그를 제출했습니다 . 실제 수정은 옵션 (또는 비슷한 것)으로 stack test프로세스를 시작 하는 것 같습니다 . delegate_ctlcSystem.Process
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.