Tại sao chức năng khung của Haskell hoạt động trong các tệp thực thi nhưng không dọn sạch trong các thử nghiệm?


10

Tôi nhìn thấy một hành vi rất kỳ lạ nơi Haskell của bracketchức năng là hành xử khác nhau tùy thuộc vào việc stack runhay stack testđược sử dụng.

Hãy xem xét đoạn mã sau, trong đó hai dấu ngoặc lồng được sử dụng để tạo và dọn sạch các container 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
                     )
          )

Khi tôi chạy cái này stack runvà ngắt với Ctrl+C, tôi nhận được kết quả mong đợi:

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

Và tôi có thể xác minh rằng cả hai container Docker được tạo và sau đó loại bỏ.

Tuy nhiên, nếu tôi dán chính xác mã này vào một bài kiểm tra và chạy stack test, chỉ (một phần) lần dọn dẹp đầu tiên xảy ra:

Inside both brackets, sleeping!
^CInner release
container2

Điều này dẫn đến một container Docker còn chạy trên máy của tôi. Chuyện gì đang xảy ra vậy?


Kiểm tra ngăn xếp có sử dụng chủ đề?
Carl

1
Tôi không chắc. Tôi nhận thấy một sự thật thú vị: nếu tôi đào thử nghiệm thực tế được biên dịch thực thi bên dưới .stack-workvà chạy trực tiếp, thì vấn đề sẽ không xảy ra. Nó chỉ xảy ra khi chạy dưới stack test.
tom

Tôi có thể đoán những gì đang xảy ra, nhưng tôi hoàn toàn không sử dụng stack. Đó chỉ là dự đoán dựa trên hành vi. 1) stack testbắt đầu các luồng công nhân để xử lý các bài kiểm tra. 2) trình xử lý SIGINT giết chết luồng chính. 3) Các chương trình Haskell chấm dứt khi luồng chính thực hiện, bỏ qua mọi luồng bổ sung. 2 là hành vi mặc định trên SIGINT cho các chương trình được biên soạn bởi GHC. 3 là cách các chủ đề hoạt động trong Haskell. 1 là một phỏng đoán hoàn chỉnh.
Carl

Câu trả lời:


6

Khi bạn sử dụng stack run, Stack sử dụng hiệu quả execlệnh gọi hệ thống để chuyển điều khiển sang tệp thực thi, do đó, quy trình cho tệp thực thi mới thay thế quy trình Stack đang chạy, giống như khi bạn chạy tệp thực thi trực tiếp từ trình bao. Đây là những gì cây quá trình trông giống như sau stack run. Đặc biệt lưu ý rằng tệp thực thi là con trực tiếp của shell Bash. Quan trọng hơn, lưu ý rằng nhóm quy trình tiền cảnh của thiết bị đầu cuối (TPGID) là 17996 và quy trình duy nhất trong nhóm quy trình đó (PGID) là bracket-test-exequy trình.

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

Kết quả là, khi bạn nhấn Ctrl-C để làm gián đoạn quá trình đang chạy dưới stack runhoặc trực tiếp từ trình bao, tín hiệu SIGINT chỉ được gửi đến bracket-test-exequy trình. Điều này đặt ra một UserInterruptngoại lệ không đồng bộ . Cách thức brackethoạt động, khi:

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

nhận được một ngoại lệ không đồng bộ trong khi xử lý body, nó chạy releasevà sau đó tăng lại ngoại lệ. Với các bracketcuộc gọi lồng nhau của bạn , điều này có tác dụng làm gián đoạn cơ thể bên trong, xử lý bản phát hành bên trong, nâng cao ngoại lệ để làm gián đoạn cơ thể bên ngoài và xử lý bản phát hành bên ngoài và cuối cùng nâng lại ngoại lệ để chấm dứt chương trình. (Nếu có nhiều hành động tiếp theo bên ngoài brackettrong mainchức năng của bạn , chúng sẽ không được thực thi.)

Mặt khác, khi bạn sử dụng stack test, Stack sử dụng withProcessWaitđể khởi chạy chương trình thực thi như một tiến trình con của stack testquy trình. Trong cây quy trình sau, lưu ý rằng đó bracket-test-testlà một quá trình con của stack test. Quan trọng, nhóm quy trình tiền cảnh của thiết bị đầu cuối là 18050 và nhóm quy trình đó bao gồm cả stack testquy trình và bracket-test-testquy trình.

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

Khi bạn nhấn Ctrl-C trong thiết bị đầu cuối, tín hiệu SIGINT được gửi đến tất cả các quy trình trong nhóm quy trình tiền cảnh của thiết bị đầu cuối để cả hai stack testbracket-test-testnhận được tín hiệu. bracket-test-testsẽ bắt đầu xử lý tín hiệu và chạy bộ hoàn thiện như mô tả ở trên. Tuy nhiên, có một điều kiện cuộc đua ở đây bởi vì khi stack testbị gián đoạn, nó ở giữa withProcessWaitđược xác định ít nhiều như sau:

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

vì vậy, khi nó bracketbị gián đoạn, nó gọi stopProcesskết thúc tiến trình con bằng cách gửi SIGTERMtín hiệu. Tương tự SIGINT, điều này không gây ra ngoại lệ không đồng bộ. Nó chỉ chấm dứt đứa trẻ ngay lập tức, nói chung trước khi nó có thể hoàn thành việc chạy bất kỳ quyết toán nào.

Tôi không thể nghĩ ra một cách đặc biệt dễ dàng để giải quyết vấn đề này. Một cách là sử dụng các phương tiện System.Posixđể đưa quy trình vào nhóm quy trình riêng của mình:

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"

Bây giờ, Ctrl-C sẽ dẫn đến SIGINT chỉ được gửi đến bracket-test-testquy trình. Nó sẽ dọn sạch, khôi phục nhóm quy trình tiền cảnh ban đầu để trỏ đến stack testquy trình và chấm dứt. Điều này sẽ dẫn đến thử nghiệm thất bại, và stack testsẽ tiếp tục chạy.

Một cách khác là cố gắng xử lý SIGTERMvà giữ cho tiến trình con chạy để thực hiện dọn dẹp, ngay cả khi stack testquá trình đã kết thúc. Điều này là xấu vì quy trình sẽ được làm sạch trong nền trong khi bạn đang nhìn vào dấu nhắc shell.


Cảm ơn các câu trả lời chi tiết! FYI Tôi đã gửi một lỗi Stack về vấn đề này ở đây: github.com/commIALhaskell/stack/issues/5144 . Có vẻ như sửa chữa thực sự sẽ là stack testđể khởi chạy các quy trình với delegate_ctlctùy chọn từ System.Process(hoặc một cái gì đó tương tự).
tom
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.