Saya melihat perilaku yang sangat aneh di mana bracket
fungsi Haskell berperilaku berbeda tergantung pada apakah stack run
atau stack test
tidak digunakan.
Pertimbangkan kode berikut, di mana dua tanda kurung bersarang digunakan untuk membuat dan membersihkan wadah 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
)
)
Ketika saya menjalankan ini dengan stack run
dan menyela dengan Ctrl+C
, saya mendapatkan hasil yang diharapkan:
Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release
Dan saya dapat memverifikasi bahwa kedua wadah Docker dibuat dan kemudian dihapus.
Namun, jika saya menempelkan kode yang sama persis ini ke dalam pengujian dan menjalankan stack test
, hanya (bagian dari) pembersihan pertama terjadi:
Inside both brackets, sleeping!
^CInner release
container2
Ini menghasilkan wadah Docker dibiarkan berjalan di mesin saya. Apa yang sedang terjadi?
- Saya sudah memastikan bahwa hal yang sama
ghc-options
diteruskan ke keduanya. - Repo demonstrasi lengkap di sini: https://github.com/thomasjm/bracket-issue
.stack-work
dan menjalankannya langsung, maka masalahnya tidak terjadi. Itu hanya terjadi ketika berjalan di bawahstack test
.stack test
memulai utas pekerja untuk menangani tes. 2) pengendali SIGINT membunuh utas utama. 3) Program Haskell berakhir ketika utas utama tidak, mengabaikan utas tambahan. 2 adalah perilaku default pada SIGINT untuk program yang dikompilasi oleh GHC. 3 adalah cara kerja utas di Haskell. 1 adalah tebakan lengkap.Jawaban:
Ketika Anda menggunakan
stack run
, Stack secara efektif menggunakanexec
system call untuk mentransfer kontrol ke executable, sehingga proses untuk executable baru menggantikan proses Stack yang berjalan, sama seperti jika Anda menjalankan executable langsung dari shell. Inilah yang tampak setelah pohon prosesstack run
. Perhatikan khususnya bahwa executable adalah anak langsung dari Bash shell. Lebih kritis lagi, perhatikan bahwa grup proses latar depan terminal (TPGID) adalah 17996, dan satu-satunya proses dalam kelompok proses (PGID) adalahbracket-test-exe
proses.Akibatnya, ketika Anda menekan Ctrl-C untuk menghentikan proses yang berjalan di bawah
stack run
atau langsung dari shell, sinyal SIGINT dikirim hanya kebracket-test-exe
proses. Ini menimbulkanUserInterrupt
pengecualian asinkron . Carabracket
kerjanya, saat:menerima pengecualian asinkron saat memproses
body
, itu berjalanrelease
dan kemudian memunculkan kembali pengecualian. Denganbracket
panggilan bersarang Anda , ini memiliki efek mengganggu tubuh bagian dalam, memproses pelepasan bagian dalam, meningkatkan kembali pengecualian untuk mengganggu bagian luar, dan memproses pelepasan bagian luar, dan akhirnya kembali meningkatkan pengecualian untuk menghentikan program. (Jika ada lebih banyak tindakan mengikuti bagian luarbracket
dalammain
fungsi Anda , mereka tidak akan dieksekusi.)Di sisi lain, ketika Anda menggunakan
stack test
, Stack menggunakanwithProcessWait
untuk meluncurkan executable sebagai proses anak daristack test
proses. Di pohon proses berikut, perhatikan bahwa itubracket-test-test
adalah proses anak daristack test
. Secara kritis, grup proses latar depan terminal adalah 18050, dan grup proses tersebut mencakupstack test
proses danbracket-test-test
proses.Ketika Anda menekan Ctrl-C di terminal, sinyal SIGINT dikirim ke semua proses dalam kelompok proses latar depan terminal sehingga keduanya
stack test
danbracket-test-test
mendapatkan sinyal.bracket-test-test
akan mulai memproses sinyal dan menjalankan finalizer seperti dijelaskan di atas. Namun, ada kondisi balapan di sini karena ketikastack test
terganggu, ia berada di tengah-tengahwithProcessWait
yang didefinisikan kurang lebih sebagai berikut:jadi, ketika
bracket
terganggu, ia memanggilstopProcess
yang menghentikan proses anak dengan mengirimkannyaSIGTERM
sinyal. Dalam konstruksinyaSIGINT
, ini tidak menimbulkan pengecualian asinkron. Itu hanya mengakhiri anak segera, umumnya sebelum dapat menyelesaikan menjalankan setiap finalis.Saya tidak bisa memikirkan cara yang sangat mudah untuk mengatasi hal ini. Salah satu caranya adalah dengan menggunakan fasilitas
System.Posix
untuk menempatkan proses ke dalam kelompok prosesnya sendiri:Sekarang, Ctrl-C akan menghasilkan SIGINT yang dikirimkan hanya ke
bracket-test-test
proses. Ini akan membersihkan, mengembalikan grup proses latar depan asli untuk menunjuk kestack test
proses, dan mengakhiri. Ini akan menghasilkan tes gagal, danstack test
hanya akan terus berjalan.Alternatifnya adalah mencoba untuk menangani
SIGTERM
dan menjaga proses anak berjalan untuk melakukan pembersihan, bahkan setelahstack test
proses tersebut dihentikan. Ini agak jelek karena prosesnya akan seperti membersihkan di latar belakang saat Anda melihat prompt shell.sumber
stack test
memulai proses dengandelegate_ctlc
opsi dariSystem.Process
(atau yang serupa).