Widzę bardzo dziwne zachowanie, w którym bracket
funkcja Haskella zachowuje się różnie, w zależności od tego, stack run
czy stack test
jest używana.
Rozważ następujący kod, w którym dwa zagnieżdżone nawiasy klamrowe są używane do tworzenia i czyszczenia kontenerów 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
)
)
Kiedy uruchamiam to stack run
i przerywam Ctrl+C
, otrzymuję oczekiwany wynik:
Inside both brackets, sleeping!
^CInner release
container2
Done with inner release
Outer release
container1
Done with outer release
I mogę zweryfikować, że oba kontenery Docker są tworzone, a następnie usuwane.
Jeśli jednak wkleję ten sam kod do testu i uruchomię stack test
, nastąpi tylko (część) pierwszego czyszczenia:
Inside both brackets, sleeping!
^CInner release
container2
Powoduje to, że na moim komputerze nadal działa kontener Docker. Co się dzieje?
- Upewniłem się, że dokładnie to samo
ghc-options
jest przekazywane do obu. - Pełne repozytorium demonstracyjne tutaj: https://github.com/thomasjm/bracket-issue
haskell
haskell-stack
Tomek
źródło
źródło
.stack-work
i uruchomię go bezpośrednio, problem się nie pojawi. Zdarza się to tylko podczas biegania podstack test
.stack test
uruchamia wątki robocze do obsługi testów. 2) moduł obsługi SIGINT zabija główny wątek. 3) Programy Haskell kończą się, gdy robi to główny wątek, ignorując wszelkie dodatkowe wątki. 2 jest domyślnym zachowaniem SIGINT dla programów skompilowanych przez GHC. 3 to, jak działają wątki w Haskell. 1 jest całkowitym domysłem.Odpowiedzi:
Kiedy używasz
stack run
, Stack efektywnie używaexec
wywołania systemowego do przeniesienia kontroli do pliku wykonywalnego, więc proces nowego pliku wykonywalnego zastępuje działający proces Stack, tak jakbyś uruchomił plik wykonywalny bezpośrednio z powłoki. Oto, jak wygląda drzewo procesustack run
. W szczególności zauważ, że plik wykonywalny jest bezpośrednim potomkiem powłoki Bash. Co bardziej krytyczne, należy pamiętać, że pierwszoplanową grupą procesów terminalu (TPGID) jest 17996, a jedynym procesem w tej grupie procesów (PGID) jestbracket-test-exe
proces.W rezultacie, gdy naciśniesz Ctrl-C, aby przerwać proces działający pod
stack run
powłoką lub bezpośrednio z powłoki, sygnał SIGINT jest dostarczany tylko dobracket-test-exe
procesu. Rodzi toUserInterrupt
wyjątek asynchroniczny . Sposóbbracket
działa, gdy:odbiera asynchroniczny wyjątek podczas przetwarzania
body
, uruchamia się,release
a następnie ponownie zgłasza wyjątek. W przypadku zagnieżdżonychbracket
wywołań skutkuje to przerwaniem wewnętrznego ciała, przetworzeniem wewnętrznego wydania, ponownym podniesieniem wyjątku w celu przerwania zewnętrznego ciała i przetworzeniem zewnętrznego wydania, a na koniec ponownym zgłoszeniem wyjątku w celu zakończenia programu. (Gdybybracket
w twojejmain
funkcji było więcej akcji , nie byłyby one wykonane).Z drugiej strony, gdy używasz
stack test
, Stack używawithProcessWait
do uruchomienia pliku wykonywalnego jako proces potomnystack test
procesu. W poniższym drzewie procesów zwróć uwagę, żebracket-test-test
jest to proces potomnystack test
. Krytycznie, pierwszą grupą procesów terminala jest 18050, a ta grupa procesów obejmuje zarównostack test
proces, jak ibracket-test-test
proces.Kiedy trafisz Ctrl-C w terminalu, sygnał SIGINT wysyłany jest do wszystkich procesów w grupie procesów planie terminalu więc zarówno
stack test
ibracket-test-test
dostać sygnał.bracket-test-test
rozpocznie przetwarzanie sygnału i uruchomi finalizatory, jak opisano powyżej. Istnieje jednak warunek wyścigu, ponieważ gdystack test
zostanie przerwany, jest w środku,withProcessWait
który jest zdefiniowany mniej więcej w następujący sposób:więc gdy
bracket
jest przerwany, wywołuje,stopProcess
co kończy proces potomny wysyłając muSIGTERM
sygnał. W przeciwieństwie doSIGINT
tego nie wywołuje to asynchronicznego wyjątku. Po prostu natychmiast kończy to dziecko, na ogół zanim może zakończyć uruchamianie jakichkolwiek finalizatorów.Nie mogę wymyślić szczególnie łatwego sposobu obejścia tego. Jednym ze sposobów jest wykorzystanie urządzeń
System.Posix
do umieszczenia procesu we własnej grupie procesów:Teraz Ctrl-C spowoduje dostarczenie SIGINT tylko do
bracket-test-test
procesu. Sprząta, przywróci oryginalną grupę procesów pierwszego planu, aby wskazywała nastack test
proces, i zakończy działanie. Spowoduje to, że test się nie powiedzie istack test
po prostu będzie działać.Alternatywą może być próba obsłużenia
SIGTERM
i utrzymania procesu potomnego w celu przeprowadzenia czyszczenia, nawet po zakończeniustack test
procesu. Jest to trochę brzydkie, ponieważ proces będzie przypominał czyszczenie w tle podczas patrzenia na monit powłoki.źródło
stack test
uruchamianie procesów zdelegate_ctlc
opcjąSystem.Process
(lub coś podobnego).